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 }