also looking at this
feat(media): implement native JPEG coding and bounded MJPEG streaming
#912
5 files
+442
-263
| @@ -198,7 +198,7 @@ const JpegDecoder = struct { | ||
| 198 | 198 | b2 = try self.readByte(); |
| 199 | 199 | } |
| 200 | 200 | |
| 201 | return @enumFromInt((@as(u16, 0xFF) << 8) | b2); | |
| 201 | return @fromBackingInt(@intCast((@as(u16, 0xFF) << 8) | b2)); | |
| 202 | 202 | } |
| 203 | 203 | |
| 204 | 204 | fn skipMarker(self: *JpegDecoder) !void { |
| @@ -455,125 +455,7 @@ const JpegDecoder = struct { | ||
| 455 | 455 | // ============================================================================ |
| 456 | 456 | |
| 457 | 457 | fn 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 | ||
| 478 | fn 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 | ||
| 535 | fn 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); | |
| 577 | 459 | } |
| 578 | 460 | |
| 579 | 461 | fn clamp(value: i32) i32 { |
| @@ -638,7 +520,7 @@ pub fn decode(allocator: std.mem.Allocator, data: []const u8) !Image { | ||
| 638 | 520 | .EOI => break, |
| 639 | 521 | else => { |
| 640 | 522 | // Skip unknown markers |
| 641 | if (@intFromEnum(marker) >= 0xFFC0) { | |
| 523 | if (@backingInt(marker) >= 0xFFC0) { | |
| 642 | 524 | try decoder.skipMarker(); |
| 643 | 525 | } |
| 644 | 526 | }, |
| @@ -720,149 +602,22 @@ pub fn decode(allocator: std.mem.Allocator, data: []const u8) !Image { | ||
| 720 | 602 | // JPEG Encoder (basic quality encoding) |
| 721 | 603 | // ============================================================================ |
| 722 | 604 | |
| 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. | |
| 723 | 607 | pub 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); | |
| 814 | 609 | } |
| 815 | ||
| 816 | fn 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, | |
| 610 | pub 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, | |
| 846 | 616 | }; |
| 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); | |
| 866 | 621 | } |
| 867 | 622 | |
| 868 | 623 | // ============================================================================ |
renamed throughout · 10 hidden lines
| @@ -870,8 +625,8 @@ fn writeStandardHuffmanTables(allocator: std.mem.Allocator, output: *std.ArrayLi renamed throughout | ||
| 870 | 625 | // ============================================================================ |
| 871 | 626 | |
| 872 | 627 | test "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)); | |
| 875 | 630 | } |
| 876 | 631 | |
| 877 | 632 | test "YCbCr to RGB conversion" { |