Created
August 12, 2025 06:57
-
-
Save revivalizer/a46b3622dad751c68afbab396ba8065d to your computer and use it in GitHub Desktop.
Zig IFF loader
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"); | |
| // This struct is not used (can't reuse directly because of endianess differences), but serves as a reference | |
| const bitmap_header = packed struct { | |
| Width: u16, | |
| Height: u16, | |
| Left: i16, | |
| Top: i16, | |
| Depth: u8, | |
| Masking: masking, | |
| Compression: compression, | |
| Pad1: u8, | |
| TransparentColor: u16, | |
| XAspect: u8, | |
| YAspect: u8, | |
| PageWidth: u16, | |
| PageHeight: u16, | |
| const compression = enum(u8) { | |
| cmpNone = 0, | |
| cmpByteRun1 = 1, | |
| }; | |
| const masking = enum(u8) { | |
| mskNone = 0, | |
| mskHasMask = 1, | |
| mskHasTransparentColor = 2, | |
| mskLasso = 3, | |
| }; | |
| }; | |
| pub const iff_image = struct { | |
| Width: u32, | |
| Height: u32, | |
| Data: []u8, // RGB byte triplets in one long array | |
| }; | |
| fn ReadChunkName(DataPtr: *[]const u8) [4]u8 { | |
| var Result: [4]u8 = undefined; | |
| for (0..4) |i| { | |
| Result[i] = DataPtr.*[i]; | |
| } | |
| DataPtr.*.ptr += 4; | |
| return Result; | |
| } | |
| fn ReadChunkSize(DataPtr: *[]const u8) u32 { | |
| const Result = @as(u32, @intCast(DataPtr.*[0])) << 24 | @as(u32, @intCast(DataPtr.*[1])) << 16 | @as(u32, @intCast(DataPtr.*[2])) << 8 | @as(u32, @intCast(DataPtr.*[3])); | |
| DataPtr.*.ptr += 4; | |
| return Result; | |
| } | |
| fn ReadU16(DataPtr: *[]const u8) u16 { | |
| var Result: u16 = undefined; | |
| Result = @as(u16, @as(u16, @intCast(DataPtr.*[0])) << 8 | @as(u16, @intCast(DataPtr.*[1]))); | |
| DataPtr.*.ptr += 2; | |
| return Result; | |
| } | |
| fn ReadI16(DataPtr: *[]const u8) i16 { | |
| var Result: i16 = undefined; | |
| Result = @as(i16, @as(i16, @intCast(DataPtr.*[0])) << 8 | @as(i16, @intCast(DataPtr.*[1]))); | |
| DataPtr.*.ptr += 2; | |
| return Result; | |
| } | |
| fn ReadU8(DataPtr: *[]const u8) u8 { | |
| const Result = DataPtr.*[0]; | |
| DataPtr.*.ptr += 1; | |
| return Result; | |
| } | |
| // Reference: https://1fish2.github.io/IFF/IFF%20docs%20with%20Commodore%20revisions/ILBM.pdf | |
| pub fn ReadIFF(IFFData: []const u8, Allocator: std.mem.Allocator) !iff_image { | |
| var Data: []const u8 = IFFData; | |
| var IFFImage: iff_image = undefined; | |
| // File header | |
| const FileIdentifier = ReadChunkName(&Data); | |
| if (!std.mem.eql(u8, "FORM", &FileIdentifier)) | |
| return error.EXPECTED_FORM_CHUNK; | |
| _ = ReadChunkSize(&Data); | |
| // ILBM header | |
| const ILBMHeaderName = ReadChunkName(&Data); | |
| if (!std.mem.eql(u8, "ILBM", &ILBMHeaderName)) | |
| return error.EXPECTED_ILBM_CHUNK; | |
| // Data we'll read in earlier chunks and use in BODY chunk | |
| var NumPlanes: u32 = undefined; | |
| var NumColors: u32 = undefined; | |
| var Palette: []const u8 = undefined; | |
| while (true) { | |
| const ChunkName = ReadChunkName(&Data); | |
| const ChunkSize = ReadChunkSize(&Data); | |
| // Chunk: Bitmap Header | |
| if (std.mem.eql(u8, "BMHD", &ChunkName)) { | |
| var HeaderData = Data; | |
| IFFImage.Width = ReadU16(&HeaderData); | |
| IFFImage.Height = ReadU16(&HeaderData); | |
| IFFImage.Data = Allocator.alloc(u8, IFFImage.Width * IFFImage.Height * 3) catch return error.ALLOC_FAILED; | |
| _ = ReadI16(&HeaderData); // Left | |
| _ = ReadI16(&HeaderData); // Top | |
| NumPlanes = ReadU8(&HeaderData); | |
| const Masking = ReadU8(&HeaderData); | |
| if (Masking != @intFromEnum(bitmap_header.masking.mskNone)) | |
| return error.MASK_TYPE_NOT_IMPLEMENTED; | |
| const Compression = ReadU8(&HeaderData); | |
| if (Compression != @intFromEnum(bitmap_header.compression.cmpByteRun1)) | |
| return error.UNSUPPORTED_COMPRESSION; | |
| _ = ReadU8(&HeaderData); // Padding | |
| _ = ReadU16(&HeaderData); // Transparent color | |
| _ = ReadU8(&HeaderData); // X aspect | |
| _ = ReadU8(&HeaderData); // Y aspect | |
| _ = ReadU16(&HeaderData); // Page width | |
| _ = ReadU16(&HeaderData); // Page height | |
| // TODO: We could set the pitch here | |
| } | |
| // Chunk: Color Map - palette | |
| else if (std.mem.eql(u8, "CMAP", &ChunkName)) { | |
| NumColors = ChunkSize / 3; | |
| // TODO: Could assert that NumColors is 2^NumPlanes | |
| Palette = Data[0..NumColors * 3]; | |
| } | |
| // Chunk: Body - pixels | |
| else if (std.mem.eql(u8, "BODY", &ChunkName)) { | |
| var BodyData = Data.ptr; | |
| // Bytes may be compressed, so we read forward order and spread into pixels | |
| // Palette lookup is done as a separate step | |
| // Clear colors | |
| for (0..IFFImage.Height) |y| { | |
| for (0..IFFImage.Width) |x| { | |
| const PixelOffset = (y * IFFImage.Width + x); | |
| IFFImage.Data[PixelOffset * 3] = 0; | |
| } | |
| } | |
| // Read compressed bitplanes | |
| for (0..IFFImage.Height) |y| { | |
| for (0..NumPlanes) |plane_| { | |
| const plane: u3 = @intCast(plane_); | |
| var x: u32 = 0; | |
| while (x < IFFImage.Width) { | |
| const n = @as(*const i8, @ptrCast(BodyData)).*; | |
| BodyData += 1; | |
| if (n >= 0) { | |
| // Copy n+1 literal bytes | |
| for (0..@intCast(n+1)) |_| { | |
| const Byte = BodyData[0]; | |
| BodyData += 1; | |
| for (0..8) |bit_| { | |
| const bit: u3 = @intCast(bit_); | |
| const PixelOffset = y * IFFImage.Width + x; | |
| IFFImage.Data[PixelOffset * 3] = IFFImage.Data[PixelOffset * 3] | (((Byte >> (7 - bit)) & 1) << plane); | |
| x += 1; // Each byte spreads to 8 pixels | |
| } | |
| } | |
| } else if (n < 0) { | |
| // Repeat next byte -n+1 times | |
| const Byte = BodyData[0]; | |
| BodyData += 1; | |
| for (0..@intCast(-n+1)) |_| { | |
| for (0..8) |bit_| { | |
| const bit: u3 = @intCast(bit_); | |
| const PixelOffset = y * IFFImage.Width + x; | |
| IFFImage.Data[PixelOffset * 3] = IFFImage.Data[PixelOffset * 3] | (((Byte >> (7 - bit)) & 1) << plane); | |
| x += 1; // Each byte spreads to 8 pixels | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Palette lookup | |
| for (0..IFFImage.Height) |y| { | |
| for (0..IFFImage.Width) |x| { | |
| const PixelOffset = (y * IFFImage.Width + x); | |
| const ColorIndex = IFFImage.Data[PixelOffset * 3]; | |
| IFFImage.Data[PixelOffset * 3 + 0] = Palette[ColorIndex * 3 + 0]; | |
| IFFImage.Data[PixelOffset * 3 + 1] = Palette[ColorIndex * 3 + 1]; | |
| IFFImage.Data[PixelOffset * 3 + 2] = Palette[ColorIndex * 3 + 2]; | |
| } | |
| } | |
| return IFFImage; | |
| } | |
| Data.ptr += ChunkSize; | |
| } | |
| return IFFImage; | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment