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/formats/jpeg.zigmodified+18-263
Changes to packages/image/src/formats/jpeg.zig
@@ -198,7 +198,7 @@ const JpegDecoder = struct {
198198 b2 = try self.readByte();
199199 }
200200
201 return @enumFromInt((@as(u16, 0xFF) << 8) | b2);
201 return @fromBackingInt(@intCast((@as(u16, 0xFF) << 8) | b2));
202202 }
203203
204204 fn skipMarker(self: *JpegDecoder) !void {
@@ -455,125 +455,7 @@ const JpegDecoder = struct {
455455// ============================================================================
456456
457457fn idct(block: *[64]i32, quant: *const [64]u16) void {
458 // Dequantize
459 for (0..64) |i| {
460 block[i] *= @intCast(quant[i]);
461 }
462
463 // Fast IDCT using scaled integers
464 // Based on AAN algorithm
465 var workspace: [64]i32 = undefined;
466
467 // Process columns
468 for (0..8) |col| {
469 idctColumn(block, &workspace, col);
470 }
471
472 // Process rows
473 for (0..8) |row| {
474 idctRow(&workspace, block, row);
475 }
476}
477
478fn idctColumn(input: *const [64]i32, output: *[64]i32, col: usize) void {
479 const s0 = input[col + 0 * 8];
480 const s1 = input[col + 1 * 8];
481 const s2 = input[col + 2 * 8];
482 const s3 = input[col + 3 * 8];
483 const s4 = input[col + 4 * 8];
484 const s5 = input[col + 5 * 8];
485 const s6 = input[col + 6 * 8];
486 const s7 = input[col + 7 * 8];
487
488 // Stage 1
489 const p2 = s2;
490 const p3 = s6;
491 const p1 = @divTrunc((p2 + p3) * 2896, 4096); // cos(4*pi/16) * 4096
492 const t2 = @divTrunc(p3 * -7017, 4096) + p1; // -cos(4*pi/16) - cos(2*pi/16)
493 const t3 = @divTrunc(p2 * 3406, 4096) + p1; // cos(4*pi/16) - cos(6*pi/16)
494
495 const t0 = (s0 + s4) << 12;
496 const t1 = (s0 - s4) << 12;
497
498 const t10 = t0 + t3;
499 const t13 = t0 - t3;
500 const t11 = t1 + t2;
501 const t12 = t1 - t2;
502
503 // Stage 2
504 const z1 = s7 + s1;
505 const z2 = s5 + s3;
506 const z3 = s7 + s3;
507 const z4 = s5 + s1;
508 const z5 = @divTrunc((z3 + z4) * 4816, 4096); // cos(2*pi/16) + cos(6*pi/16)
509
510 const t4 = @divTrunc(s7 * 799, 4096);
511 const t5 = @divTrunc(s5 * 2276, 4096);
512 const t6 = @divTrunc(s3 * 3406, 4096);
513 const t7 = @divTrunc(s1 * 4017, 4096);
514
515 const z1_scaled = @divTrunc(z1 * -1390, 4096);
516 const z2_scaled = @divTrunc(z2 * -3406, 4096);
517 const z3_scaled = @divTrunc(z3 * -7017, 4096);
518 const z4_scaled = @divTrunc(z4 * 799, 4096);
519
520 const t4_out = t4 + z1_scaled + z3_scaled;
521 const t5_out = t5 + z2_scaled + z4_scaled;
522 const t6_out = t6 + z2_scaled + z3_scaled;
523 const t7_out = t7 + z1_scaled + z4_scaled + z5;
524
525 output[col + 0 * 8] = (t10 + t7_out) >> 12;
526 output[col + 7 * 8] = (t10 - t7_out) >> 12;
527 output[col + 1 * 8] = (t11 + t6_out) >> 12;
528 output[col + 6 * 8] = (t11 - t6_out) >> 12;
529 output[col + 2 * 8] = (t12 + t5_out) >> 12;
530 output[col + 5 * 8] = (t12 - t5_out) >> 12;
531 output[col + 3 * 8] = (t13 + t4_out) >> 12;
532 output[col + 4 * 8] = (t13 - t4_out) >> 12;
533}
534
535fn idctRow(input: *const [64]i32, output: *[64]i32, row: usize) void {
536 const base = row * 8;
537 const s0 = input[base + 0];
538 const s1 = input[base + 1];
539 const s2 = input[base + 2];
540 const s3 = input[base + 3];
541 const s4 = input[base + 4];
542 const s5 = input[base + 5];
543 const s6 = input[base + 6];
544 const s7 = input[base + 7];
545
546 // Simplified 1D IDCT
547 const t0 = (s0 + s4) << 7;
548 const t1 = (s0 - s4) << 7;
549
550 const t2 = @divTrunc(s2 * 2896, 4096) - @divTrunc(s6 * 1203, 4096);
551 const t3 = @divTrunc(s2 * 1203, 4096) + @divTrunc(s6 * 2896, 4096);
552
553 const t10 = t0 + t3;
554 const t13 = t0 - t3;
555 const t11 = t1 + t2;
556 const t12 = t1 - t2;
557
558 const z1 = s1 + s7;
559 const z2 = s3 + s5;
560 const z3 = s1 + s5;
561 const z4 = s3 + s7;
562
563 const t4 = @divTrunc(s7 * 799, 4096) - @divTrunc(z1 * 1390, 4096) - @divTrunc(z3 * 3406, 4096);
564 const t5 = @divTrunc(s5 * 2276, 4096) - @divTrunc(z2 * 3406, 4096) + @divTrunc(z4 * 799, 4096);
565 const t6 = @divTrunc(s3 * 3406, 4096) - @divTrunc(z2 * 3406, 4096) - @divTrunc(z3 * 3406, 4096);
566 const t7 = @divTrunc(s1 * 4017, 4096) - @divTrunc(z1 * 1390, 4096) + @divTrunc(z4 * 799, 4096);
567
568 // Final output with level shift and clamping
569 output[base + 0] = clamp(@divTrunc(t10 + t7, 256) + 128);
570 output[base + 7] = clamp(@divTrunc(t10 - t7, 256) + 128);
571 output[base + 1] = clamp(@divTrunc(t11 + t6, 256) + 128);
572 output[base + 6] = clamp(@divTrunc(t11 - t6, 256) + 128);
573 output[base + 2] = clamp(@divTrunc(t12 + t5, 256) + 128);
574 output[base + 5] = clamp(@divTrunc(t12 - t5, 256) + 128);
575 output[base + 3] = clamp(@divTrunc(t13 + t4, 256) + 128);
576 output[base + 4] = clamp(@divTrunc(t13 - t4, 256) + 128);
458 .inverse(block, quant);
577459}
578460
579461fn clamp(value: i32) i32 {
@@ -638,7 +520,7 @@ pub fn decode(allocator: std.mem.Allocator, data: []const u8) !Image {
638520 .EOI => break,
639521 else => {
640522 // Skip unknown markers
641 if (@intFromEnum(marker) >= 0xFFC0) {
523 if (@backingInt(marker) >= 0xFFC0) {
642524 try decoder.skipMarker();
643525 }
644526 },
@@ -720,149 +602,22 @@ pub fn decode(allocator: std.mem.Allocator, data: []const u8) !Image {
720602// JPEG Encoder (basic quality encoding)
721603// ============================================================================
722604
605/// Encode baseline JPEG with a real DCT/quantization/Huffman scan. The standalone
606/// encoder also accepts borrowed, padded rows and can reuse its output capacity.
723607pub fn encode(allocator: std.mem.Allocator, img: *const Image) ![]u8 {
724 if (img.width == 0 or img.height == 0 or img.width > std.math.maxInt(u16) or img.height > std.math.maxInt(u16)) {
725 return error.InvalidDimensions;
726 }
727 switch (img.format) {
728 .grayscale8, .rgb8, .rgba8 => {},
729 else => return error.UnsupportedFormat,
730 }
731
732 var output: std.ArrayList(u8) = .empty;
733 errdefer output.deinit(allocator);
734
735 // SOI marker
736 try output.appendSlice(allocator, &[_]u8{ 0xFF, 0xD8 });
737
738 // APP0 (JFIF) marker
739 try output.appendSlice(allocator, &[_]u8{
740 0xFF, 0xE0, // APP0
741 0x00, 0x10, // Length = 16
742 'J', 'F', 'I', 'F', 0x00, // Identifier
743 0x01, 0x01, // Version 1.1
744 0x00, // Aspect ratio units (0 = no units)
745 0x00, 0x01, // X density
746 0x00, 0x01, // Y density
747 0x00, // Thumbnail width
748 0x00, // Thumbnail height
749 });
750
751 // DQT marker (quantization tables)
752 try output.appendSlice(allocator, &[_]u8{ 0xFF, 0xDB, 0x00, 0x43, 0x00 }); // Length, table 0
753 try output.appendSlice(allocator, &DEFAULT_LUMINANCE_QUANT);
754
755 // Second quantization table for chrominance
756 try output.appendSlice(allocator, &[_]u8{ 0xFF, 0xDB, 0x00, 0x43, 0x01 });
757 try output.appendSlice(allocator, &DEFAULT_CHROMINANCE_QUANT);
758
759 // SOF0 (baseline DCT)
760 const is_grayscale = img.format == .grayscale8;
761 const num_components: u8 = if (is_grayscale) 1 else 3;
762
763 try output.appendSlice(allocator, &[_]u8{
764 0xFF,
765 0xC0, // SOF0
766 0x00,
767 @intCast(8 + num_components * 3), // Length
768 0x08, // Precision (8 bits)
769 });
770 try output.appendSlice(allocator, &std.mem.toBytes(std.mem.nativeToBig(u16, @intCast(img.height))));
771 try output.appendSlice(allocator, &std.mem.toBytes(std.mem.nativeToBig(u16, @intCast(img.width))));
772 try output.append(allocator, num_components);
773
774 if (is_grayscale) {
775 try output.appendSlice(allocator, &[_]u8{ 1, 0x11, 0 }); // Y component
776 } else {
777 try output.appendSlice(allocator, &[_]u8{ 1, 0x11, 0 }); // Y component
778 try output.appendSlice(allocator, &[_]u8{ 2, 0x11, 1 }); // Cb component
779 try output.appendSlice(allocator, &[_]u8{ 3, 0x11, 1 }); // Cr component
780 }
781
782 // DHT (Huffman tables) - using standard JPEG tables
783 try writeStandardHuffmanTables(allocator, &output);
784
785 // SOS (start of scan)
786 try output.appendSlice(allocator, &[_]u8{
787 0xFF,
788 0xDA, // SOS
789 0x00,
790 @intCast(6 + num_components * 2), // Length
791 num_components,
792 });
793
794 if (is_grayscale) {
795 try output.appendSlice(allocator, &[_]u8{ 1, 0x00 }); // Component 1, DC/AC table 0
796 } else {
797 try output.appendSlice(allocator, &[_]u8{ 1, 0x00 }); // Y: tables 0/0
798 try output.appendSlice(allocator, &[_]u8{ 2, 0x11 }); // Cb: tables 1/1
799 try output.appendSlice(allocator, &[_]u8{ 3, 0x11 }); // Cr: tables 1/1
800 }
801
802 try output.appendSlice(allocator, &[_]u8{ 0x00, 0x3F, 0x00 }); // Spectral selection, successive approx
803
804 // Encode scan data (simplified - just output raw MCUs with minimal compression)
805 // This is a placeholder - full encoding would require DCT, quantization, Huffman encoding
806
807 // For now, output a minimal valid bitstream
808 // A proper implementation would encode each 8x8 block with DCT
809
810 // EOI marker
811 try output.appendSlice(allocator, &[_]u8{ 0xFF, 0xD9 });
812
813 return output.toOwnedSlice(allocator);
608 return encodeWithQuality(allocator, img, 90);
814609}
815
816fn writeStandardHuffmanTables(allocator: std.mem.Allocator, output: *std.ArrayList(u8)) !void {
817 // DC luminance table
818 try output.appendSlice(allocator, &[_]u8{
819 0xFF, 0xC4, // DHT marker
820 0x00, 0x1F, // Length
821 0x00, // DC table 0
822 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Bits
823 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, // Values
824 });
825
826 // AC luminance table
827 try output.appendSlice(allocator, &[_]u8{
828 0xFF, 0xC4, // DHT marker
829 0x00, 0xB5, // Length
830 0x10, // AC table 0
831 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7D, // Bits
832 });
833 // AC values (162 bytes)
834 const ac_values = [_]u8{
835 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
836 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0,
837 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28,
838 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
839 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
840 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
841 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7,
842 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5,
843 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2,
844 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
845 0xF9, 0xFA,
610pub fn encodeWithQuality(allocator: std.mem.Allocator, img: *const Image, quality: u8) ![]u8 {
611 const channels: u8 = switch (img.format) {
612 .grayscale8 => 1,
613 .rgb8 => 3,
614 .rgba8 => 4,
615 else => return error.UnsupportedFormat,
846616 };
847 try output.appendSlice(allocator, &ac_values);
848
849 // DC chrominance table
850 try output.appendSlice(allocator, &[_]u8{
851 0xFF, 0xC4, // DHT marker
852 0x00, 0x1F, // Length
853 0x01, // DC table 1
854 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // Bits
855 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, // Values
856 });
857
858 // AC chrominance table
859 try output.appendSlice(allocator, &[_]u8{
860 0xFF, 0xC4, // DHT marker
861 0x00, 0xB5, // Length
862 0x11, // AC table 1
863 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, // Bits
864 });
865 try output.appendSlice(allocator, &ac_values);
617 var encoder = .Encoder.init(allocator);
618 defer encoder.deinit();
619 _ = try encoder.encode(img.pixels, img.width, img.height, @as(usize, img.width) * channels, channels, quality);
620 return encoder.output.toOwnedSlice(allocator);
866621}
867622
868623// ============================================================================
renamed throughout · 10 hidden lines
Changes to packages/image/src/formats/jpeg.zig
@@ -870,8 +625,8 @@ fn writeStandardHuffmanTables(allocator: std.mem.Allocator, output: *std.ArrayLi renamed throughout
870625// ============================================================================
871626
872627test "JPEG marker constants" {
873 try std.testing.expectEqual(@as(u16, 0xFFD8), @intFromEnum(Marker.SOI));
874 try std.testing.expectEqual(@as(u16, 0xFFD9), @intFromEnum(Marker.EOI));
628 try std.testing.expectEqual(@as(u16, 0xFFD8), @backingInt(Marker.SOI));
629 try std.testing.expectEqual(@as(u16, 0xFFD9), @backingInt(Marker.EOI));
875630}
876631
877632test "YCbCr to RGB conversion" {
packages/image/src/jpeg_dct.zigadded+39-0
Changes to packages/image/src/jpeg_dct.zig
@@ -0,0 +1,39 @@
1//! Shared orthonormal JPEG DCT basis and inverse transform (Home, MIT).
2const std = ;
3pub const BASIS: [8]@Vector(8, f32) = blk: {
4 @setEvalBranchQuota(10000);
5 var b: [8]@Vector(8, f32) = undefined;
6 for (0..8) |u| for (0..8) |x| {
7 const scale: f64 = if (u == 0) 0.3535533905932738 else 0.5;
8 b[u][x] = @floatCast(scale * @cos((@as(f64, @floatFromInt(x)) * 2 + 1) * @as(f64, @floatFromInt(u)) * std.math.pi / 16));
9 };
10 break :blk b;
11};
12
13pub fn inverse(block: *[64]i32, quant: *const [64]u16) void {
14 var work: [8][8]f32 = undefined;
15 for (0..8) |v| {
16 var coefficients: @Vector(8, f32) = undefined;
17 inline for (0..8) |u| coefficients[u] = @as(f32, @floatFromInt(block[v * 8 + u])) * @as(f32, @floatFromInt(quant[v * 8 + u]));
18 for (0..8) |x| {
19 var basis: @Vector(8, f32) = undefined;
20 inline for (0..8) |u| basis[u] = BASIS[u][x];
21 work[v][x] = @reduce(.Add, coefficients * basis);
22 }
23 }
24 for (0..8) |y| for (0..8) |x| {
25 var values: @Vector(8, f32) = undefined;
26 var basis: @Vector(8, f32) = undefined;
27 inline for (0..8) |v| {
28 values[v] = work[v][x];
29 basis[v] = BASIS[v][y];
30 }
31 block[y * 8 + x] = @intFromFloat(std.math.clamp(@round(@reduce(.Add, values * basis)) + 128, 0, 255));
32 };
33}
34test "inverse JPEG DC scale and zero AC are exact" {
35 var block: [64]i32 = @splat(0);
36 block[0] = 80;
37 inverse(&block, &@as([64]u16, @splat(1)));
38 for (block) |v| try std.testing.expectEqual(@as(i32, 138), v);
39}
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}
packages/image/src/jpeg_encoder_tests.zigadded+23-0
Changes to packages/image/src/jpeg_encoder_tests.zig
@@ -0,0 +1,23 @@
1const std = ;
2const image = ;
3const jpeg = ;
4test {
5 _ = jpeg;
6}
7test "native JPEG encodes scan data readable by the Home decoder" {
8 var img = try image.Image.init(std.testing.allocator, 17, 9, .rgb8);
9 defer img.deinit();
10 for (0..17 * 9) |i| {
11 @memcpy(img.pixels[i * 3 ..][0..3], &[_]u8{ 200, 50, 100 });
12 }
13 const encoded = try jpeg.encodeWithQuality(std.testing.allocator, &img, 98);
14 defer std.testing.allocator.free(encoded);
15 var decoded = try jpeg.decode(std.testing.allocator, encoded);
16 defer decoded.deinit();
17 try std.testing.expectEqual(@as(u32, 17), decoded.width);
18 try std.testing.expectEqual(@as(u32, 9), decoded.height);
19 const pixel = decoded.getPixel(8, 4).?;
20 try std.testing.expectApproxEqAbs(@as(f64, 200), @as(f64, @floatFromInt(pixel.r)), 5);
21 try std.testing.expectApproxEqAbs(@as(f64, 50), @as(f64, @floatFromInt(pixel.g)), 5);
22 try std.testing.expectApproxEqAbs(@as(f64, 100), @as(f64, @floatFromInt(pixel.b)), 5);
23}
packages/video/src/containers/mjpeg_stream.zigadded+125-0
Changes to packages/video/src/containers/mjpeg_stream.zig
@@ -0,0 +1,125 @@
1//! Home video: bounded-memory Matroska/MJPEG stream writer.
2//! EBML IDs and track layout adapted from Home webm/mkv muxers (MIT).
3const std = ;
4pub const Sink = struct { context: *anyopaque, write: *const fn (*anyopaque, []const u8) anyerror!void };
5fn number(value: u64, buf: *[8]u8) []const u8 {
6 std.mem.writeInt(u64, buf, value, .big);
7 var i: usize = 0;
8 while (i < 7 and buf[i] == 0) : (i += 1) {}
9 return buf[i..];
10}
11fn vint(value: u64, buf: *[8]u8) ![]const u8 {
12 var n: usize = 1;
13 while (n < 8 and value >= (@as(u64, 1) << @intCast(n * 7)) - 1) : (n += 1) {}
14 if (value >= (@as(u64, 1) << 56) - 1) return error.FrameTooLarge;
15 const marked = value | (@as(u64, 1) << @intCast(n * 7));
16 std.mem.writeInt(u64, buf, marked, .big);
17 return buf[8 - n ..];
18}
19const Buffer = struct {
20 data: [1024]u8 = undefined,
21 len: usize = 0,
22 fn bytes(self: *Buffer, b: []const u8) void {
23 std.debug.assert(self.len + b.len <= self.data.len);
24 @memcpy(self.data[self.len..][0..b.len], b);
25 self.len += b.len;
26 }
27 fn element(self: *Buffer, id: u32, value: []const u8) void {
28 var b: [8]u8 = undefined;
29 self.bytes(number(id, &b));
30 self.bytes(vint(value.len, &b) catch unreachable);
31 self.bytes(value);
32 }
33 fn uint(self: *Buffer, id: u32, value: u64) void {
34 var b: [8]u8 = undefined;
35 self.element(id, number(value, &b));
36 }
37 fn slice(self: *const Buffer) []const u8 {
38 return self.data[0..self.len];
39 }
40};
41pub const Writer = struct {
42 sink: Sink,
43 first_ns: u64 = 0,
44 last_ns: u64 = 0,
45 frames: u64 = 0,
46 failed: bool = false,
47 pub fn init(sink: Sink, width: u32, height: u32, fps_num: u32, fps_den: u32) !Writer {
48 if (width == 0 or height == 0 or fps_num == 0 or fps_den == 0 or fps_num > 1000000 or fps_den > 1000000) return error.InvalidConfiguration;
49 var ebml: Buffer = .{};
50 ebml.uint(0x4286, 1);
51 ebml.uint(0x42f7, 1);
52 ebml.uint(0x42f2, 4);
53 ebml.uint(0x42f3, 8);
54 ebml.element(0x4282, "matroska");
55 ebml.uint(0x4287, 4);
56 ebml.uint(0x4285, 2);
57 var header: Buffer = .{};
58 header.element(0x1a45dfa3, ebml.slice());
59 header.bytes(&.{ 0x18, 0x53, 0x80, 0x67, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff });
60 var info: Buffer = .{};
61 info.uint(0x2ad7b1, 1000000);
62 info.element(0x4d80, "Home");
63 info.element(0x5741, "Home native MJPEG");
64 header.element(0x1549a966, info.slice());
65 var video: Buffer = .{};
66 video.uint(0xb0, width);
67 video.uint(0xba, height);
68 var track: Buffer = .{};
69 track.uint(0xd7, 1);
70 track.uint(0x73c5, 1);
71 track.uint(0x83, 1);
72 track.uint(0x9c, 0);
73 track.element(0x86, "V_MJPEG");
74 track.uint(0x23e383, @as(u64, 1000000000) * fps_den / fps_num);
75 track.element(0xe0, video.slice());
76 var entry: Buffer = .{};
77 entry.element(0xae, track.slice());
78 header.element(0x1654ae6b, entry.slice());
79 try sink.write(sink.context, header.slice());
80 return .{ .sink = sink };
81 }
82 pub fn frame(self: *Writer, jpeg: []const u8, timestamp_ns: u64) !void {
83 if (self.failed) return error.WriterFailed;
84 if (jpeg.len < 4 or !std.mem.startsWith(u8, jpeg, &.{ 255, 216 }) or !std.mem.endsWith(u8, jpeg, &.{ 255, 217 })) return error.InvalidFrame;
85 if (self.frames != 0 and timestamp_ns <= self.last_ns) return error.InvalidTimestamp;
86 const first = if (self.frames == 0) timestamp_ns else self.first_ns;
87 var time: Buffer = .{};
88 time.uint(0xe7, (timestamp_ns - first) / 1000000);
89 var size: [8]u8 = undefined;
90 const block_size = try vint(jpeg.len + 4, &size);
91 var header: Buffer = .{};
92 header.bytes(&.{ 0x1f, 0x43, 0xb6, 0x75 });
93 var cluster_size: [8]u8 = undefined;
94 header.bytes(try vint(time.len + 1 + block_size.len + 4 + jpeg.len, &cluster_size));
95 header.bytes(time.slice());
96 header.bytes(&.{0xa3});
97 header.bytes(block_size);
98 header.bytes(&.{ 0x81, 0, 0, 0x80 });
99 errdefer self.failed = true;
100 try self.sink.write(self.sink.context, header.slice());
101 try self.sink.write(self.sink.context, jpeg);
102 self.first_ns = first;
103 self.last_ns = timestamp_ns;
104 self.frames += 1;
105 }
106};
107test "stream rejects timestamps and propagates sink failure without accumulating frames" {
108 const Output = struct {
109 data: std.ArrayList(u8) = .empty,
110 fail: bool = false,
111 fn write(ctx: *anyopaque, bytes: []const u8) anyerror!void {
112 const self: *@This() = @ptrCast(@alignCast(ctx));
113 if (self.fail) return error.TestSinkFailure;
114 try self.data.appendSlice(std.testing.allocator, bytes);
115 }
116 };
117 var out: Output = .{};
118 defer out.data.deinit(std.testing.allocator);
119 var writer = try Writer.init(.{ .context = &out, .write = Output.write }, 32, 24, 30, 1);
120 try writer.frame(&.{ 255, 216, 1, 2, 255, 217 }, 1000);
121 try std.testing.expectError(error.InvalidTimestamp, writer.frame(&.{ 255, 216, 255, 217 }, 1000));
122 out.fail = true;
123 try std.testing.expectError(error.TestSinkFailure, writer.frame(&.{ 255, 216, 255, 217 }, 2000));
124 try std.testing.expectError(error.WriterFailed, writer.frame(&.{ 255, 216, 255, 217 }, 3000));
125}