commit 0e1cb264983a6d6e7d5dfabb6290f2d299cbd8b0
parent 2948465e35e00a2b2aca59599b770e67b15eb9f1
Author: Christian Ermann <christianermann@gmail.com>
Date: Sat, 16 Aug 2025 14:18:27 -0700
Add header parsing
Diffstat:
4 files changed, 512 insertions(+), 19 deletions(-)
diff --git a/sample.exr b/sample.exr
Binary files differ.
diff --git a/src/exr.zig b/src/exr.zig
@@ -0,0 +1,506 @@
+const std = @import("std");
+
+const Version = packed struct(u32) {
+ format_version: u8,
+ single_part_tiled: bool,
+ long_names: bool,
+ non_image: bool,
+ multipart: bool,
+ padding: u20,
+
+ pub fn imageType(this: *const Version) !ImageType {
+ if (this.single_part_tiled and (this.non_image or this.multipart)) {
+ return error.InvalidImageType;
+ }
+ if (this.single_part_tiled) {
+ return .single_part_tile;
+ }
+ if (this.non_image and this.multipart) {
+ return .multi_part_deep;
+ }
+ if (this.non_image) {
+ return .single_part_deep;
+ }
+ if (this.multipart) {
+ return .multi_part;
+ }
+ return .single_part_scan_line;
+ }
+};
+
+const ImageType = enum {
+ single_part_scan_line,
+ single_part_tile,
+ multi_part,
+ single_part_deep,
+ multi_part_deep,
+};
+
+const AttributeType = enum {
+ box2i,
+ box2f,
+ bytes,
+ chlist,
+ chromaticities,
+ compression,
+ double,
+ env_map,
+ float,
+ int,
+ key_code,
+ line_order,
+ m33f,
+ m44f,
+ preview,
+ rational,
+ string,
+ string_vector,
+ tile_desc,
+ time_code,
+ v2i,
+ v2f,
+ v3i,
+ v3f,
+};
+
+const AttributeValue = union(AttributeType) {
+ box2i: Box2i,
+ box2f: Box2f,
+ bytes: Bytes,
+ chlist: []Channel,
+ chromaticities: Chromaticities,
+ compression: Compression,
+ double: f64,
+ env_map: EnvMap,
+ float: f32,
+ int: i32,
+ key_code: KeyCode,
+ line_order: LineOrder,
+ m33f: [9]f32,
+ m44f: [16]f32,
+ preview: Preview,
+ rational: Rational,
+ string: String,
+ string_vector: []String,
+ tile_desc: TileDesc,
+ time_code: TimeCode,
+ v2i: [2]i32,
+ v2f: [2]f32,
+ v3i: [3]i32,
+ v3f: [3]f32,
+};
+
+const Box2i = packed struct {
+ x_min: i32,
+ y_min: i32,
+ x_max: i32,
+ y_max: i32,
+};
+
+const Box2f = packed struct {
+ x_min: f32,
+ y_min: f32,
+ x_max: f32,
+ y_max: f32,
+};
+
+const Bytes = struct {
+};
+
+const Channel = struct {
+ name: String,
+ pixel_type: u32,
+ p_linear: u8,
+ reserved: [3]u8 = .{ 0, 0, 0 },
+ x_sampling: u32,
+ y_sampling: u32,
+
+ pub fn parse(reader: anytype) !?Channel {
+ const name_string = try String.parseNullTerminated(reader);
+ if (name_string.len == 0) {
+ return null;
+ }
+ const pixel_type = try reader.readInt(u32, .little);
+ const p_linear = try reader.readInt(u8, .little);
+ try reader.skipBytes(3, .{});
+ const x_sampling = try reader.readInt(u32, .little);
+ const y_sampling = try reader.readInt(u32, .little);
+ return .{
+ .name = name_string,
+ .pixel_type = pixel_type,
+ .p_linear = p_linear,
+ .x_sampling = x_sampling,
+ .y_sampling = y_sampling,
+ };
+ }
+};
+
+const Chromaticities = struct {
+ red_x: f32,
+ red_y: f32,
+ green_x: f32,
+ green_y: f32,
+ blue_x: f32,
+ blue_y: f32,
+ white_x: f32,
+ white_y: f32,
+};
+
+const Compression = enum(u8) {
+ none = 0,
+ rle = 1,
+ zips = 2,
+ zip = 3,
+ piz = 4,
+ pxr24 = 5,
+ b44 = 6,
+ b44a = 7,
+ dwaa = 8,
+ dwab = 9,
+ htj2k = 10,
+};
+
+const EnvMap = enum(u8) {
+ lat_long = 0,
+ cube = 1,
+};
+
+const KeyCode = struct {
+ film_mfc_code: i32,
+ film_type: i32,
+ prefix: i32,
+ count: i32,
+ perf_offset: i32,
+ perfs_per_frame: i32,
+ perfs_per_count: i32,
+};
+
+const LineOrder = enum(u8) {
+ increasing_y = 0,
+ decreasing_y = 1,
+ random_y = 2,
+};
+
+const Preview = struct {
+ width: u32,
+ height: u32,
+ pixels: []const u8,
+};
+
+const Rational = struct {
+ integer: i32,
+ fraction: u32,
+};
+
+const String = struct {
+ len: u32,
+ buf: [255]u8,
+
+ pub fn parseNullTerminated(reader: anytype) !String {
+ var string = String{ .len = 0, .buf = undefined };
+ var stream = std.io.fixedBufferStream(&string.buf);
+ const writer = stream.writer();
+ try reader.streamUntilDelimiter(writer, 0, string.buf.len);
+ string.len = @intCast(stream.pos);
+ return string;
+ }
+
+ pub fn parseLength(reader: anytype, len: u32) !String {
+ var string = String{ .len = len, .buf = undefined };
+ try reader.readNoEof(string.buf[0..string.len]);
+ return string;
+ }
+
+ pub fn slice(string: *const String) []const u8 {
+ return string.buf[0..string.len];
+ }
+};
+
+const LevelMode = enum(u4) {
+ one = 0,
+ mipmap = 1,
+ ripmap = 2,
+};
+
+const RoundingMode = enum(u4) {
+ down = 0,
+ up = 1,
+};
+
+const TileDesc = packed struct {
+ x_size: u32,
+ y_size: u32,
+ level_mode: LevelMode,
+ rounding_mode: RoundingMode,
+};
+
+const TimeCode = struct {
+ time_and_flags: u32,
+ user_data: u32,
+};
+
+const Attribute = struct {
+ name: String,
+ value: AttributeValue,
+
+ pub fn parse(reader: anytype, allocator: std.mem.Allocator) !?Attribute {
+ const name_string = try String.parseNullTerminated(reader);
+ if (name_string.len == 0) {
+ return null;
+ }
+ const type_string = try String.parseNullTerminated(reader);
+ const size = try reader.readInt(u32, .little);
+ const value = try Attribute.parseValue(
+ type_string.slice(),
+ size,
+ reader,
+ allocator,
+ );
+ return .{
+ .name = name_string,
+ .value = value,
+ };
+ }
+
+ fn parseValue(
+ type_str: []const u8,
+ size: u32,
+ reader: anytype,
+ allocator: std.mem.Allocator,
+ ) !AttributeValue {
+ if (std.mem.eql(u8, type_str, "box2i")) {
+ const value = try reader.readStruct(Box2i);
+ return .{ .box2i = value };
+ }
+ else if (std.mem.eql(u8, type_str, "box2f")) {
+ const value = try reader.readStruct(Box2f);
+ return .{ .box2f = value };
+ }
+ else if (std.mem.eql(u8, type_str, "bytes")) {
+ return error.NotImplemented;
+ }
+ else if (std.mem.eql(u8, type_str, "chlist")) {
+ var list = std.ArrayList(Channel).init(allocator);
+ errdefer list.deinit();
+ while (try Channel.parse(reader)) |*channel| {
+ try list.append(channel.*);
+ }
+ const channels = try list.toOwnedSlice();
+ return .{ .chlist = channels };
+ }
+ else if (std.mem.eql(u8, type_str, "chromaticities")) {
+ return error.NotImplemented;
+ }
+ else if (std.mem.eql(u8, type_str, "compression")) {
+ const value_as_int = try reader.readInt(u8, .little);
+ const value = @as(Compression, @enumFromInt(value_as_int));
+ return .{ .compression = value };
+ }
+ else if (std.mem.eql(u8, type_str, "double")) {
+ var bytes: [8]u8 = undefined;
+ try reader.readNoEof(&bytes);
+ const value = std.mem.bytesToValue(f32, &bytes);
+ return .{ .double = value };
+ }
+ else if (std.mem.eql(u8, type_str, "envmap")) {
+ return error.NotImplemented;
+ }
+ else if (std.mem.eql(u8, type_str, "float")) {
+ var bytes: [4]u8 = undefined;
+ try reader.readNoEof(&bytes);
+ const value = std.mem.bytesToValue(f32, &bytes);
+ return .{ .float = value };
+ }
+ else if (std.mem.eql(u8, type_str, "int")) {
+ return error.NotImplemented;
+ }
+ else if (std.mem.eql(u8, type_str, "keycode")) {
+ return error.NotImplemented;
+ }
+ else if (std.mem.eql(u8, type_str, "lineOrder")) {
+ const value_as_int = try reader.readInt(u8, .little);
+ const value = @as(LineOrder, @enumFromInt(value_as_int));
+ return .{ .line_order = value };
+ }
+ else if (std.mem.eql(u8, type_str, "m33f")) {
+ return error.NotImplemented;
+ }
+ else if (std.mem.eql(u8, type_str, "m44f")) {
+ return error.NotImplemented;
+ }
+ else if (std.mem.eql(u8, type_str, "preview")) {
+ return error.NotImplemented;
+ }
+ else if (std.mem.eql(u8, type_str, "rational")) {
+ return error.NotImplemented;
+ }
+ else if (std.mem.eql(u8, type_str, "string")) {
+ const string = try String.parseLength(reader, size);
+ return .{ .string = string };
+ }
+ else if (std.mem.eql(u8, type_str, "stringvector")) {
+ return error.NotImplemented;
+ }
+ else if (std.mem.eql(u8, type_str, "tiledesc")) {
+ return error.NotImplemented;
+ }
+ else if (std.mem.eql(u8, type_str, "timecode")) {
+ return error.NotImplemented;
+ }
+ else if (std.mem.eql(u8, type_str, "v2i")) {
+ const value0 = try reader.readInt(i32, .little);
+ const value1 = try reader.readInt(i32, .little);
+ return .{ .v2i = .{ value0, value1 } };
+ }
+ else if (std.mem.eql(u8, type_str, "v2f")) {
+ var bytes: [8]u8 = undefined;
+ try reader.readNoEof(&bytes);
+ const value0 = std.mem.bytesToValue(f32, &bytes[0..4]);
+ const value1 = std.mem.bytesToValue(f32, &bytes[4..8]);
+ return .{ .v2f = .{ value0, value1 } };
+ }
+ else if (std.mem.eql(u8, type_str, "v3i")) {
+ const value0 = try reader.readInt(i32, .little);
+ const value1 = try reader.readInt(i32, .little);
+ const value2 = try reader.readInt(i32, .little);
+ return .{ .v3i = .{ value0, value1, value2 } };
+ }
+ else if (std.mem.eql(u8, type_str, "v3f")) {
+ var bytes: [12]u8 = undefined;
+ try reader.readNoEof(&bytes);
+ const value0 = std.mem.bytesToValue(f32, &bytes[0..4]);
+ const value1 = std.mem.bytesToValue(f32, &bytes[4..8]);
+ const value2 = std.mem.bytesToValue(f32, &bytes[8..12]);
+ return .{ .v3f = .{ value0, value1, value2 } };
+ }
+ else {
+ return error.InvalidAttributeType;
+ }
+ }
+};
+
+const AttributeSet = packed struct(u32) {
+ channels: bool = false,
+ compression: bool = false,
+ data_window: bool = false,
+ display_window: bool = false,
+ line_order: bool = false,
+ pixel_aspect_ratio: bool = false,
+ screen_window_center: bool = false,
+ screen_window_width: bool = false,
+ _padding: u24 = 0,
+};
+
+const required_attributes = AttributeSet{
+ .channels = true,
+ .compression = true,
+ .data_window = true,
+ .display_window = true,
+ .line_order = true,
+ .pixel_aspect_ratio = true,
+ .screen_window_center = true,
+ .screen_window_width = true,
+};
+
+// TODO: decide on a strategy for handling attributes for non-scanline types
+const Header = struct {
+ channels: []Channel,
+ compression: Compression,
+ data_window: Box2i,
+ display_window: Box2i,
+ line_order: LineOrder,
+ pixel_aspect_ratio: f32,
+ screen_window_center: [2]f32,
+ screen_window_width: f32,
+
+ pub fn parse(reader: anytype, allocator: std.mem.Allocator) !Header {
+ var attrib_set = AttributeSet{};
+ var header: Header = undefined;
+ while (try Attribute.parse(reader, allocator)) |*attrib| {
+ if (std.mem.eql(u8, attrib.name.slice(), "channels")) {
+ if (attrib_set.channels) {
+ return error.DuplicateAttribute;
+ }
+ header.channels = attrib.value.chlist;
+ attrib_set.channels = true;
+ }
+ else if (std.mem.eql(u8, attrib.name.slice(), "compression")) {
+ if (attrib_set.compression) {
+ return error.DuplicateAttribute;
+ }
+ header.compression = attrib.value.compression;
+ attrib_set.compression = true;
+ }
+ else if (std.mem.eql(u8, attrib.name.slice(), "dataWindow")) {
+ if (attrib_set.data_window) {
+ return error.DuplicateAttribute;
+ }
+ header.data_window = attrib.value.box2i;
+ attrib_set.data_window = true;
+ }
+ else if (std.mem.eql(u8, attrib.name.slice(), "displayWindow")) {
+ if (attrib_set.display_window) {
+ return error.DuplicateAttribute;
+ }
+ header.display_window = attrib.value.box2i;
+ attrib_set.display_window = true;
+ }
+ else if (std.mem.eql(u8, attrib.name.slice(), "lineOrder")) {
+ if (attrib_set.line_order) {
+ return error.DuplicateAttribute;
+ }
+ header.line_order = attrib.value.line_order;
+ attrib_set.line_order = true;
+ }
+ else if (std.mem.eql(u8, attrib.name.slice(), "pixelAspectRatio")) {
+ if (attrib_set.pixel_aspect_ratio) {
+ return error.DuplicateAttribute;
+ }
+ header.pixel_aspect_ratio = attrib.value.float;
+ attrib_set.pixel_aspect_ratio = true;
+ }
+ else if (std.mem.eql(u8, attrib.name.slice(), "screenWindowCenter")) {
+ if (attrib_set.screen_window_center) {
+ return error.DuplicateAttribute;
+ }
+ header.screen_window_center = attrib.value.v2f;
+ attrib_set.screen_window_center = true;
+ }
+ else if (std.mem.eql(u8, attrib.name.slice(), "screenWindowWidth")) {
+ if (attrib_set.screen_window_width) {
+ return error.DuplicateAttribute;
+ }
+ header.screen_window_width = attrib.value.float;
+ attrib_set.screen_window_width = true;
+ }
+ }
+ if (attrib_set != required_attributes) {
+ return error.MissingAttributes;
+ }
+ return header;
+ }
+};
+
+pub fn loadFile(path: []const u8, allocator: std.mem.Allocator) !void {
+ var file = try std.fs.cwd().openFile(path, .{ .mode = .read_only });
+ defer file.close();
+
+ var source = std.io.StreamSource{ .file = file };
+ var reader = std.io.bufferedReader(source.reader());
+ var stream = reader.reader();
+
+ const magic = try stream.readInt(u32, .little);
+
+ std.debug.print("magic = {}\n", .{magic});
+
+ const version = try stream.readStruct(Version);
+
+ const image_type = version.imageType();
+
+ std.debug.print("version = {}\n", .{version});
+ std.debug.print("type = {any}\n", .{image_type});
+
+ // TODO: multipart files have a list of headers
+ const header = try Header.parse(stream, allocator);
+ std.debug.print("header = {}\n", .{header});
+}
diff --git a/src/main.zig b/src/main.zig
@@ -1,24 +1,9 @@
const std = @import("std");
+const exr = @import("exr.zig");
pub fn main() !void {
- // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`)
- std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ const allocator = gpa.allocator();
- // stdout is for the actual output of your application, for example if you
- // are implementing gzip, then only the compressed bytes should be sent to
- // stdout, not any debugging messages.
- const stdout_file = std.io.getStdOut().writer();
- var bw = std.io.bufferedWriter(stdout_file);
- const stdout = bw.writer();
-
- try stdout.print("Run `zig build test` to run the tests.\n", .{});
-
- try bw.flush(); // don't forget to flush!
-}
-
-test "simple test" {
- var list = std.ArrayList(i32).init(std.testing.allocator);
- defer list.deinit(); // try commenting this out and see if zig detects the memory leak!
- try list.append(42);
- try std.testing.expectEqual(@as(i32, 42), list.pop());
+ try exr.loadFile("sample.exr", allocator);
}
diff --git a/src/root.zig b/src/root.zig
@@ -3,6 +3,8 @@ const testing = std.testing;
pub const lut = @import("piz/lut.zig");
pub const huffman = @import("piz/huffman.zig");
+pub const wavelet = @import("piz/wavelet.zig");
+pub const exr = @import("exr.zig");
test {
std.testing.refAllDecls(@This());