From a9a5bba54a1fff25d9b6dfdcc2c959bfc87cd1a7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Mar 2025 10:40:00 -0700 Subject: [PATCH] cool --- src/CLAUDE.md | 51 ++++++++-- src/image/encoder.zig | 66 +++++++++++++ src/image/encoder_darwin.zig | 2 + src/image/encoder_linux.zig | 4 +- src/image/encoder_tests.zig | 183 +++++++++++++++++++++++++++++++++++ src/image/libpng.zig | 2 +- 6 files changed, 298 insertions(+), 10 deletions(-) diff --git a/src/CLAUDE.md b/src/CLAUDE.md index e74e0d4454..8304df75c5 100644 --- a/src/CLAUDE.md +++ b/src/CLAUDE.md @@ -41,14 +41,44 @@ Run `zig test src/image/pixel_format.zig` to test the pixel format conversion. Run `zig test src/image/streaming_tests.zig` to test the streaming and encoder functionality. Run `zig test src/image/encoder_tests.zig` to test the encoding and transcoding functionality. +4. JavaScript bindings: + +Match these TypeScript signatures: + +```ts +namespace Bun { + interface Image { + readonly encoding: "jpg" | "png" | "webp" | "avif"; + + size(): Promise<{ width: number; height: number }>; + resize(width: number, height: number, quality?: number): Image; + resize(options: { + x: number; + y: number; + width: number; + height: number; + quality?: number; + }): Image; + bytes(): Promise; + blob(): Promise; + jpg(options: { quality?: number }): Image; + png(options: { quality?: number }): Image; + webp(options: { quality?: number }): Image; + avif(options: { quality?: number }): Image; + } + function image(bytes: Uint8Array): Image; +} +``` + Use Zig's @Vector intrinsics for SIMD. Here's a couple examples: ``` + /// Count the occurrences of a character in an ASCII byte array /// uses SIMD pub fn countChar(self: string, char: u8) usize { - var total: usize = 0; - var remaining = self; +var total: usize = 0; +var remaining = self; const splatted: AsciiVector = @splat(char); @@ -65,13 +95,14 @@ pub fn countChar(self: string, char: u8) usize { } return total; + } -fn indexOfInterestingCharacterInStringLiteral(text_: []const u8, quote: u8) ?usize { - var text = text_; - const quote_: @Vector(strings.ascii_vector_size, u8) = @splat(@as(u8, quote)); - const backslash: @Vector(strings.ascii_vector_size, u8) = @splat(@as(u8, '\\')); - const V1x16 = strings.AsciiVectorU1; +fn indexOfInterestingCharacterInStringLiteral(text*: []const u8, quote: u8) ?usize { +var text = text*; +const quote\_: @Vector(strings.ascii_vector_size, u8) = @splat(@as(u8, quote)); +const backslash: @Vector(strings.ascii_vector_size, u8) = @splat(@as(u8, '\\')); +const V1x16 = strings.AsciiVectorU1; while (text.len >= strings.ascii_vector_size) { const vec: strings.AsciiVector = text[0..strings.ascii_vector_size].*; @@ -92,7 +123,9 @@ fn indexOfInterestingCharacterInStringLiteral(text_: []const u8, quote: u8) ?usi } return null; + } + ``` Some tips for working with Zig: @@ -219,3 +252,7 @@ Here's a complete list of Zig builtin functions: - @workGroupId - @workGroupSize - @workItemId + +``` + +``` diff --git a/src/image/encoder.zig b/src/image/encoder.zig index 86a8e9ea9c..be7ce2751c 100644 --- a/src/image/encoder.zig +++ b/src/image/encoder.zig @@ -14,6 +14,8 @@ pub const ImageFormat = enum { PNG, WEBP, AVIF, + TIFF, + HEIC, /// Get the file extension for this format pub fn fileExtension(self: ImageFormat) []const u8 { @@ -22,6 +24,8 @@ pub const ImageFormat = enum { .PNG => ".png", .WEBP => ".webp", .AVIF => ".avif", + .TIFF => ".tiff", + .HEIC => ".heic", }; } @@ -32,6 +36,8 @@ pub const ImageFormat = enum { .PNG => "image/png", .WEBP => "image/webp", .AVIF => "image/avif", + .TIFF => "image/tiff", + .HEIC => "image/heic", }; } }; @@ -241,6 +247,38 @@ pub fn encodePNG( return try encode(allocator, source, width, height, src_format, options); } +// Simple TIFF encoding with default options +pub fn encodeTIFF( + allocator: std.mem.Allocator, + source: []const u8, + width: usize, + height: usize, + src_format: PixelFormat, +) ![]u8 { + const options = EncodingOptions{ + .format = .TIFF, + }; + + return try encode(allocator, source, width, height, src_format, options); +} + +// HEIC encoding with quality setting +pub fn encodeHEIC( + allocator: std.mem.Allocator, + source: []const u8, + width: usize, + height: usize, + src_format: PixelFormat, + quality: u8, +) ![]u8 { + const options = EncodingOptions{ + .format = .HEIC, + .quality = .{ .quality = quality }, + }; + + return try encode(allocator, source, width, height, src_format, options); +} + /// Transcode image data directly from one format to another without decoding to raw pixels /// This is more efficient than decoding and re-encoding when converting between file formats pub fn transcode( @@ -289,4 +327,32 @@ pub fn transcodeToPNG( }; return try transcode(allocator, jpeg_data, .JPEG, .PNG, options); +} + +/// Transcode an image file to TIFF +pub fn transcodeToTIFF( + allocator: std.mem.Allocator, + source_data: []const u8, + source_format: ImageFormat, +) ![]u8 { + const options = EncodingOptions{ + .format = .TIFF, + }; + + return try transcode(allocator, source_data, source_format, .TIFF, options); +} + +/// Transcode an image file to HEIC with specified quality +pub fn transcodeToHEIC( + allocator: std.mem.Allocator, + source_data: []const u8, + source_format: ImageFormat, + quality: u8, +) ![]u8 { + const options = EncodingOptions{ + .format = .HEIC, + .quality = .{ .quality = quality }, + }; + + return try transcode(allocator, source_data, source_format, .HEIC, options); } \ No newline at end of file diff --git a/src/image/encoder_darwin.zig b/src/image/encoder_darwin.zig index 87726c3666..4e8df95651 100644 --- a/src/image/encoder_darwin.zig +++ b/src/image/encoder_darwin.zig @@ -29,6 +29,8 @@ fn getUTIForFormat(format: ImageFormat) c.CFStringRef { .PNG => CFSTR("public.png"), .WEBP => CFSTR("org.webmproject.webp"), // WebP type .AVIF => CFSTR("public.avif"), // AVIF type + .TIFF => CFSTR("public.tiff"), // TIFF type + .HEIC => CFSTR("public.heic"), // HEIC type }; } diff --git a/src/image/encoder_linux.zig b/src/image/encoder_linux.zig index b40ea0077e..4865ea0b29 100644 --- a/src/image/encoder_linux.zig +++ b/src/image/encoder_linux.zig @@ -180,7 +180,7 @@ fn encodeJPEG( libjpeg.jpeg_finish_compress(&cinfo); // Copy the JPEG data to our own buffer - var result = try allocator.alloc(u8, jpeg_buffer_size); + const result = try allocator.alloc(u8, jpeg_buffer_size); @memcpy(result, jpeg_buffer[0..jpeg_buffer_size]); // Clean up @@ -247,7 +247,7 @@ fn encodeWebP( } // Copy to our own buffer - var result = try allocator.alloc(u8, output_size); + const result = try allocator.alloc(u8, output_size); @memcpy(result, output[0..output_size]); // Free WebP's output buffer diff --git a/src/image/encoder_tests.zig b/src/image/encoder_tests.zig index f1d14d6419..04b167bfce 100644 --- a/src/image/encoder_tests.zig +++ b/src/image/encoder_tests.zig @@ -362,3 +362,186 @@ test "Transcode with different quality settings" { // so we use a loose check try testing.expect(jpeg_sizes[0] <= jpeg_sizes[2]); } + +// Test TIFF encoding +test "Encode TIFF" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // Create test RGBA image + const width = 200; + const height = 200; + const image_format = PixelFormat.RGBA; + + const image_data = try createTestImage(allocator, width, height, image_format); + + // Encode to TIFF + const encoded_tiff = encoder.encodeTIFF(allocator, image_data, width, height, image_format) catch |err| { + if (err == error.NotImplemented) { + std.debug.print("TIFF encoder not implemented on this platform, skipping test\n", .{}); + return; + } + return err; + }; + defer allocator.free(encoded_tiff); + + // Verify we got some data back + try testing.expect(encoded_tiff.len > 0); + + // Verify TIFF signature (either II or MM for Intel or Motorola byte order) + try testing.expect(encoded_tiff[0] == encoded_tiff[1]); // Either II or MM + try testing.expect(encoded_tiff[0] == 'I' or encoded_tiff[0] == 'M'); + + // Check for TIFF identifier (42 in appropriate byte order) + if (encoded_tiff[0] == 'I') { + // Little endian (Intel) + try testing.expectEqual(@as(u8, 42), encoded_tiff[2]); + try testing.expectEqual(@as(u8, 0), encoded_tiff[3]); + } else { + // Big endian (Motorola) + try testing.expectEqual(@as(u8, 0), encoded_tiff[2]); + try testing.expectEqual(@as(u8, 42), encoded_tiff[3]); + } + + // Optionally save the file for visual inspection + if (false) { + try saveToFile(allocator, encoded_tiff, "test_output.tiff"); + } +} + +// Test HEIC encoding +test "Encode HEIC" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // Create test RGBA image + const width = 200; + const height = 200; + const image_format = PixelFormat.RGBA; + + const image_data = try createTestImage(allocator, width, height, image_format); + + // Encode to HEIC with quality 80 + const encoded_heic = encoder.encodeHEIC(allocator, image_data, width, height, image_format, 80) catch |err| { + if (err == error.NotImplemented or err == error.DestinationCreationFailed) { + std.debug.print("HEIC encoder not implemented or not supported on this platform, skipping test\n", .{}); + return; + } + return err; + }; + defer allocator.free(encoded_heic); + + // Verify we got some data back + try testing.expect(encoded_heic.len > 0); + + // HEIC files start with ftyp box + // Check for 'ftyp' marker at position 4-8 + if (encoded_heic.len >= 8) { + try testing.expectEqual(@as(u8, 'f'), encoded_heic[4]); + try testing.expectEqual(@as(u8, 't'), encoded_heic[5]); + try testing.expectEqual(@as(u8, 'y'), encoded_heic[6]); + try testing.expectEqual(@as(u8, 'p'), encoded_heic[7]); + } + + // Optionally save the file for visual inspection + if (false) { + try saveToFile(allocator, encoded_heic, "test_output.heic"); + } +} + +// Test transcoding to TIFF +test "Transcode to TIFF" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // Create test RGBA image + const width = 200; + const height = 200; + const image_format = PixelFormat.RGBA; + + const image_data = try createTestImage(allocator, width, height, image_format); + + // First encode to PNG + const png_data = encoder.encodePNG(allocator, image_data, width, height, image_format) catch |err| { + if (err == error.NotImplemented) { + std.debug.print("PNG encoder not implemented on this platform, skipping test\n", .{}); + return; + } + return err; + }; + defer allocator.free(png_data); + + // Transcode PNG to TIFF + const transcoded_tiff = encoder.transcodeToTIFF(allocator, png_data, .PNG) catch |err| { + if (err == error.NotImplemented) { + std.debug.print("Transcode to TIFF not implemented on this platform, skipping test\n", .{}); + return; + } + return err; + }; + defer allocator.free(transcoded_tiff); + + // Verify TIFF signature + try testing.expect(transcoded_tiff.len > 0); + try testing.expect(transcoded_tiff[0] == transcoded_tiff[1]); // Either II or MM + try testing.expect(transcoded_tiff[0] == 'I' or transcoded_tiff[0] == 'M'); + + // Optionally save the files for visual inspection + if (false) { + try saveToFile(allocator, png_data, "test_original.png"); + try saveToFile(allocator, transcoded_tiff, "test_transcoded.tiff"); + } +} + +// Test transcoding to HEIC +test "Transcode to HEIC" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // Create test RGBA image + const width = 200; + const height = 200; + const image_format = PixelFormat.RGBA; + + const image_data = try createTestImage(allocator, width, height, image_format); + + // First encode to PNG + const png_data = encoder.encodePNG(allocator, image_data, width, height, image_format) catch |err| { + if (err == error.NotImplemented) { + std.debug.print("PNG encoder not implemented on this platform, skipping test\n", .{}); + return; + } + return err; + }; + defer allocator.free(png_data); + + // Transcode PNG to HEIC + const transcoded_heic = encoder.transcodeToHEIC(allocator, png_data, .PNG, 80) catch |err| { + if (err == error.NotImplemented or err == error.DestinationCreationFailed) { + std.debug.print("Transcode to HEIC not implemented or not supported on this platform, skipping test\n", .{}); + return; + } + return err; + }; + defer allocator.free(transcoded_heic); + + // Verify HEIC signature (look for ftyp marker) + try testing.expect(transcoded_heic.len > 0); + + if (transcoded_heic.len >= 8) { + try testing.expectEqual(@as(u8, 'f'), transcoded_heic[4]); + try testing.expectEqual(@as(u8, 't'), transcoded_heic[5]); + try testing.expectEqual(@as(u8, 'y'), transcoded_heic[6]); + try testing.expectEqual(@as(u8, 'p'), transcoded_heic[7]); + } + + // Optionally save the files for visual inspection + if (false) { + try saveToFile(allocator, png_data, "test_original.png"); + try saveToFile(allocator, transcoded_heic, "test_transcoded.heic"); + } +} diff --git a/src/image/libpng.zig b/src/image/libpng.zig index c2b5ca7f6b..3e0e7e1b8f 100644 --- a/src/image/libpng.zig +++ b/src/image/libpng.zig @@ -154,7 +154,7 @@ fn loadSymbol(comptime T: type, name: [:0]const u8) ?T { if (lib_handle) |handle| { const symbol = std.c.dlsym(handle, name.ptr); if (symbol == null) return null; - return @ptrCast(T, symbol); + return @as(T, @ptrCast(symbol)); } return null; }