Skip to content

Instantly share code, notes, and snippets.

@revivalizer
Created August 12, 2025 06:57
Show Gist options
  • Select an option

  • Save revivalizer/a46b3622dad751c68afbab396ba8065d to your computer and use it in GitHub Desktop.

Select an option

Save revivalizer/a46b3622dad751c68afbab396ba8065d to your computer and use it in GitHub Desktop.
Zig IFF loader
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