lib/gif/src/test.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const gif = @import("root.zig");
3
4 const DecodeBounds = gif.DecodeBounds;
5 const DecodePlan = gif.DecodePlan;
6 const DecodeStorage = gif.DecodeStorage;
7 const Animation = gif.Animation;
8 const Error = gif.Error;
9 const animationFromBytes = gif.animationFromBytes;
10 const AnimationView = gif.AnimationView;
11 const FrameView = gif.FrameView;
12 const EncodeStorage = gif.EncodeStorage;
13 const EncodeScratch = gif.EncodeScratch;
14 const bytesFromAnimation = gif.bytesFromAnimation;
15
16 test "gif package namespace" {
17 std.testing.refAllDecls(gif);
18 _ = @import("decode/test.zig");
19 _ = @import("encode/test.zig");
20 }
21
22 const witness_static = [_]u8{
23 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x02, 0x00, 0x02, 0x00, 0xf1, 0x03, 0x00, 0xff, 0x00, 0x00,
24 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x21, 0xf9, 0x04, 0x00, 0x00, 0x00, 0x00,
25 0x00, 0x21, 0xff, 0x0b, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x61, 0x67, 0x69, 0x63, 0x6b, 0x0e,
26 0x67, 0x61, 0x6d, 0x6d, 0x61, 0x3d, 0x30, 0x2e, 0x34, 0x35, 0x34, 0x35, 0x34, 0x35, 0x00, 0x2c,
27 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x02, 0x03, 0x44, 0x34, 0x05, 0x00, 0x3b,
28 };
29
30 const witness_animation = [_]u8{
31 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x02, 0x00, 0x01, 0x00, 0xf0, 0x00, 0x00, 0xff, 0x00, 0x00,
32 0x00, 0x00, 0x00, 0x21, 0xff, 0x0b, 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e,
33 0x30, 0x03, 0x01, 0x02, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x19, 0x00, 0x00, 0x00, 0x21, 0xff,
34 0x0b, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x61, 0x67, 0x69, 0x63, 0x6b, 0x0e, 0x67, 0x61, 0x6d,
35 0x6d, 0x61, 0x3d, 0x30, 0x2e, 0x34, 0x35, 0x34, 0x35, 0x34, 0x35, 0x00, 0x2c, 0x00, 0x00, 0x00,
36 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x04, 0x0a, 0x00, 0x21, 0xf9, 0x04, 0x00, 0x19,
37 0x00, 0x00, 0x00, 0x21, 0xff, 0x0b, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x61, 0x67, 0x69, 0x63,
38 0x6b, 0x0e, 0x67, 0x61, 0x6d, 0x6d, 0x61, 0x3d, 0x30, 0x2e, 0x34, 0x35, 0x34, 0x35, 0x34, 0x35,
39 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0xff, 0x00, 0x00,
40 0x00, 0x02, 0x02, 0x04, 0x0a, 0x00, 0x3b,
41 };
42
43 const test_decode_bounds = DecodeBounds{
44 .canvas_pixels = 16,
45 .frame_pixels = 16,
46 .frames = 4,
47 .compressed_bytes = 64,
48 .retained_rgba8_bytes = 256,
49 };
50
51 const TestDecode = struct {
52 storage: DecodeStorage,
53 animation: Animation,
54
55 fn init(allocator: std.mem.Allocator, bytes: []const u8) !TestDecode {
56 var storage = try DecodeStorage.init(allocator, .{
57 .bytes = bytes,
58 .bounds = test_decode_bounds,
59 });
60 errdefer storage.deinit(allocator);
61 storage.activate();
62 const animation = try animationFromBytes(&storage, bytes);
63 return .{ .storage = storage, .animation = animation };
64 }
65
66 fn deinit(self: *TestDecode, allocator: std.mem.Allocator) void {
67 self.storage.reset();
68 self.storage.deinit(allocator);
69 self.* = undefined;
70 }
71 };
72
73 test "decodes an ImageMagick static witness" {
74 const allocator = std.testing.allocator;
75 var decoded = try TestDecode.init(allocator, &witness_static);
76 defer decoded.deinit(allocator);
77 const animation = decoded.animation;
78
79 try std.testing.expectEqual(@as(u32, 2), animation.width);
80 try std.testing.expectEqual(@as(u32, 2), animation.height);
81 try std.testing.expectEqual(@as(usize, 1), animation.frames.len);
82 try std.testing.expectEqual(@as(?u16, null), animation.loop_count);
83 try std.testing.expectEqualSlices(u8, &.{
84 255, 0, 0, 255, 0, 255, 0, 255,
85 0, 0, 255, 255, 255, 255, 255, 255,
86 }, animation.frames[0].rgba8);
87 }
88
89 test "decodes an ImageMagick animation witness" {
90 const allocator = std.testing.allocator;
91 var decoded = try TestDecode.init(allocator, &witness_animation);
92 defer decoded.deinit(allocator);
93 const animation = decoded.animation;
94
95 try std.testing.expectEqual(@as(u32, 2), animation.width);
96 try std.testing.expectEqual(@as(u32, 1), animation.height);
97 try std.testing.expectEqual(@as(usize, 2), animation.frames.len);
98 try std.testing.expectEqual(@as(?u16, 2), animation.loop_count);
99 try std.testing.expectEqual(@as(u16, 25), animation.frames[0].delay_cs);
100 try std.testing.expectEqual(@as(u16, 25), animation.frames[1].delay_cs);
101 try std.testing.expectEqualSlices(u8, &.{ 255, 0, 0, 255, 255, 0, 0, 255 }, animation.frames[0].rgba8);
102 try std.testing.expectEqualSlices(u8, &.{ 0, 0, 255, 255, 0, 0, 255, 255 }, animation.frames[1].rgba8);
103 }
104
105 test "rejects non-gif bytes" {
106 try std.testing.expectError(Error.InvalidSignature, DecodePlan.inspect("PNG then some", test_decode_bounds));
107 try std.testing.expectError(Error.TruncatedInput, DecodePlan.inspect(witness_static[0..20], test_decode_bounds));
108 }
109
110 test "encoding the decoded ImageMagick witness reproduces its frames" {
111 const allocator = std.testing.allocator;
112 var original_decode = try TestDecode.init(allocator, &witness_animation);
113 defer original_decode.deinit(allocator);
114 const original = original_decode.animation;
115
116 var views = try allocator.alloc(FrameView, original.frames.len);
117 defer allocator.free(views);
118 for (original.frames, 0..) |frame, i| {
119 views[i] = .{ .rgba8 = frame.rgba8, .delay_cs = frame.delay_cs };
120 }
121
122 const view = AnimationView{
123 .width = original.width,
124 .height = original.height,
125 .frames = views,
126 .loop_count = original.loop_count,
127 };
128 var scratch: EncodeScratch = undefined;
129 var encode_storage = try EncodeStorage.init(allocator, .{
130 .animation = view,
131 .bounds = .{
132 .canvas_pixels = @as(usize, original.width) * @as(usize, original.height),
133 .frames = original.frames.len,
134 .palette_entries = 256,
135 },
136 .scratch = &scratch,
137 });
138 defer encode_storage.deinit(allocator);
139 encode_storage.activate();
140 const bytes = try bytesFromAnimation(&encode_storage, view);
141 defer encode_storage.reset();
142
143 var reencoded_decode = try TestDecode.init(allocator, bytes);
144 defer reencoded_decode.deinit(allocator);
145 const reencoded = reencoded_decode.animation;
146
147 try std.testing.expectEqual(original.width, reencoded.width);
148 try std.testing.expectEqual(original.height, reencoded.height);
149 try std.testing.expectEqual(original.loop_count, reencoded.loop_count);
150 try std.testing.expectEqual(original.frames.len, reencoded.frames.len);
151 for (original.frames, reencoded.frames) |expected, actual| {
152 try std.testing.expectEqual(expected.delay_cs, actual.delay_cs);
153 try std.testing.expectEqualSlices(u8, expected.rgba8, actual.rgba8);
154 }
155 }
156
157 test "GIF root composes caller-selected decode bounds and storage" {
158 comptime {
159 @stardustClaim(
160 @import("alloc_phase").capacity.witness(@import("./decode/root.zig").Storage, "gif_decode_root"),
161 null,
162 null,
163 null,
164 null,
165 null,
166 null,
167 );
168 }
169
170 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
171 const allocator = counting.allocator();
172 const plan = try DecodePlan.inspect(&witness_animation, test_decode_bounds);
173 var storage = try DecodeStorage.init(allocator, .{
174 .bytes = &witness_animation,
175 .bounds = plan.exactBounds(),
176 });
177 defer storage.deinit(allocator);
178 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
179 try std.testing.expectEqual(storage.status().storage_bytes, counting.allocated_bytes);
180 try std.testing.expectEqual(plan.canvas_pixels, storage.status().canvas_pixels);
181 try std.testing.expectEqual(plan.frames, storage.status().frames);
182 storage.activate();
183 const animation = try animationFromBytes(&storage, &witness_animation);
184 defer storage.reset();
185 try std.testing.expectEqual(plan.width, animation.width);
186 try std.testing.expectEqual(plan.height, animation.height);
187 }
188
189 test "GIF root composes caller-selected encode bounds and storage" {
190 comptime {
191 @stardustClaim(
192 @import("alloc_phase").capacity.witness(@import("./encode/root.zig").Storage, "gif_encode_root"),
193 null,
194 null,
195 null,
196 null,
197 null,
198 null,
199 );
200 }
201
202 const rgba = [_]u8{
203 251, 73, 52, 255,
204 0, 0, 0, 0,
205 };
206 const animation = AnimationView{
207 .width = 2,
208 .height = 1,
209 .frames = &.{.{ .rgba8 = &rgba }},
210 };
211 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
212 var scratch: EncodeScratch = undefined;
213 var storage = try EncodeStorage.init(counting.allocator(), .{
214 .animation = animation,
215 .bounds = .{ .canvas_pixels = 2, .frames = 1, .palette_entries = 2 },
216 .scratch = &scratch,
217 });
218 defer storage.deinit(counting.allocator());
219 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
220 storage.activate();
221 const bytes = try bytesFromAnimation(&storage, animation);
222 defer storage.reset();
223 try std.testing.expectEqualSlices(u8, "GIF89a", bytes[0..6]);
224 try std.testing.expectEqual(storage.status().output_bytes, bytes.len);
225 }