decodeRadianceHdr function Assets and loading
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);
}