lib/png/src/encode/plan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const flate = std.compress.flate;
3 const model = @import("model.zig");
4
5 pub const stream_bytes: usize = 4096;
6 pub const signature_bytes: usize = 8;
7 pub const framed_bytes: usize = 49;
8 pub const stored_zlib_header = [_]u8{ 0x78, 0x01 };
9
10 pub const Bounds = struct {
11 image_pixels: usize,
12
13 pub fn exact(image: model.ImageView) model.EncodeError!Bounds {
14 return .{ .image_pixels = try model.pixelCount(image.width, image.height) };
15 }
16 };
17
18 pub const ScratchRegions = struct {
19 window: []u8,
20 raw: []u8,
21 count: []u8,
22 };
23
24 pub const Scratch = struct {
25 window: [flate.max_window_len]u8 = undefined,
26 raw: [stream_bytes]u8 = undefined,
27 count: [stream_bytes]u8 = undefined,
28
29 pub const bytes: usize = flate.max_window_len + stream_bytes * 2;
30
31 pub fn regions(self: *Scratch) ScratchRegions {
32 return .{ .window = &self.window, .raw = &self.raw, .count = &self.count };
33 }
34 };
35
36 pub const Error = model.EncodeError || model.Exhaustion || std.Io.Writer.Error;
37
38 pub const Plan = struct {
39 input_hash: u64,
40 width: i32,
41 height: i32,
42 image_pixels: usize,
43 color: model.Color,
44 raw_bytes: usize,
45 zlib_bytes: usize,
46 output_bytes: usize,
47 options: model.Options,
48 bounds: Bounds,
49
50 pub fn inspect(image: model.ImageView, bounds: Bounds, scratch: *Scratch) Error!Plan {
51 return inspectRegions(image, bounds, scratch.regions());
52 }
53
54 pub fn inspectRegions(
55 image: model.ImageView,
56 bounds: Bounds,
57 scratch: ScratchRegions,
58 ) Error!Plan {
59 const image_pixels = try model.pixelCount(image.width, image.height);
60 if (image_pixels > bounds.image_pixels) return error.ImagePixelCapacityExceeded;
61 const pixel_bytes = try model.pixelByteCount(image.width, image.height);
62 if (image.rgba8.len != pixel_bytes) return error.PixelSizeMismatch;
63 const color = model.colorForPixels(image.rgba8);
64 const row_bytes = try multiplied(@as(usize, @intCast(image.width)), color.bytesPerPixel());
65 const raw_bytes = try multiplied(
66 try added(row_bytes, 1),
67 @as(usize, @intCast(image.height)),
68 );
69 const zlib_bytes = switch (image.options.compression) {
70 .compressed => try measureCompressed(image, color, scratch),
71 .stored => try storedBytes(raw_bytes),
72 };
73 return .{
74 .input_hash = model.imageHash(image),
75 .width = image.width,
76 .height = image.height,
77 .image_pixels = image_pixels,
78 .color = color,
79 .raw_bytes = raw_bytes,
80 .zlib_bytes = zlib_bytes,
81 .output_bytes = try added(signature_bytes + framed_bytes, zlib_bytes),
82 .options = image.options,
83 .bounds = bounds,
84 };
85 }
86
87 pub fn exactBounds(self: Plan) Bounds {
88 return .{ .image_pixels = self.image_pixels };
89 }
90 };
91
92 pub fn streamRaw(
93 writer: *std.Io.Writer,
94 image: model.ImageView,
95 color: model.Color,
96 raw: []u8,
97 ) std.Io.Writer.Error!void {
98 std.debug.assert(raw.len >= stream_bytes);
99 var source = model.RawSource.init(image, color);
100 while (source.remaining() > 0) {
101 const count = source.read(raw[0..stream_bytes]);
102 std.debug.assert(count > 0);
103 try writer.writeAll(raw[0..count]);
104 }
105 }
106
107 fn measureCompressed(
108 image: model.ImageView,
109 color: model.Color,
110 scratch: ScratchRegions,
111 ) Error!usize {
112 std.debug.assert(scratch.window.len >= flate.max_window_len);
113 std.debug.assert(scratch.raw.len >= stream_bytes);
114 std.debug.assert(scratch.count.len >= stream_bytes);
115 var output: std.Io.Writer.Discarding = .init(scratch.count);
116 var compressor = try flate.Compress.init(
117 &output.writer,
118 scratch.window,
119 .zlib,
120 flate.Compress.Options.level_1,
121 );
122 try streamRaw(&compressor.writer, image, color, scratch.raw);
123 try compressor.finish();
124 return std.math.cast(usize, output.fullCount()) orelse error.CapacityOverflow;
125 }
126
127 fn storedBytes(raw_bytes: usize) Error!usize {
128 const blocks = try dividedUp(raw_bytes, std.math.maxInt(u16));
129 const overhead = try added(stored_zlib_header.len + 4, try multiplied(blocks, 5));
130 return try added(raw_bytes, overhead);
131 }
132
133 fn added(left: usize, right: usize) Error!usize {
134 return std.math.add(usize, left, right) catch error.CapacityOverflow;
135 }
136
137 fn multiplied(left: usize, right: usize) Error!usize {
138 return std.math.mul(usize, left, right) catch error.CapacityOverflow;
139 }
140
141 fn dividedUp(numerator: usize, denominator: usize) Error!usize {
142 return try added(numerator, denominator - 1) / denominator;
143 }
144
145 test "PNG planning measures exact compressed and stored output" {
146 const pixels = [_]u8{ 1, 2, 3, 255, 4, 5, 6, 255 };
147 var scratch: Scratch = undefined;
148 const compressed = try Plan.inspect(
149 .{ .rgba8 = &pixels, .width = 2, .height = 1 },
150 .{ .image_pixels = 2 },
151 &scratch,
152 );
153 const stored = try Plan.inspect(
154 .{
155 .rgba8 = &pixels,
156 .width = 2,
157 .height = 1,
158 .options = .{ .compression = .stored },
159 },
160 .{ .image_pixels = 2 },
161 &scratch,
162 );
163 try std.testing.expect(compressed.zlib_bytes < stored.zlib_bytes);
164 try std.testing.expectEqual(@as(usize, 7), stored.raw_bytes);
165 try std.testing.expectEqual(@as(usize, 18), stored.zlib_bytes);
166 try std.testing.expectEqual(stored.zlib_bytes + 57, stored.output_bytes);
167 }