Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

Conversation

@braheezy
Copy link
Contributor

Here lies a functional GIF writer. It includes:

  • LZW encoder, as required by the format, with tests
  • GIF encoder
    • options for looping and converting color formats the required palette-based (indexed) format. This conversion is false by default...I think that's right? At least, it's an option.
    • Use of a global_palette to track shared colors across frames, ultimately saving space
  • GIF encoding tests

As I did with the JPEG writer, this is a loose port of the GIF encoder from the Go standard library.

Closes #11


To manually test and see the results, here's a client program that takes in a GIF and writes it back out, the reads it again.

Client program
const std = @import("std");
const zigimg = @import("zigimg");

var read_buffer: [4096]u8 = undefined;
var write_buffer: [4096]u8 = undefined;

pub fn main() !void {
    const allocator = std.heap.page_allocator;
    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    if (args.len != 3) {
        std.debug.print("Usage: {s} <input_file> <output_file>\n", .{args[0]});
        return;
    }

    const input_file = args[1];
    const output_file = args[2];

    var input_stream = try std.fs.cwd().openFile(input_file, .{});
    defer input_stream.close();

    std.debug.print("Reading GIF from {s}...\n", .{input_file});

    var image = try zigimg.Image.fromFile(allocator, input_stream, &read_buffer);
    defer image.deinit(allocator);

    std.debug.print("Read GIF: {d}x{d}, format: {s}\n", .{ image.width, image.height, @tagName(image.pixels) });

    // Print palette info if indexed
    switch (image.pixels) {
        .indexed1 => |data| std.debug.print("Palette size: {d} colors\n", .{data.palette.len}),
        .indexed2 => |data| std.debug.print("Palette size: {d} colors\n", .{data.palette.len}),
        .indexed4 => |data| std.debug.print("Palette size: {d} colors\n", .{data.palette.len}),
        .indexed8 => |data| std.debug.print("Palette size: {d} colors\n", .{data.palette.len}),
        .indexed16 => |data| std.debug.print("Palette size: {d} colors\n", .{data.palette.len}),
        else => {},
    }

    std.debug.print("Writing GIF to {s}...\n", .{output_file});

    try image.writeToFilePath(allocator, output_file, &write_buffer, .{ .gif = .{ .auto_convert = true } });

    std.debug.print("GIF successfully written to {s}\n", .{output_file});

    // Test: Try to read the GIF back to verify it's valid
    std.debug.print("Testing round-trip: reading back the GIF...\n", .{});
    var read_file = try std.fs.cwd().openFile(output_file, .{});
    defer read_file.close();
    var read_back = try zigimg.Image.fromFile(allocator, read_file, &read_buffer);
    defer read_back.deinit(allocator);
    std.debug.print("Successfully read back GIF: {d}x{d}, format: {s}\n", .{ read_back.width, read_back.height, @tagName(read_back.pixels) });
}

Example output:

./zig-out/bin/gif_client rotating_earth.gif out.gif 
Reading GIF from rotating_earth.gif...
Read GIF: 400x400, format: indexed8
Palette size: 256 colors
Writing GIF to out.gif...
GIF successfully written to out.gif
Testing round-trip: reading back the GIF...
Successfully read back GIF: 400x400, format: indexed8

Copy link
Collaborator

@mlarouche mlarouche left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for doing this! See my comments for the requested changes.


// Hash table: maps (prefix_code << 8 | byte) -> code
// Entry format: (key << 12) | code, where key = (prefix << 8 | byte)
table: [table_size]u32 = [_]u32{invalid_entry} ** table_size,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use table: [table_size]u32 = @splat(invalid_entry), here

pub const AnimationFrame = struct {
pixels: color.PixelStorage,
duration: f32,
/// Frame disposal method (format-specific, 0 = none/unspecified)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first I was a bit afraid it is too GIF specific but this looks that it should be common enough for another animated format such as Animated PNG.


/// Increment next code and handle overflow/reset
/// Returns true if table was reset (out of codes)
fn incNextCode(self: *Self, writer: *std.Io.Writer) Error!bool {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incNextCode -> incrementNextCode

return .rgb24;
}

fn writeHeader(writer: *std.Io.Writer, image: Image, pixels: *const color.PixelStorage, loop_count: i32) Image.WriteError!void {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

writeHeader should not dependent of Image

_ = write_stream;
_ = image;
_ = encoder_options;
const writer = write_stream.writer();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no way to write GIF without using Image. This function should fill out the fields already present in GIF and the write function should use those fields like frames, comments, application_infos, global_color_table. So you can write GIF that have filled those fields manually.

var converted_pixels: ?color.PixelStorage = null;
defer if (converted_pixels) |pixels| pixels.deinit(allocator);

var pixels_to_use = &image.pixels;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auto conversion only applies to the first image in an animation, is that intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was not intended, maybe leftover behavior from the tiny animated GIFs I tested with.

}
};

const paletteEntryCounts = [_]usize{ 2, 4, 8, 16, 32, 64, 128, 256 };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be palette_entry_counts or PALETTE_ENTRY_COUNTS


fn writeLoopExtension(writer: *std.Io.Writer, loop_count: u16) Image.WriteError!void {
try writer.writeAll(&[_]u8{
0x21, // Extension Introducer.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should either pack those into a extern struct for a data block header or use the enums already present in this file.

Example:

const data_block_header: ExtensionDataHeader = .{
.data_kind = .extension,
.extension_kind = .application_extension,
.block_size = 0x0b,

@mlarouche mlarouche merged commit 9714df0 into zigimg:master Jan 5, 2026
6 checks passed
@mlarouche
Copy link
Collaborator

Thanks again @braheezy !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Write GIF

2 participants