also looking at this
feat(media): implement native JPEG coding and bounded MJPEG streaming
#912
5 files
+442
-263
| @@ -0,0 +1,237 @@ | ||
| 1 | //! Home native baseline JPEG encoder. MIT; see NOTICE. | |
| 2 | const std = ; | |
| 3 | const 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 | }; | |
| 13 | const 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 | }; | |
| 23 | const 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 | }; | |
| 33 | const 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 | }; | |
| 46 | const DC_COUNTS = [16]u8{ 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0 }; | |
| 47 | const AC_COUNTS = [16]u8{ 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 125 }; | |
| 48 | const DC_VALUES = [12]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; | |
| 49 | const Huffman = struct { codes: [256]u16 = @splat(0), lengths: [256]u5 = @splat(0) }; | |
| 50 | fn 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 | } | |
| 65 | const DC = table(DC_COUNTS, &DC_VALUES); | |
| 66 | const AC = table(AC_COUNTS, &AC_VALUES); | |
| 67 | const BASIS = .BASIS; | |
| 68 | pub 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 | }; | |
| 227 | test "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 | } | |