This commit is contained in:
Jarred Sumner
2025-03-23 10:40:00 -07:00
parent a0bcd46411
commit a9a5bba54a
6 changed files with 298 additions and 10 deletions

View File

@@ -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<Buffer>;
blob(): Promise<Blob>;
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
```
```

View File

@@ -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);
}

View File

@@ -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
};
}

View File

@@ -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

View File

@@ -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");
}
}

View File

@@ -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;
}