Created
August 5, 2025 23:16
-
-
Save Opioid/3436c037ab159d96d1b3e3310863b965 to your computer and use it in GitHub Desktop.
RGBE reader
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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