decodeRadianceHdr function Assets and loading

DecodedHdr decodeRadianceHdr(
  1. Uint8List bytes, {
  2. int? maxWidth,
})

Decodes a Radiance .hdr/.pic RGBE image from bytes.

When maxWidth is set and the source is wider, the image is box-downsampled by an integer factor during decode (averaging in linear space) so a very large panorama (e.g. 16K) never materializes at full resolution. A realtime environment map does not need more than a few thousand pixels wide.

Implementation

DecodedHdr decodeRadianceHdr(Uint8List bytes, {int? maxWidth}) {
  var pos = 0;

  String readLine() {
    final sb = StringBuffer();
    while (pos < bytes.length) {
      final c = bytes[pos++];
      if (c == 0x0a) break; // newline
      sb.writeCharCode(c);
    }
    return sb.toString();
  }

  final magic = readLine();
  if (!magic.startsWith('#?')) {
    throw HdrFormatException('Not a Radiance HDR file (bad magic)');
  }
  // Header lines until a blank line; we only need the format.
  String? format;
  while (pos < bytes.length) {
    final line = readLine();
    if (line.isEmpty) break;
    if (line.startsWith('FORMAT=')) format = line.substring(7).trim();
  }
  if (format != null && format != '32-bit_rle_rgbe') {
    throw HdrFormatException('Unsupported HDR format "$format"');
  }

  final resolution = readLine();
  // Expect "-Y height +X width" (the standard top-down, left-right layout).
  final match = RegExp(r'^-Y\s+(\d+)\s+\+X\s+(\d+)$').firstMatch(resolution);
  if (match == null) {
    throw HdrFormatException('Unsupported HDR orientation "$resolution"');
  }
  final height = int.parse(match.group(1)!);
  final width = int.parse(match.group(2)!);
  if (width <= 0 || height <= 0) {
    throw HdrFormatException('Invalid HDR dimensions ${width}x$height');
  }

  // Integer box-downsample factor: every factor x factor source block averages
  // to one output texel. Output dimensions drop the partial trailing block.
  final factor = (maxWidth != null && maxWidth > 0 && width > maxWidth)
      ? (width + maxWidth - 1) ~/ maxWidth
      : 1;
  final outWidth = width ~/ factor;
  final outHeight = height ~/ factor;

  final pixels = Float32List(outWidth * outHeight * 4); // linear accumulator
  final scanline = Uint8List(width * 4); // RGBE for one source row

  for (var y = 0; y < height; y++) {
    pos = _readScanline(bytes, pos, scanline, width);
    final outY = y ~/ factor;
    if (outY >= outHeight) continue; // trailing partial block
    final rowOffset = outY * outWidth * 4;
    for (var x = 0; x < width; x++) {
      final outX = x ~/ factor;
      if (outX >= outWidth) continue;
      final e = scanline[x * 4 + 3];
      if (e == 0) continue;
      // RGBE -> linear float: component / 256 * 2^(exponent - 128).
      final scale = _ldexp(1.0, e - (128 + 8));
      final o = rowOffset + outX * 4;
      pixels[o] += scanline[x * 4 + 0] * scale;
      pixels[o + 1] += scanline[x * 4 + 1] * scale;
      pixels[o + 2] += scanline[x * 4 + 2] * scale;
    }
  }

  // Normalize the box sum and set opaque alpha (factor 1 is a plain copy).
  final norm = 1.0 / (factor * factor);
  for (var i = 0; i < pixels.length; i += 4) {
    pixels[i] *= norm;
    pixels[i + 1] *= norm;
    pixels[i + 2] *= norm;
    pixels[i + 3] = 1.0;
  }
  return DecodedHdr(pixels, outWidth, outHeight);
}