exr-zig

Use EXR images
git clone git://git.electrosoup.com/exr-zig
Log | Files | Refs

commit 0e1cb264983a6d6e7d5dfabb6290f2d299cbd8b0
parent 2948465e35e00a2b2aca59599b770e67b15eb9f1
Author: Christian Ermann <christianermann@gmail.com>
Date:   Sat, 16 Aug 2025 14:18:27 -0700

Add header parsing

Diffstat:
Asample.exr | 0
Asrc/exr.zig | 506+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.zig | 23++++-------------------
Msrc/root.zig | 2++
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());