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" {