lib/png/src/decode/test.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const flate = std.compress.flate;
  3 const png = @import("../root.zig");
  4 const subject = @import("root.zig");
  5 pub const fixture = @import("fixture.zig");
  6 const encode = png.encode;
  7 const Bounds = subject.Bounds;
  8 const Storage = subject.Storage;
  9 const Decoding = subject.Decoding;
 10 const imageFromBytes = subject.imageFromBytes;
 11 
 12 test "decode round-trips the encoder output" {
 13     const width = 5;
 14     const height = 3;
 15     var source_pixels: [width * height * 4]u8 = undefined;
 16     for (&source_pixels, 0..) |*byte, index| byte.* = @intCast((index * 7) % 251);
 17     var encoded = try encode.Encoding.init(std.testing.allocator, .{
 18         .rgba8 = &source_pixels,
 19         .width = width,
 20         .height = height,
 21     });
 22     defer encoded.deinit(std.testing.allocator);
 23     var decoded = try Decoding.init(std.testing.allocator, encoded.bytes);
 24     defer decoded.deinit(std.testing.allocator);
 25     try std.testing.expectEqual(@as(u32, width), decoded.image.width);
 26     try std.testing.expectEqual(@as(u32, height), decoded.image.height);
 27     try std.testing.expectEqualSlices(u8, &source_pixels, decoded.image.rgba8);
 28 }
 29 
 30 test "decode reads a real indexed PNG past its ancillary chunks" {
 31     const witness = @import("fixture.zig").indexed;
 32     var decoded = try Decoding.init(std.testing.allocator, &witness);
 33     defer decoded.deinit(std.testing.allocator);
 34     try std.testing.expectEqual(@as(u32, 2), decoded.image.width);
 35     try std.testing.expectEqual(@as(u32, 2), decoded.image.height);
 36     try std.testing.expectEqualSlices(u8, &.{
 37         255, 0, 0,   255, 0,   255, 0,   255,
 38         0,   0, 255, 255, 255, 255, 255, 255,
 39     }, decoded.image.rgba8);
 40 }
 41 
 42 test "PNG decode storage rejects concurrent use and resets after decode" {
 43     comptime {
 44         @stardustClaim(
 45             @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "png_decode_reuse"),
 46             null,
 47             null,
 48             null,
 49             null,
 50             null,
 51             null,
 52         );
 53     }
 54 
 55     const witness = @import("fixture.zig").indexed;
 56     var storage = try Storage.init(std.testing.allocator, .{
 57         .bytes = &witness,
 58         .bounds = try Bounds.exact(&witness),
 59     });
 60     defer storage.deinit(std.testing.allocator);
 61     storage.activate();
 62     const first = try imageFromBytes(&storage, &witness);
 63     try std.testing.expectError(error.DecodeStorageInUse, imageFromBytes(&storage, &witness));
 64     const hash = std.hash.Wyhash.hash(0, first.rgba8);
 65     storage.reset();
 66     const second = try imageFromBytes(&storage, &witness);
 67     defer storage.reset();
 68     try std.testing.expectEqual(hash, std.hash.Wyhash.hash(0, second.rgba8));
 69 }
 70 
 71 test "Activated PNG decode storage performs no backing allocation" {
 72     comptime {
 73         @stardustClaim(
 74             @import("alloc_phase").capacity.witness(@import("./root.zig").Storage, "png_decode_sealed"),
 75             null,
 76             null,
 77             null,
 78             null,
 79             null,
 80             null,
 81         );
 82     }
 83 
 84     const witness = @import("fixture.zig").indexed;
 85     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
 86     var storage = try Storage.init(counting.allocator(), .{
 87         .bytes = &witness,
 88         .bounds = try Bounds.exact(&witness),
 89     });
 90     defer storage.deinit(counting.allocator());
 91     storage.activate();
 92     const image = try imageFromBytes(&storage, &witness);
 93     defer storage.reset();
 94     try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
 95     try std.testing.expectEqual(@as(usize, 0), counting.resize_index);
 96     try std.testing.expectEqual(storage.status().rgba8_bytes, image.rgba8.len);
 97 }
 98 
 99 test "decode rejects bad input" {
100     try std.testing.expectError(error.InvalidSignature, Bounds.exact("not a png"));
101 }
102 
103 test "bounded decoding rejects excess capacity before allocation" {
104     const witness = @import("fixture.zig").indexed;
105     var bounds = try Bounds.exact(&witness);
106     bounds.image_pixels -= 1;
107     var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
108     try std.testing.expectError(
109         error.ImagePixelCapacityExceeded,
110         Decoding.initWithBounds(counting.allocator(), &witness, bounds),
111     );
112     try std.testing.expectEqual(@as(usize, 0), counting.alloc_index);
113 }
114 
115 test "decode expands RGB PNG rows to RGBA" {
116     const source = [_]u8{ 10, 20, 30, 40, 50, 60 };
117     const encoded = try testPngAlloc(std.testing.allocator, 2, 1, 8, 2, 3, &source, &.{}, &.{});
118     defer std.testing.allocator.free(encoded);
119     var decoded = try Decoding.init(std.testing.allocator, encoded);
120     defer decoded.deinit(std.testing.allocator);
121     try std.testing.expectEqualSlices(u8, &.{
122         10, 20, 30, 255,
123         40, 50, 60, 255,
124     }, decoded.image.rgba8);
125 }
126 
127 test "decode expands grayscale alpha PNG rows to RGBA" {
128     const source = [_]u8{ 10, 128, 40, 255 };
129     const encoded = try testPngAlloc(std.testing.allocator, 2, 1, 8, 4, 2, &source, &.{}, &.{});
130     defer std.testing.allocator.free(encoded);
131     var decoded = try Decoding.init(std.testing.allocator, encoded);
132     defer decoded.deinit(std.testing.allocator);
133     try std.testing.expectEqualSlices(u8, &.{
134         10, 10, 10, 128,
135         40, 40, 40, 255,
136     }, decoded.image.rgba8);
137 }
138 
139 test "decode expands an indexed PNG through its palette" {
140     const indices = [_]u8{0b00011011};
141     const palette = [_]u8{
142         255, 0, 0,   0,   255, 0,
143         0,   0, 255, 255, 255, 255,
144     };
145     const encoded = try testPngAlloc(
146         std.testing.allocator,
147         4,
148         1,
149         2,
150         3,
151         1,
152         &indices,
153         &palette,
154         &.{},
155     );
156     defer std.testing.allocator.free(encoded);
157     var decoded = try Decoding.init(std.testing.allocator, encoded);
158     defer decoded.deinit(std.testing.allocator);
159     try std.testing.expectEqualSlices(u8, &.{
160         255, 0, 0,   255, 0,   255, 0,   255,
161         0,   0, 255, 255, 255, 255, 255, 255,
162     }, decoded.image.rgba8);
163 }
164 
165 test "decode honors palette transparency" {
166     const indices = [_]u8{0b00000001};
167     const palette = [_]u8{ 10, 20, 30, 40, 50, 60 };
168     const transparency = [_]u8{ 0, 128 };
169     const encoded = try testPngAlloc(
170         std.testing.allocator,
171         2,
172         1,
173         4,
174         3,
175         1,
176         &indices,
177         &palette,
178         &transparency,
179     );
180     defer std.testing.allocator.free(encoded);
181     var decoded = try Decoding.init(std.testing.allocator, encoded);
182     defer decoded.deinit(std.testing.allocator);
183     try std.testing.expectEqualSlices(u8, &.{
184         10, 20, 30, 0,
185         40, 50, 60, 128,
186     }, decoded.image.rgba8);
187 }
188 
189 test "decode scales a 1-bit grayscale PNG" {
190     const source = [_]u8{0b10000000};
191     const encoded = try testPngAlloc(std.testing.allocator, 2, 1, 1, 0, 1, &source, &.{}, &.{});
192     defer std.testing.allocator.free(encoded);
193     var decoded = try Decoding.init(std.testing.allocator, encoded);
194     defer decoded.deinit(std.testing.allocator);
195     try std.testing.expectEqualSlices(u8, &.{
196         255, 255, 255, 255,
197         0,   0,   0,   255,
198     }, decoded.image.rgba8);
199 }
200 
201 test "decode reduces a 16-bit truecolor PNG to eight bits" {
202     const source = [_]u8{ 0xab, 0xcd, 0x12, 0x34, 0xff, 0x00 };
203     const encoded = try testPngAlloc(std.testing.allocator, 1, 1, 16, 2, 3, &source, &.{}, &.{});
204     defer std.testing.allocator.free(encoded);
205     var decoded = try Decoding.init(std.testing.allocator, encoded);
206     defer decoded.deinit(std.testing.allocator);
207     try std.testing.expectEqualSlices(u8, &.{ 0xab, 0x12, 0xff, 255 }, decoded.image.rgba8);
208 }
209 
210 test "decode requires a palette for indexed color" {
211     const source = [_]u8{0};
212     const encoded = try testPngAlloc(std.testing.allocator, 1, 1, 8, 3, 1, &source, &.{}, &.{});
213     defer std.testing.allocator.free(encoded);
214     try std.testing.expectError(error.MissingPalette, Bounds.exact(encoded));
215 }
216 
217 test "decode streams consecutive IDAT chunks without concatenation" {
218     const source = [_]u8{ 10, 20, 30, 40, 50, 60 };
219     const encoded = try testPngAlloc(std.testing.allocator, 2, 1, 8, 2, 3, &source, &.{}, &.{});
220     defer std.testing.allocator.free(encoded);
221     const split = try splitIdatAlloc(std.testing.allocator, encoded);
222     defer std.testing.allocator.free(split);
223     var decoded = try Decoding.init(std.testing.allocator, split);
224     defer decoded.deinit(std.testing.allocator);
225     try std.testing.expectEqualSlices(u8, &.{
226         10, 20, 30, 255,
227         40, 50, 60, 255,
228     }, decoded.image.rgba8);
229 }
230 
231 test "PNG decode rejects changed input before output mutation" {
232     const witness = @import("fixture.zig").indexed;
233     var storage = try Storage.init(std.testing.allocator, .{
234         .bytes = &witness,
235         .bounds = try Bounds.exact(&witness),
236     });
237     defer storage.deinit(std.testing.allocator);
238     storage.activate();
239     @memset(storage.output, 0xa5);
240     const output_hash = std.hash.Wyhash.hash(0, storage.output);
241     var changed = witness;
242     changed[changed.len - 1] ^= 1;
243     try std.testing.expectError(error.DecodeInputMismatch, imageFromBytes(&storage, &changed));
244     try std.testing.expectEqual(output_hash, std.hash.Wyhash.hash(0, storage.output));
245     _ = try imageFromBytes(&storage, &witness);
246     storage.reset();
247 }
248 
249 test "PNG decode errors release storage before retry" {
250     const witness = @import("fixture.zig").indexed;
251     var corrupted = witness;
252     corrupted[try testChunkDataOffset(&corrupted, "IDAT")] = 0;
253     var storage = try Storage.init(std.testing.allocator, .{
254         .bytes = &corrupted,
255         .bounds = try Bounds.exact(&corrupted),
256     });
257     defer storage.deinit(std.testing.allocator);
258     storage.activate();
259     try std.testing.expectError(error.TruncatedImage, imageFromBytes(&storage, &corrupted));
260     try std.testing.expect(!storage.status().in_use);
261     try std.testing.expectError(error.TruncatedImage, imageFromBytes(&storage, &corrupted));
262     try std.testing.expect(!storage.status().in_use);
263 }
264 
265 test "decoding allocation baseline" {
266     const width = 320;
267     const height = 180;
268     const source = try std.testing.allocator.alloc(u8, width * height * 4);
269     defer std.testing.allocator.free(source);
270     for (0..width * height) |pixel| {
271         source[pixel * 4] = @intCast((pixel % 16) * 16);
272         source[pixel * 4 + 1] = @intCast((15 - (pixel % 16)) * 16);
273         source[pixel * 4 + 2] = 128;
274         source[pixel * 4 + 3] = 255;
275     }
276     inline for (.{
277         .{ encode.Compression.compressed, @as(usize, 1_087) },
278         .{ encode.Compression.stored, @as(usize, 173_058) },
279     }) |expected| {
280         var encoded = try encode.Encoding.init(std.testing.allocator, .{
281             .rgba8 = source,
282             .width = width,
283             .height = height,
284             .options = .{ .compression = expected[0] },
285         });
286         defer encoded.deinit(std.testing.allocator);
287         try std.testing.expectEqual(expected[1], encoded.bytes.len);
288         var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
289         var decoded = try Decoding.init(counting.allocator(), encoded.bytes);
290         try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
291         try std.testing.expectEqual(@as(usize, 0), counting.resize_index);
292         try std.testing.expectEqual(decoded.status().storage_bytes, counting.allocated_bytes);
293         try std.testing.expectEqual(@as(usize, 297_856), decoded.status().storage_bytes);
294         try std.testing.expectEqual(
295             @as(u64, 10_254_699_363_429_783_698),
296             std.hash.Wyhash.hash(0, decoded.image.rgba8),
297         );
298         decoded.deinit(counting.allocator());
299         try std.testing.expectEqual(counting.allocated_bytes, counting.freed_bytes);
300     }
301 }
302 
303 fn testPngAlloc(
304     allocator: std.mem.Allocator,
305     width: u32,
306     height: u32,
307     bit_depth: u8,
308     color_type: u8,
309     samples: usize,
310     source: []const u8,
311     palette: []const u8,
312     transparency: []const u8,
313 ) ![]u8 {
314     var output = std.Io.Writer.Allocating.init(allocator);
315     defer output.deinit();
316     try output.writer.writeAll(&encode.signature);
317     var header: [13]u8 = @splat(0);
318     std.mem.writeInt(u32, header[0..4], width, .big);
319     std.mem.writeInt(u32, header[4..8], height, .big);
320     header[8] = bit_depth;
321     header[9] = color_type;
322     try testWriteChunk(&output.writer, "IHDR", &header);
323     if (palette.len > 0) try testWriteChunk(&output.writer, "PLTE", palette);
324     if (transparency.len > 0) try testWriteChunk(&output.writer, "tRNS", transparency);
325     const row_bytes = (@as(usize, width) * samples * bit_depth + 7) / 8;
326     const raw = try allocator.alloc(u8, (row_bytes + 1) * @as(usize, height));
327     defer allocator.free(raw);
328     for (0..height) |y| {
329         const offset = y * (row_bytes + 1);
330         raw[offset] = 0;
331         @memcpy(raw[offset + 1 ..][0..row_bytes], source[y * row_bytes ..][0..row_bytes]);
332     }
333     var compressed = try std.Io.Writer.Allocating.initCapacity(allocator, 64);
334     defer compressed.deinit();
335     const window = try allocator.alloc(u8, flate.max_window_len);
336     defer allocator.free(window);
337     var compressor = try flate.Compress.init(
338         &compressed.writer,
339         window,
340         .zlib,
341         flate.Compress.Options.level_1,
342     );
343     try compressor.writer.writeAll(raw);
344     try compressor.finish();
345     try testWriteChunk(&output.writer, "IDAT", compressed.written());
346     try testWriteChunk(&output.writer, "IEND", &.{});
347     return try output.toOwnedSlice();
348 }
349 
350 fn splitIdatAlloc(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
351     const data_offset = try testChunkDataOffset(bytes, "IDAT");
352     const chunk_offset = data_offset - 8;
353     const length: usize = @intCast(std.mem.readInt(u32, bytes[chunk_offset..][0..4], .big));
354     const split = @max(@as(usize, 1), length / 2);
355     var output = std.Io.Writer.Allocating.init(allocator);
356     defer output.deinit();
357     try output.writer.writeAll(bytes[0..chunk_offset]);
358     try testWriteChunk(&output.writer, "IDAT", bytes[data_offset..][0..split]);
359     try testWriteChunk(&output.writer, "IDAT", bytes[data_offset + split ..][0 .. length - split]);
360     try output.writer.writeAll(bytes[data_offset + length + 4 ..]);
361     return try output.toOwnedSlice();
362 }
363 
364 fn testChunkDataOffset(bytes: []const u8, wanted: *const [4]u8) !usize {
365     var offset: usize = encode.signature.len;
366     while (bytes.len - offset >= 8) {
367         const length: usize = @intCast(std.mem.readInt(u32, bytes[offset..][0..4], .big));
368         const data_offset = offset + 8;
369         if (std.mem.eql(u8, bytes[offset + 4 ..][0..4], wanted)) return data_offset;
370         offset = data_offset + length + 4;
371     }
372     return error.InvalidChunk;
373 }
374 
375 fn testWriteChunk(writer: *std.Io.Writer, kind: *const [4]u8, data: []const u8) !void {
376     try writer.writeInt(u32, @intCast(data.len), .big);
377     try writer.writeAll(kind);
378     try writer.writeAll(data);
379     var crc = std.hash.Crc32.init();
380     crc.update(kind);
381     crc.update(data);
382     try writer.writeInt(u32, crc.final(), .big);
383 }