ReviewOS

also looking at this

home-lang/home

feat(media): implement native JPEG coding and bounded MJPEG streaming

#912
Merged chris wants to merge feat/native-jpeg-mjpeg into main
5 files +442 -263
packages/image/src/jpeg_encoder.zigadded+237-0
Changes to packages/image/src/jpeg_encoder.zig
@@ -0,0 +1,237 @@
1//! Home native baseline JPEG encoder. MIT; see NOTICE.
2const std = ;
3const ZIGZAG = [64]u8{
4 0, 1, 8, 16, 9, 2, 3, 10,
5 17, 24, 32, 25, 18, 11, 4, 5,
6 12, 19, 26, 33, 40, 48, 41, 34,
7 27, 20, 13, 6, 7, 14, 21, 28,
8 35, 42, 49, 56, 57, 50, 43, 36,
9 29, 22, 15, 23, 30, 37, 44, 51,
10 58, 59, 52, 45, 38, 31, 39, 46,
11 53, 60, 61, 54, 47, 55, 62, 63,
12};
13const DEFAULT_LUMINANCE_QUANT = [64]u8{
14 16, 11, 10, 16, 24, 40, 51, 61,
15 12, 12, 14, 19, 26, 58, 60, 55,
16 14, 13, 16, 24, 40, 57, 69, 56,
17 14, 17, 22, 29, 51, 87, 80, 62,
18 18, 22, 37, 56, 68, 109, 103, 77,
19 24, 35, 55, 64, 81, 104, 113, 92,
20 49, 64, 78, 87, 103, 121, 120, 101,
21 72, 92, 95, 98, 112, 100, 103, 99,
22};
23const DEFAULT_CHROMINANCE_QUANT = [64]u8{
24 17, 18, 24, 47, 99, 99, 99, 99,
25 18, 21, 26, 66, 99, 99, 99, 99,
26 24, 26, 56, 99, 99, 99, 99, 99,
27 47, 66, 99, 99, 99, 99, 99, 99,
28 99, 99, 99, 99, 99, 99, 99, 99,
29 99, 99, 99, 99, 99, 99, 99, 99,
30 99, 99, 99, 99, 99, 99, 99, 99,
31 99, 99, 99, 99, 99, 99, 99, 99,
32};
33const AC_VALUES = [_]u8{
34 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
35 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0,
36 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
37 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
38 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
39 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
40 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
41 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5,
42 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
43 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
44 0xF9, 0xFA,
45};
46const DC_COUNTS = [16]u8{ 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 };
47const AC_COUNTS = [16]u8{ 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 125 };
48const DC_VALUES = [12]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
49const Huffman = struct { codes: [256]u16 = @splat(0), lengths: [256]u5 = @splat(0) };
50fn table(counts: [16]u8, values: []const u8) Huffman {
51 var result: Huffman = .{};
52 var code: u16 = 0;
53 var n: usize = 0;
54 for (counts, 0..) |num, i| {
55 for (0..num) |_| {
56 result.codes[values[n]] = code;
57 result.lengths[values[n]] = @intCast(i + 1);
58 code += 1;
59 n += 1;
60 }
61 if (i < 15) code <<= 1;
62 }
63 return result;
64}
65const DC = table(DC_COUNTS, &DC_VALUES);
66const AC = table(AC_COUNTS, &AC_VALUES);
67const BASIS = .BASIS;
68pub const Encoder = struct {
69 allocator: std.mem.Allocator,
70 output: std.ArrayList(u8) = .empty,
71 bits: u32 = 0,
72 bit_count: u6 = 0,
73 pub fn init(allocator: std.mem.Allocator) Encoder {
74 return .{ .allocator = allocator };
75 }
76 pub fn deinit(self: *Encoder) void {
77 self.output.deinit(self.allocator);
78 }
79 fn byte(self: *Encoder, v: u8) !void {
80 try self.output.append(self.allocator, v);
81 }
82 fn bytes(self: *Encoder, v: []const u8) !void {
83 try self.output.appendSlice(self.allocator, v);
84 }
85 fn word(self: *Encoder, v: u16) !void {
86 var b: [2]u8 = undefined;
87 std.mem.writeInt(u16, &b, v, .big);
88 try self.bytes(&b);
89 }
90 fn put(self: *Encoder, code: u16, len: u5) !void {
91 self.bits = (self.bits << len) | code;
92 self.bit_count += len;
93 while (self.bit_count >= 8) {
94 self.bit_count -= 8;
95 const b: u8 = @truncate(self.bits >> @intCast(self.bit_count));
96 try self.byte(b);
97 if (b == 255) try self.byte(0);
98 }
99 }
100 fn symbol(self: *Encoder, t: *const Huffman, v: u8) !void {
101 if (t.lengths[v] == 0) return error.InvalidCoefficient;
102 try self.put(t.codes[v], t.lengths[v]);
103 }
104 fn amplitude(self: *Encoder, v: i32) !u5 {
105 _ = self;
106 const magnitude: u32 = @intCast(@abs(v));
107 return @intCast(if (magnitude == 0) 0 else 32 - @clz(magnitude));
108 }
109 fn value(self: *Encoder, v: i32, n: u5) !void {
110 if (n == 0) return;
111 const code: u16 = @intCast(if (v < 0) v + @as(i32, @intCast((@as(u32, 1) << n) - 1)) else v);
112 try self.put(code, n);
113 }
114 fn block(self: *Encoder, input: [64]f32, quant: [64]u8, last_dc: *i32) !void {
115 var intermediate: [8]@Vector(8, f32) = undefined;
116 for (0..8) |y| {
117 const line: @Vector(8, f32) = input[y * 8 ..][0..8].*;
118 inline for (0..8) |u| intermediate[y][u] = @reduce(.Add, line * BASIS[u]);
119 }
120 var coefficients: [64]i32 = undefined;
121 inline for (0..8) |u| {
122 var col: @Vector(8, f32) = undefined;
123 inline for (0..8) |y| col[y] = intermediate[y][u];
124 for (0..8) |v| coefficients[v * 8 + u] = @intFromFloat(@round(@reduce(.Add, col * BASIS[v]) / @as(f32, @floatFromInt(quant[v * 8 + u]))));
125 }
126 const delta = coefficients[0] - last_dc.*;
127 last_dc.* = coefficients[0];
128 const dc_n = try self.amplitude(delta);
129 try self.symbol(&DC, dc_n);
130 try self.value(delta, dc_n);
131 var run: u8 = 0;
132 for (ZIGZAG[1..]) |index| {
133 const v = coefficients[index];
134 if (v == 0) {
135 run += 1;
136 continue;
137 }
138 while (run >= 16) {
139 try self.symbol(&AC, 0xf0);
140 run -= 16;
141 }
142 const n = try self.amplitude(v);
143 if (n > 10) return error.InvalidCoefficient;
144 try self.symbol(&AC, (run << 4) | @as(u8, n));
145 try self.value(v, n);
146 run = 0;
147 }
148 if (run != 0) try self.symbol(&AC, 0);
149 }
150 /// Borrowed output is valid until the next encode/deinit. RGB/RGBA/gray input
151 /// can contain row padding. 4:4:4, baseline Huffman coding, edge replication.
152 pub fn encode(self: *Encoder, data: []const u8, width: usize, height: usize, stride: usize, channels: u8, quality: u8) ![]const u8 {
153 if (width == 0 or height == 0 or width > 65535 or height > 65535) return error.InvalidDimensions;
154 if (channels != 1 and channels != 3 and channels != 4) return error.UnsupportedFormat;
155 if (quality < 1 or quality > 100) return error.InvalidQuality;
156 const row = std.math.mul(usize, width, channels) catch return error.InvalidDimensions;
157 const needed = std.math.add(usize, std.math.mul(usize, height - 1, stride) catch return error.InvalidDimensions, row) catch return error.InvalidDimensions;
158 if (stride < row or data.len < needed) return error.BufferTooSmall;
159 self.output.clearRetainingCapacity();
160 self.bits = 0;
161 self.bit_count = 0;
162 const components: u8 = if (channels == 1) 1 else 3;
163 var quant: [2][64]u8 = undefined;
164 const scale: u32 = if (quality < 50) 5000 / @as(u32, quality) else 200 - @as(u32, quality) * 2;
165 for ([2][64]u8{ DEFAULT_LUMINANCE_QUANT, DEFAULT_CHROMINANCE_QUANT }, 0..) |q, t| for (q, 0..) |v, i| {
166 quant[t][i] = @intCast(std.math.clamp((@as(u32, v) * scale + 50) / 100, 1, 255));
167 };
168 try self.bytes(&.{ 255, 216, 255, 224, 0, 16, 'J', 'F', 'I', 'F', 0, 1, 1, 0, 0, 1, 0, 1, 0, 0 });
169 for (0..2) |t| {
170 try self.bytes(&.{ 255, 219, 0, 67 });
171 try self.byte(@intCast(t));
172 for (ZIGZAG) |i| try self.byte(quant[t][i]);
173 }
174 try self.bytes(&.{ 255, 192 });
175 try self.word(8 + @as(u16, components) * 3);
176 try self.byte(8);
177 try self.word(@intCast(height));
178 try self.word(@intCast(width));
179 try self.byte(components);
180 for (0..components) |i| {
181 try self.byte(@intCast(i + 1));
182 try self.byte(0x11);
183 try self.byte(if (i == 0) 0 else 1);
184 }
185 try self.bytes(&.{ 255, 196, 0, 31, 0 });
186 try self.bytes(&DC_COUNTS);
187 try self.bytes(&DC_VALUES);
188 try self.bytes(&.{ 255, 196, 0, 181, 0x10 });
189 try self.bytes(&AC_COUNTS);
190 try self.bytes(&AC_VALUES);
191 try self.bytes(&.{ 255, 218 });
192 try self.word(6 + @as(u16, components) * 2);
193 try self.byte(components);
194 for (0..components) |i| {
195 try self.byte(@intCast(i + 1));
196 try self.byte(0);
197 }
198 try self.bytes(&.{ 0, 63, 0 });
199 var last_dc: [3]i32 = @splat(0);
200 var by: usize = 0;
201 while (by < height) : (by += 8) {
202 var bx: usize = 0;
203 while (bx < width) : (bx += 8) {
204 var blocks: [3][64]f32 = undefined;
205 for (0..8) |y| for (0..8) |x| {
206 const off = @min(by + y, height - 1) * stride + @min(bx + x, width - 1) * channels;
207 if (channels == 1) blocks[0][y * 8 + x] = @as(f32, @floatFromInt(data[off])) - 128 else {
208 const red: i32 = data[off];
209 const green: i32 = data[off + 1];
210 const blue: i32 = data[off + 2];
211 blocks[0][y * 8 + x] = @floatFromInt(((77 * red + 150 * green + 29 * blue) >> 8) - 128);
212 blocks[1][y * 8 + x] = @floatFromInt((-43 * red - 85 * green + 128 * blue) >> 8);
213 blocks[2][y * 8 + x] = @floatFromInt((128 * red - 107 * green - 21 * blue) >> 8);
214 }
215 };
216 for (0..components) |i| try self.block(blocks[i], quant[if (i == 0) 0 else 1], &last_dc[i]);
217 }
218 }
219 if (self.bit_count > 0) {
220 const pad: u5 = @intCast(8 - self.bit_count);
221 try self.put(@intCast((@as(u16, 1) << @intCast(pad)) - 1), pad);
222 }
223 try self.bytes(&.{ 255, 217 });
224 return self.output.items;
225 }
226};
227test "baseline JPEG encodes real scan data and checks input" {
228 var enc = Encoder.init(std.testing.allocator);
229 defer enc.deinit();
230 var pixels: [17 * 9 * 3]u8 = undefined;
231 for (&pixels, 0..) |*v, i| v.* = @truncate(i * 17);
232 const bytes = try enc.encode(&pixels, 17, 9, 17 * 3, 3, 90);
233 try std.testing.expect(bytes.len > 400);
234 try std.testing.expectEqualSlices(u8, &.{ 255, 217 }, bytes[bytes.len - 2 ..]);
235 try std.testing.expectError(error.InvalidQuality, enc.encode(&pixels, 17, 9, 51, 3, 0));
236 try std.testing.expectError(error.BufferTooSmall, enc.encode(&pixels, 17, 9, 52, 3, 90));
237}