lib/gif/src/encode/plan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const lzw = @import("gif_lzw");
3 const model = @import("model.zig");
4
5 pub const Bounds = struct {
6 canvas_pixels: usize,
7 frames: usize,
8 palette_entries: usize,
9 };
10
11 pub const Error = model.EncodeError || model.Exhaustion || lzw.Error;
12
13 pub const Plan = struct {
14 input_hash: u64,
15 width: u32,
16 height: u32,
17 canvas_pixels: usize,
18 frames: usize,
19 palette_entries: usize,
20 table_entries: usize,
21 max_compressed_bytes: usize,
22 output_bytes: usize,
23 bounds: Bounds,
24 palette: model.Palette,
25
26 pub fn inspect(
27 animation: model.AnimationView,
28 bounds: Bounds,
29 scratch: *lzw.Scratch,
30 ) Error!Plan {
31 return inspectDictionary(animation, bounds, scratch.dictionary());
32 }
33
34 pub fn inspectDictionary(
35 animation: model.AnimationView,
36 bounds: Bounds,
37 dictionary: lzw.Dictionary,
38 ) Error!Plan {
39 if (animation.width == 0 or animation.height == 0) return error.InvalidDimensions;
40 if (animation.width > std.math.maxInt(u16) or animation.height > std.math.maxInt(u16)) {
41 return error.InvalidDimensions;
42 }
43 if (animation.frames.len == 0) return error.NoFrames;
44
45 const canvas_pixels = try multiplied(animation.width, animation.height);
46 if (canvas_pixels > bounds.canvas_pixels) return error.CanvasPixelCapacityExceeded;
47 if (animation.frames.len > bounds.frames) return error.FrameCapacityExceeded;
48 const rgba8_bytes = try multiplied(canvas_pixels, 4);
49 for (animation.frames) |frame| {
50 if (frame.rgba8.len != rgba8_bytes) return error.FrameSizeMismatch;
51 }
52
53 const palette = try model.Palette.build(animation.frames);
54 const palette_entries = palette.entryCount();
55 if (palette_entries > bounds.palette_entries) return error.PaletteCapacityExceeded;
56 const table_entries = palette.tableEntries();
57 var output_bytes = try added(13, try multiplied(table_entries, 3));
58 if (animation.loop_count != null) output_bytes = try added(output_bytes, 19);
59
60 var max_compressed_bytes: usize = 0;
61 for (animation.frames) |frame| {
62 const source = model.PixelSource{ .palette = &palette, .rgba8 = frame.rgba8 };
63 const compressed = try lzw.measureSource(palette.minCodeSize(), source, dictionary);
64 max_compressed_bytes = @max(max_compressed_bytes, compressed);
65 output_bytes = try added(output_bytes, try frameBytes(compressed));
66 }
67 output_bytes = try added(output_bytes, 1);
68
69 return .{
70 .input_hash = animationHash(animation),
71 .width = animation.width,
72 .height = animation.height,
73 .canvas_pixels = canvas_pixels,
74 .frames = animation.frames.len,
75 .palette_entries = palette_entries,
76 .table_entries = table_entries,
77 .max_compressed_bytes = max_compressed_bytes,
78 .output_bytes = output_bytes,
79 .bounds = bounds,
80 .palette = palette,
81 };
82 }
83
84 pub fn exactBounds(self: Plan) Bounds {
85 return .{
86 .canvas_pixels = self.canvas_pixels,
87 .frames = self.frames,
88 .palette_entries = self.palette_entries,
89 };
90 }
91 };
92
93 fn frameBytes(compressed_bytes: usize) lzw.Error!usize {
94 const block_count = try dividedUp(compressed_bytes, 255);
95 var bytes = try added(19, compressed_bytes);
96 bytes = try added(bytes, block_count);
97 return try added(bytes, 1);
98 }
99
100 fn animationHash(animation: model.AnimationView) u64 {
101 var hash = std.hash.Wyhash.init(0);
102 hash.update(std.mem.asBytes(&animation.width));
103 hash.update(std.mem.asBytes(&animation.height));
104 const has_loop: u8 = @intFromBool(animation.loop_count != null);
105 hash.update(std.mem.asBytes(&has_loop));
106 if (animation.loop_count) |loop_count| hash.update(std.mem.asBytes(&loop_count));
107 const frame_count = animation.frames.len;
108 hash.update(std.mem.asBytes(&frame_count));
109 for (animation.frames) |frame| {
110 hash.update(std.mem.asBytes(&frame.delay_cs));
111 hash.update(frame.rgba8);
112 }
113 return hash.final();
114 }
115
116 fn added(left: usize, right: usize) lzw.Error!usize {
117 return std.math.add(usize, left, right) catch error.CapacityOverflow;
118 }
119
120 fn multiplied(left: anytype, right: anytype) lzw.Error!usize {
121 return std.math.mul(usize, @intCast(left), @intCast(right)) catch error.CapacityOverflow;
122 }
123
124 fn dividedUp(numerator: usize, denominator: usize) lzw.Error!usize {
125 const adjusted = try added(numerator, denominator - 1);
126 return adjusted / denominator;
127 }