Skip to content

Instantly share code, notes, and snippets.

@Opioid
Created August 5, 2025 23:16
Show Gist options
  • Select an option

  • Save Opioid/3436c037ab159d96d1b3e3310863b965 to your computer and use it in GitHub Desktop.

Select an option

Save Opioid/3436c037ab159d96d1b3e3310863b965 to your computer and use it in GitHub Desktop.
RGBE reader
const std = @import("std");
const Allocator = std.mem.Allocator;
pub fn main() !void {
const alloc = std.heap.c_allocator;
{
var file = try std.fs.cwd().openFile("sunny_morning_spherical.hdr", .{});
defer file.close();
try RgbeDeprecatedReader.read(alloc, file);
}
{
var file = try std.fs.cwd().openFile("sunny_morning_spherical.hdr", .{});
defer file.close();
try RgbeReader.read(alloc, &file);
}
}
const Header = struct {
width: u32,
height: u32,
};
const Pack3f = struct {
v: [3]f32,
};
const Image = struct {
pixels: []Pack3f,
};
const Error = error{
BadInitialToken,
MissingFormatSpecifier,
MissingImageSizeSpecifier,
WrongScanlineWidth,
BadScanlineData,
};
fn averageColor(image: Image) Pack3f {
var average: Pack3f = .{ .v = .{ 0.0, 0.0, 0.0 } };
const flen: f32 = @floatFromInt(image.pixels.len);
for (image.pixels) |p| {
average.v[0] += p.v[0] / flen;
average.v[1] += p.v[1] / flen;
average.v[2] += p.v[2] / flen;
}
return average;
}
const RgbeReader = struct {
pub fn read(alloc: Allocator, file: *std.fs.File) !void {
const start = std.time.milliTimestamp();
var buffer: [4096]u8 = undefined;
var stream = file.reader(&buffer);
const reader = &stream.interface;
const header = try readHeader(reader);
std.debug.print("dimensions {}x{}\n", .{ header.width, header.height });
var image = Image{ .pixels = try alloc.alloc(Pack3f, header.width * header.height) };
defer alloc.free(image.pixels);
try readPixelsRLE(alloc, reader, header.width, header.height, &image);
std.debug.print("read time {}\n", .{std.time.milliTimestamp() - start});
const average = averageColor(image);
std.debug.print("average {}\n", .{average});
}
fn readHeader(stream: *std.Io.Reader) !Header {
var buf: [128]u8 = undefined;
{
var writer: std.Io.Writer = .fixed(&buf);
_ = try stream.streamDelimiterLimit(&writer, '\n', .limited(buf.len));
stream.toss(1);
if (!std.mem.startsWith(u8, writer.buffered(), "#?")) {
return Error.BadInitialToken;
}
}
var format_specifier: bool = false;
while (true) {
var writer: std.Io.Writer = .fixed(&buf);
_ = try stream.streamDelimiterLimit(&writer, '\n', .limited(buf.len));
stream.toss(1);
const line = writer.buffered();
if (0 == line.len or 0 == line[0]) {
// blank lines signifies end of meta data header
break;
}
if (std.mem.eql(u8, line, "FORMAT=32-bit_rle_rgbe")) {
format_specifier = true;
}
}
if (!format_specifier) {
return Error.MissingFormatSpecifier;
}
var writer: std.Io.Writer = .fixed(&buf);
_ = try stream.streamDelimiterLimit(&writer, '\n', .limited(buf.len));
stream.toss(1);
var line = writer.buffered();
var i = (std.mem.indexOfScalar(u8, line, ' ') orelse return Error.MissingImageSizeSpecifier) + 1;
if (!std.mem.eql(u8, line[0..i], "-Y ")) {
return Error.MissingImageSizeSpecifier;
}
line = line[i..];
i = (std.mem.indexOfScalar(u8, line, ' ') orelse return Error.MissingImageSizeSpecifier);
const height = std.fmt.parseInt(u32, line[0..i], 0) catch return Error.MissingImageSizeSpecifier;
line = line[i + 1 ..];
i = (std.mem.indexOfScalar(u8, line, ' ') orelse return Error.MissingImageSizeSpecifier) + 1;
if (!std.mem.eql(u8, line[0..i], "+X ")) {
return Error.MissingImageSizeSpecifier;
}
const width = std.fmt.parseInt(u32, line[i..], 0) catch return Error.MissingImageSizeSpecifier;
return Header{ .width = width, .height = height };
}
fn readPixelsRLE(
alloc: Allocator,
stream: *std.Io.Reader,
scanline_width: u32,
num_scanlines: u32,
image: *Image,
) !void {
if (scanline_width < 8 or scanline_width > 0x7fff) {
return try readPixels(stream, scanline_width * num_scanlines, image, 0);
}
var offset: u32 = 0;
var rgbe: [4]u8 = undefined;
var buf: [2]u8 = undefined;
var scanline_buffer = try alloc.alloc(u8, 4 * scanline_width);
defer alloc.free(scanline_buffer);
var s = num_scanlines;
while (s > 0) : (s -= 1) {
_ = try stream.readSliceShort(&rgbe);
if (rgbe[0] != 2 or rgbe[1] != 2 or (rgbe[2] & 0x80) != 0) {
// this file is not run length encoded
image.pixels[offset] = rgbeToPack3f(rgbe);
return try readPixels(stream, scanline_width * num_scanlines - 1, image, 1);
}
if ((@as(u32, rgbe[2]) << 8 | @as(u32, rgbe[3])) != scanline_width) {
return Error.WrongScanlineWidth;
}
// read each of the four channels for the scanline into the buffer
var c: u32 = 0;
var index: u32 = 0;
while (c < 4) : (c += 1) {
const end = (c + 1) * scanline_width;
while (index < end) {
_ = try stream.readSliceShort(&buf);
if (buf[0] > 128) {
// a run of the same value
var count = @as(u32, buf[0]) - 128;
if (count == 0 or count > end - index) {
return Error.BadScanlineData;
}
while (count > 0) : (count -= 1) {
scanline_buffer[index] = buf[1];
index += 1;
}
} else {
// a non-run
var count = @as(u32, buf[0]);
if (count == 0 or count > end - index) {
return Error.BadScanlineData;
}
scanline_buffer[index] = buf[1];
index += 1;
count -= 1;
if (count > 0) {
_ = try stream.readSliceShort(scanline_buffer[index .. index + count]);
index += count;
}
}
}
}
// now convert data from buffer into floats
for (0..scanline_width) |i| {
rgbe[0] = scanline_buffer[i];
rgbe[1] = scanline_buffer[i + scanline_width];
rgbe[2] = scanline_buffer[i + 2 * scanline_width];
rgbe[3] = scanline_buffer[i + 3 * scanline_width];
image.pixels[offset] = rgbeToPack3f(rgbe);
offset += 1;
}
}
}
fn readPixels(stream: *std.Io.Reader, num_pixels: u32, image: *Image, offset: u32) !void {
var rgbe: [4]u8 = undefined;
var i = num_pixels;
var o = offset;
while (i > 0) : (i -= 1) {
_ = try stream.readSliceShort(&rgbe);
image.pixels[o] = rgbeToPack3f(rgbe);
o += 1;
}
}
};
const RgbeDeprecatedReader = struct {
pub fn read(alloc: Allocator, file: std.fs.File) !void {
const start = std.time.milliTimestamp();
var stream: std.io.BufferedReader(4096, std.fs.File.DeprecatedReader) = undefined;
stream.start = 0;
stream.end = 0;
stream.unbuffered_reader = file.deprecatedReader();
const reader = stream.reader();
const header = try readHeader(reader);
std.debug.print("dimensions {}x{}\n", .{ header.width, header.height });
var image = Image{ .pixels = try alloc.alloc(Pack3f, header.width * header.height) };
defer alloc.free(image.pixels);
try readPixelsRLE(alloc, reader, header.width, header.height, &image);
std.debug.print("read time {}\n", .{std.time.milliTimestamp() - start});
const average = averageColor(image);
std.debug.print("average {}\n", .{average});
}
fn readHeader(stream: anytype) !Header {
var buf: [128]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buf);
{
fbs.reset();
try stream.streamUntilDelimiter(fbs.writer(), '\n', buf.len);
if (!std.mem.startsWith(u8, fbs.getWritten(), "#?")) {
return Error.BadInitialToken;
}
}
var format_specifier: bool = false;
while (true) {
fbs.reset();
try stream.streamUntilDelimiter(fbs.writer(), '\n', buf.len);
const line = fbs.getWritten();
if (0 == line.len or 0 == line[0]) {
// blank lines signifies end of meta data header
break;
}
if (std.mem.eql(u8, line, "FORMAT=32-bit_rle_rgbe")) {
format_specifier = true;
}
}
if (!format_specifier) {
return Error.MissingFormatSpecifier;
}
fbs.reset();
try stream.streamUntilDelimiter(fbs.writer(), '\n', buf.len);
var line = fbs.getWritten();
var i = (std.mem.indexOfScalar(u8, line, ' ') orelse return Error.MissingImageSizeSpecifier) + 1;
if (!std.mem.eql(u8, line[0..i], "-Y ")) {
return Error.MissingImageSizeSpecifier;
}
line = line[i..];
i = (std.mem.indexOfScalar(u8, line, ' ') orelse return Error.MissingImageSizeSpecifier);
const height = std.fmt.parseInt(u32, line[0..i], 0) catch return Error.MissingImageSizeSpecifier;
line = line[i + 1 ..];
i = (std.mem.indexOfScalar(u8, line, ' ') orelse return Error.MissingImageSizeSpecifier) + 1;
if (!std.mem.eql(u8, line[0..i], "+X ")) {
return Error.MissingImageSizeSpecifier;
}
const width = std.fmt.parseInt(u32, line[i..], 0) catch return Error.MissingImageSizeSpecifier;
return Header{ .width = width, .height = height };
}
fn readPixelsRLE(
alloc: Allocator,
stream: anytype,
scanline_width: u32,
num_scanlines: u32,
image: *Image,
) !void {
if (scanline_width < 8 or scanline_width > 0x7fff) {
return try readPixels(stream, scanline_width * num_scanlines, image, 0);
}
var offset: u32 = 0;
var rgbe: [4]u8 = undefined;
var buf: [2]u8 = undefined;
var scanline_buffer = try alloc.alloc(u8, 4 * scanline_width);
defer alloc.free(scanline_buffer);
var s = num_scanlines;
while (s > 0) : (s -= 1) {
_ = try stream.readAll(&rgbe);
if (rgbe[0] != 2 or rgbe[1] != 2 or (rgbe[2] & 0x80) != 0) {
// this file is not run length encoded
image.pixels[offset] = rgbeToPack3f(rgbe);
return try readPixels(stream, scanline_width * num_scanlines - 1, image, 1);
}
if ((@as(u32, rgbe[2]) << 8 | @as(u32, rgbe[3])) != scanline_width) {
return Error.WrongScanlineWidth;
}
// read each of the four channels for the scanline into the buffer
var c: u32 = 0;
var index: u32 = 0;
while (c < 4) : (c += 1) {
const end = (c + 1) * scanline_width;
while (index < end) {
_ = try stream.readAll(&buf);
if (buf[0] > 128) {
// a run of the same value
var count = @as(u32, buf[0]) - 128;
if (count == 0 or count > end - index) {
return Error.BadScanlineData;
}
while (count > 0) : (count -= 1) {
scanline_buffer[index] = buf[1];
index += 1;
}
} else {
// a non-run
var count = @as(u32, buf[0]);
if (count == 0 or count > end - index) {
return Error.BadScanlineData;
}
scanline_buffer[index] = buf[1];
index += 1;
count -= 1;
if (count > 0) {
_ = try stream.readAll(scanline_buffer[index .. index + count]);
index += count;
}
}
}
}
// now convert data from buffer into floats
for (0..scanline_width) |i| {
rgbe[0] = scanline_buffer[i];
rgbe[1] = scanline_buffer[i + scanline_width];
rgbe[2] = scanline_buffer[i + 2 * scanline_width];
rgbe[3] = scanline_buffer[i + 3 * scanline_width];
image.pixels[offset] = rgbeToPack3f(rgbe);
offset += 1;
}
}
}
fn readPixels(stream: anytype, num_pixels: u32, image: *Image, offset: u32) !void {
var rgbe: [4]u8 = undefined;
var i = num_pixels;
var o = offset;
while (i > 0) : (i -= 1) {
_ = try stream.readAll(&rgbe);
image.pixels[o] = rgbeToPack3f(rgbe);
o += 1;
}
}
};
// https://cbloomrants.blogspot.com/2020/06/widespread-error-in-radiance-hdr-rgbe.html
fn rgbeToPack3f(rgbe: [4]u8) Pack3f {
if (rgbe[3] > 0) {
// nonzero pixel
const f = std.math.scalbn(@as(f32, 1.0), @as(i32, rgbe[3]) - (128 + 8));
return .{
.v = .{
(@as(f32, @floatFromInt(rgbe[0])) + 0.5) * f,
(@as(f32, @floatFromInt(rgbe[1])) + 0.5) * f,
(@as(f32, @floatFromInt(rgbe[2])) + 0.5) * f,
},
};
}
return .{ .v = .{ 0.0, 0.0, 0.0 } };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment