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 }