Files
bun.sh/src/image/encoder_tests.zig
Jarred Sumner a9a5bba54a cool
2025-03-23 10:40:00 -07:00

548 lines
19 KiB
Zig

const std = @import("std");
const testing = std.testing;
const encoder = @import("encoder.zig");
const pixel_format = @import("pixel_format.zig");
const PixelFormat = pixel_format.PixelFormat;
// Mock testing data creation
fn createTestImage(allocator: std.mem.Allocator, width: usize, height: usize, format: PixelFormat) ![]u8 {
const bytes_per_pixel = format.getBytesPerPixel();
const buffer_size = width * height * bytes_per_pixel;
var buffer = try allocator.alloc(u8, buffer_size);
errdefer allocator.free(buffer);
// Fill with a simple gradient pattern
for (0..height) |y| {
for (0..width) |x| {
const pixel_index = (y * width + x) * bytes_per_pixel;
switch (format) {
.Gray => {
// Simple diagonal gradient
buffer[pixel_index] = @as(u8, @intCast((x + y) % 256));
},
.GrayAlpha => {
// Gray gradient with full alpha
buffer[pixel_index] = @as(u8, @intCast((x + y) % 256));
buffer[pixel_index + 1] = 255; // Full alpha
},
.RGB => {
// Red gradient in x, green gradient in y, blue constant
buffer[pixel_index] = @as(u8, @intCast(x % 256)); // R
buffer[pixel_index + 1] = @as(u8, @intCast(y % 256)); // G
buffer[pixel_index + 2] = 128; // B constant
},
.RGBA => {
// RGB gradient with full alpha
buffer[pixel_index] = @as(u8, @intCast(x % 256)); // R
buffer[pixel_index + 1] = @as(u8, @intCast(y % 256)); // G
buffer[pixel_index + 2] = 128; // B constant
buffer[pixel_index + 3] = 255; // Full alpha
},
.BGR => {
// Blue gradient in x, green gradient in y, red constant
buffer[pixel_index] = 128; // B constant
buffer[pixel_index + 1] = @as(u8, @intCast(y % 256)); // G
buffer[pixel_index + 2] = @as(u8, @intCast(x % 256)); // R
},
.BGRA => {
// BGR gradient with full alpha
buffer[pixel_index] = 128; // B constant
buffer[pixel_index + 1] = @as(u8, @intCast(y % 256)); // G
buffer[pixel_index + 2] = @as(u8, @intCast(x % 256)); // R
buffer[pixel_index + 3] = 255; // Full alpha
},
.ARGB => {
// ARGB format
buffer[pixel_index] = 255; // A full
buffer[pixel_index + 1] = @as(u8, @intCast(x % 256)); // R
buffer[pixel_index + 2] = @as(u8, @intCast(y % 256)); // G
buffer[pixel_index + 3] = 128; // B constant
},
.ABGR => {
// ABGR format
buffer[pixel_index] = 255; // A full
buffer[pixel_index + 1] = 128; // B constant
buffer[pixel_index + 2] = @as(u8, @intCast(y % 256)); // G
buffer[pixel_index + 3] = @as(u8, @intCast(x % 256)); // R
},
}
}
}
return buffer;
}
// Utility to save an encoded image to a file for visual inspection
fn saveToFile(_: std.mem.Allocator, data: []const u8, filename: []const u8) !void {
const file = try std.fs.cwd().createFile(filename, .{});
defer file.close();
try file.writeAll(data);
}
test "Encode JPEG" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Create test RGB image
const width = 256;
const height = 256;
const image_format = PixelFormat.RGB;
const image_data = try createTestImage(allocator, width, height, image_format);
// Encode to JPEG with quality 80
const quality = 80;
const encoded_jpeg = try encoder.encodeJPEG(allocator, image_data, width, height, image_format, quality);
// Verify we got some data back (simple sanity check)
try testing.expect(encoded_jpeg.len > 0);
// Optionally save the file for visual inspection
// Note: This is normally disabled in automated tests
if (false) {
try saveToFile(allocator, encoded_jpeg, "test_output.jpg");
}
}
test "Encode PNG" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Create test RGBA image
const width = 256;
const height = 256;
const image_format = PixelFormat.RGBA;
const image_data = try createTestImage(allocator, width, height, image_format);
// Encode to PNG
const encoded_png = try encoder.encodePNG(allocator, image_data, width, height, image_format);
// Verify we got some data back
try testing.expect(encoded_png.len > 0);
// Optionally save the file for visual inspection
// Note: This is normally disabled in automated tests
if (false) {
try saveToFile(allocator, encoded_png, "test_output.png");
}
}
// Test various pixel format conversions
test "Encode different pixel formats" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Create test images with different pixel formats
const width = 100;
const height = 100;
const formats = [_]PixelFormat{
.Gray,
.GrayAlpha,
.RGB,
.RGBA,
.BGR,
.BGRA,
};
var test_failures = false;
for (formats) |format| {
const image_data = try createTestImage(allocator, width, height, format);
// Set up encoding options
const options = encoder.EncodingOptions{
.format = .JPEG,
.quality = .{ .quality = 85 },
};
// Encode the image
const encoded_data = encoder.encode(allocator, image_data, width, height, format, options) catch |err| {
// If this specific format causes an error, note it but continue with other formats
if (err == error.ImageCreationFailed or err == error.NotImplemented or err == error.UnsupportedColorSpace) {
std.debug.print("Format {any} encoding failed: {s}\n", .{ format, @errorName(err) });
test_failures = true;
continue;
}
return err;
};
defer allocator.free(encoded_data);
// Basic validation
try testing.expect(encoded_data.len > 0);
// Verify JPEG signature
try testing.expect(encoded_data[0] == 0xFF);
try testing.expect(encoded_data[1] == 0xD8);
}
// If some formats failed but others succeeded, that's OK
// This makes the test more portable across platforms with different capabilities
if (test_failures) {
std.debug.print("Note: Some formats failed but test continued\n", .{});
}
}
// Test direct transcoding between formats
test "Transcode PNG to JPEG" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Create test RGBA image
const width = 256;
const height = 256;
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 JPEG
const jpeg_options = encoder.EncodingOptions{
.format = .JPEG,
.quality = .{ .quality = 90 },
};
const transcoded_jpeg = encoder.transcode(
allocator,
png_data,
.PNG,
.JPEG,
jpeg_options,
) catch |err| {
if (err == error.NotImplemented) {
std.debug.print("Transcode not implemented on this platform, skipping test\n", .{});
return;
}
return err;
};
defer allocator.free(transcoded_jpeg);
// Verify JPEG signature
try testing.expect(transcoded_jpeg.len > 0);
try testing.expect(transcoded_jpeg[0] == 0xFF);
try testing.expect(transcoded_jpeg[1] == 0xD8);
try testing.expect(transcoded_jpeg[2] == 0xFF);
// Optionally save the files for visual inspection
if (false) {
try saveToFile(allocator, png_data, "test_original.png");
try saveToFile(allocator, transcoded_jpeg, "test_transcoded.jpg");
}
}
// Test round trip transcoding
test "Transcode Round Trip (PNG -> JPEG -> PNG)" {
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 JPEG
const transcoded_jpeg = encoder.transcodeToJPEG(allocator, png_data, 90) catch |err| {
if (err == error.NotImplemented) {
std.debug.print("TranscodeToJPEG not implemented on this platform, skipping test\n", .{});
return;
}
return err;
};
defer allocator.free(transcoded_jpeg);
// Now transcode back to PNG
const transcoded_png = encoder.transcodeToPNG(allocator, transcoded_jpeg) catch |err| {
if (err == error.NotImplemented) {
std.debug.print("TranscodeToPNG not implemented on this platform, skipping test\n", .{});
return;
}
return err;
};
defer allocator.free(transcoded_png);
// Verify PNG signature
try testing.expect(transcoded_png.len > 0);
try testing.expectEqual(@as(u8, 0x89), transcoded_png[0]);
try testing.expectEqual(@as(u8, 0x50), transcoded_png[1]); // P
try testing.expectEqual(@as(u8, 0x4E), transcoded_png[2]); // N
try testing.expectEqual(@as(u8, 0x47), transcoded_png[3]); // G
// Optionally save the files for visual inspection
if (false) {
try saveToFile(allocator, png_data, "test_original.png");
try saveToFile(allocator, transcoded_jpeg, "test_intermediate.jpg");
try saveToFile(allocator, transcoded_png, "test_roundtrip.png");
}
}
// Test transcoding with various quality settings
test "Transcode with different quality settings" {
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 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);
// Test different quality levels for JPEG
const qualities = [_]u8{ 30, 60, 90 };
var jpeg_sizes = [qualities.len]usize{ 0, 0, 0 };
for (qualities, 0..) |quality, i| {
const transcoded_jpeg = encoder.transcodeToJPEG(allocator, png_data, quality) catch |err| {
if (err == error.NotImplemented) {
std.debug.print("TranscodeToJPEG not implemented on this platform, skipping test\n", .{});
return;
}
return err;
};
defer allocator.free(transcoded_jpeg);
// Verify JPEG signature
try testing.expect(transcoded_jpeg.len > 0);
try testing.expect(transcoded_jpeg[0] == 0xFF);
try testing.expect(transcoded_jpeg[1] == 0xD8);
// Store size for comparison
jpeg_sizes[i] = transcoded_jpeg.len;
// Optionally save the files for visual inspection
if (false) {
const filename = try std.fmt.allocPrint(allocator, "test_quality_{d}.jpg", .{quality});
defer allocator.free(filename);
try saveToFile(allocator, transcoded_jpeg, filename);
}
}
// Verify that higher quality generally means larger file
// Note: This is a general trend but not guaranteed for all images
// 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");
}
}