mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
355 lines
15 KiB
Zig
355 lines
15 KiB
Zig
const std = @import("std");
|
|
const pixel_format = @import("pixel_format.zig");
|
|
const PixelFormat = pixel_format.PixelFormat;
|
|
const EncodingOptions = @import("encoder.zig").EncodingOptions;
|
|
const ImageFormat = @import("encoder.zig").ImageFormat;
|
|
|
|
// Import the required macOS frameworks for type definitions only
|
|
const c = @cImport({
|
|
@cInclude("CoreFoundation/CoreFoundation.h");
|
|
@cInclude("CoreGraphics/CoreGraphics.h");
|
|
@cInclude("ImageIO/ImageIO.h");
|
|
@cInclude("dlfcn.h");
|
|
});
|
|
|
|
// Function pointer types for dynamically loaded functions
|
|
const CoreFrameworkFunctions = struct {
|
|
// CoreFoundation functions
|
|
CFStringCreateWithBytes: *const @TypeOf(c.CFStringCreateWithBytes),
|
|
CFRelease: *const @TypeOf(c.CFRelease),
|
|
CFDataCreateMutable: *const @TypeOf(c.CFDataCreateMutable),
|
|
CFDataGetLength: *const @TypeOf(c.CFDataGetLength),
|
|
CFDataGetBytePtr: *const @TypeOf(c.CFDataGetBytePtr),
|
|
CFDictionaryCreateMutable: *const @TypeOf(c.CFDictionaryCreateMutable),
|
|
CFDictionarySetValue: *const @TypeOf(c.CFDictionarySetValue),
|
|
CFNumberCreate: *const @TypeOf(c.CFNumberCreate),
|
|
|
|
// CoreGraphics functions
|
|
CGDataProviderCreateWithData: *const @TypeOf(c.CGDataProviderCreateWithData),
|
|
CGDataProviderRelease: *const @TypeOf(c.CGDataProviderRelease),
|
|
CGImageSourceCreateWithDataProvider: *const @TypeOf(c.CGImageSourceCreateWithDataProvider),
|
|
CGImageSourceCreateImageAtIndex: *const @TypeOf(c.CGImageSourceCreateImageAtIndex),
|
|
CGImageRelease: *const @TypeOf(c.CGImageRelease),
|
|
CGImageDestinationCreateWithData: *const @TypeOf(c.CGImageDestinationCreateWithData),
|
|
CGImageDestinationAddImage: *const @TypeOf(c.CGImageDestinationAddImage),
|
|
CGImageDestinationFinalize: *const @TypeOf(c.CGImageDestinationFinalize),
|
|
CGColorSpaceCreateDeviceRGB: *const @TypeOf(c.CGColorSpaceCreateDeviceRGB),
|
|
CGColorSpaceCreateDeviceGray: *const @TypeOf(c.CGColorSpaceCreateDeviceGray),
|
|
CGColorSpaceRelease: *const @TypeOf(c.CGColorSpaceRelease),
|
|
CGImageCreate: *const @TypeOf(c.CGImageCreate),
|
|
|
|
kCFTypeDictionaryKeyCallBacks: *const @TypeOf(c.kCFTypeDictionaryKeyCallBacks),
|
|
kCFTypeDictionaryValueCallBacks: *const @TypeOf(c.kCFTypeDictionaryValueCallBacks),
|
|
kCGImageDestinationLossyCompressionQuality: *const anyopaque,
|
|
};
|
|
|
|
// Global instance of function pointers
|
|
var cf: CoreFrameworkFunctions = undefined;
|
|
|
|
// Framework handles
|
|
var core_foundation_handle: ?*anyopaque = null;
|
|
var core_graphics_handle: ?*anyopaque = null;
|
|
var image_io_handle: ?*anyopaque = null;
|
|
var failed_to_init_frameworks = false;
|
|
|
|
// Function to load a symbol from a library
|
|
fn loadSymbol(handle: ?*anyopaque, name: [*:0]const u8) ?*anyopaque {
|
|
const symbol = c.dlsym(handle, name);
|
|
if (symbol == null) {
|
|
std.debug.print("Failed to load symbol: {s}\n", .{name});
|
|
}
|
|
return symbol;
|
|
}
|
|
|
|
// Function to initialize the dynamic libraries and load all required symbols
|
|
fn _initFrameworks() void {
|
|
|
|
// Load frameworks
|
|
core_foundation_handle = c.dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", c.RTLD_LAZY);
|
|
if (core_foundation_handle == null) @panic("Failed to load CoreFoundation");
|
|
|
|
core_graphics_handle = c.dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", c.RTLD_LAZY);
|
|
if (core_graphics_handle == null) @panic("Failed to load CoreGraphics");
|
|
|
|
image_io_handle = c.dlopen("/System/Library/Frameworks/ImageIO.framework/ImageIO", c.RTLD_LAZY);
|
|
if (image_io_handle == null) @panic("Failed to load ImageIO");
|
|
|
|
// Initialize function pointers
|
|
cf.CFStringCreateWithBytes = @alignCast(@ptrCast(loadSymbol(core_foundation_handle, "CFStringCreateWithBytes").?));
|
|
cf.CFRelease = @alignCast(@ptrCast(loadSymbol(core_foundation_handle, "CFRelease").?));
|
|
cf.CFDataCreateMutable = @alignCast(@ptrCast(loadSymbol(core_foundation_handle, "CFDataCreateMutable").?));
|
|
cf.CFDataGetLength = @alignCast(@ptrCast(loadSymbol(core_foundation_handle, "CFDataGetLength").?));
|
|
cf.CFDataGetBytePtr = @alignCast(@ptrCast(loadSymbol(core_foundation_handle, "CFDataGetBytePtr").?));
|
|
cf.CFDictionaryCreateMutable = @alignCast(@ptrCast(loadSymbol(core_foundation_handle, "CFDictionaryCreateMutable").?));
|
|
cf.CFDictionarySetValue = @alignCast(@ptrCast(loadSymbol(core_foundation_handle, "CFDictionarySetValue").?));
|
|
cf.CFNumberCreate = @alignCast(@ptrCast(loadSymbol(core_foundation_handle, "CFNumberCreate").?));
|
|
cf.CGDataProviderCreateWithData = @alignCast(@ptrCast(loadSymbol(core_graphics_handle, "CGDataProviderCreateWithData").?));
|
|
cf.CGDataProviderRelease = @alignCast(@ptrCast(loadSymbol(core_graphics_handle, "CGDataProviderRelease").?));
|
|
cf.CGImageSourceCreateWithDataProvider = @alignCast(@ptrCast(loadSymbol(image_io_handle, "CGImageSourceCreateWithDataProvider").?));
|
|
cf.CGImageSourceCreateImageAtIndex = @alignCast(@ptrCast(loadSymbol(image_io_handle, "CGImageSourceCreateImageAtIndex").?));
|
|
cf.CGImageRelease = @alignCast(@ptrCast(loadSymbol(core_graphics_handle, "CGImageRelease").?));
|
|
cf.CGImageDestinationCreateWithData = @alignCast(@ptrCast(loadSymbol(image_io_handle, "CGImageDestinationCreateWithData").?));
|
|
cf.CGImageDestinationAddImage = @alignCast(@ptrCast(loadSymbol(image_io_handle, "CGImageDestinationAddImage").?));
|
|
cf.CGImageDestinationFinalize = @alignCast(@ptrCast(loadSymbol(image_io_handle, "CGImageDestinationFinalize").?));
|
|
cf.CGColorSpaceCreateDeviceRGB = @alignCast(@ptrCast(loadSymbol(core_graphics_handle, "CGColorSpaceCreateDeviceRGB").?));
|
|
cf.CGColorSpaceCreateDeviceGray = @alignCast(@ptrCast(loadSymbol(core_graphics_handle, "CGColorSpaceCreateDeviceGray").?));
|
|
cf.CGColorSpaceRelease = @alignCast(@ptrCast(loadSymbol(core_graphics_handle, "CGColorSpaceRelease").?));
|
|
cf.CGImageCreate = @alignCast(@ptrCast(loadSymbol(core_graphics_handle, "CGImageCreate").?));
|
|
cf.kCFTypeDictionaryKeyCallBacks = @alignCast(@ptrCast(loadSymbol(core_foundation_handle, "kCFTypeDictionaryKeyCallBacks").?));
|
|
cf.kCFTypeDictionaryValueCallBacks = @alignCast(@ptrCast(loadSymbol(core_foundation_handle, "kCFTypeDictionaryValueCallBacks").?));
|
|
const kCGImageDestinationLossyCompressionQuality: *const *const anyopaque = @alignCast(@ptrCast(loadSymbol(image_io_handle, "kCGImageDestinationLossyCompressionQuality").?));
|
|
cf.kCGImageDestinationLossyCompressionQuality = kCGImageDestinationLossyCompressionQuality.*;
|
|
}
|
|
|
|
var init_frameworks_once = std.once(_initFrameworks);
|
|
fn initFrameworks() void {
|
|
init_frameworks_once.call();
|
|
}
|
|
|
|
/// Helper to create a CoreFoundation string
|
|
fn CFSTR(str: []const u8) c.CFStringRef {
|
|
return cf.CFStringCreateWithBytes(
|
|
null,
|
|
str.ptr,
|
|
@as(c_long, @intCast(str.len)),
|
|
c.kCFStringEncodingUTF8,
|
|
@as(u8, 0), // Boolean false (0) for isExternalRepresentation
|
|
);
|
|
}
|
|
|
|
/// Create a UTI for the specified format
|
|
fn getUTIForFormat(format: ImageFormat) c.CFStringRef {
|
|
return switch (format) {
|
|
.JPEG => CFSTR("public.jpeg"),
|
|
.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
|
|
};
|
|
}
|
|
|
|
/// Transcode an image 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(
|
|
allocator: std.mem.Allocator,
|
|
source_data: []const u8,
|
|
source_format: ImageFormat,
|
|
target_format: ImageFormat,
|
|
options: EncodingOptions,
|
|
) ![]u8 {
|
|
// Initialize the frameworks if not already loaded
|
|
initFrameworks();
|
|
|
|
// Create a data provider from our input buffer
|
|
const data_provider = cf.CGDataProviderCreateWithData(
|
|
null, // Info parameter (unused)
|
|
source_data.ptr,
|
|
source_data.len,
|
|
null, // Release callback (we manage the memory ourselves)
|
|
);
|
|
defer cf.CGDataProviderRelease(data_provider);
|
|
|
|
// Create an image source from the data provider
|
|
const source_type_id = getUTIForFormat(source_format);
|
|
if (source_type_id == null) return error.CFStringCreationFailed;
|
|
defer cf.CFRelease(source_type_id);
|
|
|
|
const image_source = cf.CGImageSourceCreateWithDataProvider(data_provider, null);
|
|
if (image_source == null) {
|
|
return error.InvalidSourceImage;
|
|
}
|
|
defer cf.CFRelease(image_source);
|
|
|
|
// Get the image from the source
|
|
const cg_image = cf.CGImageSourceCreateImageAtIndex(image_source, 0, null);
|
|
if (cg_image == null) {
|
|
return error.ImageCreationFailed;
|
|
}
|
|
defer cf.CGImageRelease(cg_image);
|
|
|
|
// Create a mutable data object to hold the output
|
|
const data = cf.CFDataCreateMutable(null, 0);
|
|
if (data == null) {
|
|
return error.MemoryAllocationFailed;
|
|
}
|
|
defer cf.CFRelease(data);
|
|
|
|
// Create a CGImageDestination for the requested format
|
|
const type_id = getUTIForFormat(target_format);
|
|
if (type_id == null) return error.CFStringCreationFailed;
|
|
defer cf.CFRelease(type_id);
|
|
|
|
const destination = cf.CGImageDestinationCreateWithData(
|
|
data,
|
|
type_id,
|
|
1, // Number of images (just one)
|
|
null, // Options (none)
|
|
);
|
|
if (destination == null) {
|
|
return error.DestinationCreationFailed;
|
|
}
|
|
defer cf.CFRelease(destination);
|
|
|
|
// Create properties dictionary with quality setting
|
|
const properties = cf.CFDictionaryCreateMutable(
|
|
null,
|
|
0,
|
|
cf.kCFTypeDictionaryKeyCallBacks,
|
|
cf.kCFTypeDictionaryValueCallBacks,
|
|
);
|
|
defer cf.CFRelease(properties);
|
|
|
|
// Set compression quality
|
|
const quality_value = @as(f32, @floatFromInt(options.quality.quality)) / 100.0;
|
|
const quality_number = cf.CFNumberCreate(null, c.kCFNumberFloat32Type, &quality_value);
|
|
defer cf.CFRelease(quality_number);
|
|
cf.CFDictionarySetValue(properties, cf.kCGImageDestinationLossyCompressionQuality, quality_number);
|
|
|
|
// Add the image with properties
|
|
cf.CGImageDestinationAddImage(destination, cg_image, properties);
|
|
|
|
// Finalize the destination
|
|
if (!cf.CGImageDestinationFinalize(destination)) {
|
|
return error.EncodingFailed;
|
|
}
|
|
|
|
// Get the encoded data
|
|
const cf_data_len = cf.CFDataGetLength(data);
|
|
const cf_data_ptr = cf.CFDataGetBytePtr(data);
|
|
|
|
// Copy to a Zig-managed buffer
|
|
const output = try allocator.alloc(u8, @as(usize, @intCast(cf_data_len)));
|
|
@memcpy(output, cf_data_ptr[0..@as(usize, @intCast(cf_data_len))]);
|
|
|
|
return output;
|
|
}
|
|
|
|
/// MacOS implementation using CoreGraphics and ImageIO
|
|
pub fn encode(
|
|
allocator: std.mem.Allocator,
|
|
source: []const u8,
|
|
width: usize,
|
|
height: usize,
|
|
format: PixelFormat,
|
|
options: EncodingOptions,
|
|
) ![]u8 {
|
|
// Initialize the frameworks if not already loaded
|
|
initFrameworks();
|
|
|
|
// Early return if dimensions are invalid
|
|
if (width == 0 or height == 0) {
|
|
return error.InvalidDimensions;
|
|
}
|
|
|
|
// Calculate bytes per pixel and row bytes
|
|
const bytes_per_pixel = format.getBytesPerPixel();
|
|
const bytes_per_row = width * bytes_per_pixel;
|
|
|
|
// Create the color space
|
|
const color_space = switch (format.getColorChannels()) {
|
|
1 => cf.CGColorSpaceCreateDeviceGray(),
|
|
3 => cf.CGColorSpaceCreateDeviceRGB(),
|
|
else => return error.UnsupportedColorSpace,
|
|
};
|
|
defer cf.CGColorSpaceRelease(color_space);
|
|
|
|
// Determine bitmap info based on pixel format
|
|
var bitmap_info: c_uint = 0;
|
|
|
|
switch (format) {
|
|
.RGB => bitmap_info = c.kCGImageAlphaNone | c.kCGBitmapByteOrderDefault,
|
|
.RGBA => bitmap_info = c.kCGImageAlphaPremultipliedLast | c.kCGBitmapByteOrderDefault,
|
|
.BGR => bitmap_info = c.kCGImageAlphaNone | c.kCGBitmapByteOrder32Little,
|
|
.BGRA => bitmap_info = c.kCGImageAlphaPremultipliedFirst | c.kCGBitmapByteOrder32Little,
|
|
.Gray => bitmap_info = c.kCGImageAlphaNone | c.kCGBitmapByteOrderDefault,
|
|
.GrayAlpha => bitmap_info = c.kCGImageAlphaPremultipliedLast | c.kCGBitmapByteOrderDefault,
|
|
.ARGB => bitmap_info = c.kCGImageAlphaPremultipliedFirst | c.kCGBitmapByteOrderDefault,
|
|
.ABGR => bitmap_info = c.kCGImageAlphaPremultipliedFirst | c.kCGBitmapByteOrder32Big,
|
|
}
|
|
|
|
// Create a data provider from our buffer
|
|
const data_provider = cf.CGDataProviderCreateWithData(
|
|
null, // Info parameter (unused)
|
|
source.ptr,
|
|
source.len,
|
|
null, // Release callback (we manage the memory ourselves)
|
|
);
|
|
defer cf.CGDataProviderRelease(data_provider);
|
|
|
|
// Create the CGImage
|
|
const cg_image = cf.CGImageCreate(
|
|
@as(usize, @intCast(width)),
|
|
@as(usize, @intCast(height)),
|
|
8, // Bits per component
|
|
8 * bytes_per_pixel, // Bits per pixel
|
|
bytes_per_row,
|
|
color_space,
|
|
bitmap_info,
|
|
data_provider,
|
|
null, // No decode array
|
|
false, // Should interpolate
|
|
c.kCGRenderingIntentDefault,
|
|
);
|
|
if (cg_image == null) {
|
|
return error.ImageCreationFailed;
|
|
}
|
|
defer cf.CGImageRelease(cg_image);
|
|
|
|
// Create a CFMutableData to hold the output
|
|
const data = cf.CFDataCreateMutable(null, 0);
|
|
if (data == null) {
|
|
return error.MemoryAllocationFailed;
|
|
}
|
|
defer cf.CFRelease(data);
|
|
|
|
// Create a CGImageDestination for the requested format
|
|
const type_id = getUTIForFormat(options.format);
|
|
if (type_id == null) return error.CFStringCreationFailed;
|
|
defer cf.CFRelease(type_id);
|
|
|
|
const destination = cf.CGImageDestinationCreateWithData(
|
|
data,
|
|
type_id,
|
|
1, // Number of images (just one)
|
|
null, // Options (none)
|
|
);
|
|
if (destination == null) {
|
|
return error.DestinationCreationFailed;
|
|
}
|
|
defer cf.CFRelease(destination);
|
|
|
|
// Create properties dictionary with quality setting
|
|
const properties = cf.CFDictionaryCreateMutable(
|
|
null,
|
|
0,
|
|
cf.kCFTypeDictionaryKeyCallBacks,
|
|
cf.kCFTypeDictionaryValueCallBacks,
|
|
);
|
|
defer cf.CFRelease(properties);
|
|
|
|
// Set compression quality
|
|
const quality_value = @as(f32, @floatFromInt(options.quality.quality)) / 100.0;
|
|
const quality_number = cf.CFNumberCreate(null, c.kCFNumberFloat32Type, &quality_value);
|
|
defer cf.CFRelease(quality_number);
|
|
cf.CFDictionarySetValue(properties, cf.kCGImageDestinationLossyCompressionQuality, quality_number);
|
|
|
|
// Add the image with properties
|
|
cf.CGImageDestinationAddImage(destination, cg_image, properties);
|
|
|
|
// Finalize the destination
|
|
if (!cf.CGImageDestinationFinalize(destination)) {
|
|
return error.EncodingFailed;
|
|
}
|
|
|
|
// Get the encoded data
|
|
const cf_data_len = cf.CFDataGetLength(data);
|
|
const cf_data_ptr = cf.CFDataGetBytePtr(data);
|
|
|
|
// Copy to a Zig-managed buffer
|
|
const output = try allocator.alloc(u8, @as(usize, @intCast(cf_data_len)));
|
|
@memcpy(output, cf_data_ptr[0..@as(usize, @intCast(cf_data_len))]);
|
|
|
|
return output;
|
|
}
|