From 1bbbd776ff420da4818f5a42d65a9fb854dd6738 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 23 Mar 2025 22:15:56 -0700 Subject: [PATCH] ok --- src/image/Image.classes.ts | 48 +-- src/image/Image.zig | 671 ----------------------------------- src/image/encoder_darwin.zig | 208 ++++++++--- src/image/streaming.zig | 207 ++++++----- 4 files changed, 275 insertions(+), 859 deletions(-) diff --git a/src/image/Image.classes.ts b/src/image/Image.classes.ts index 3a5fbee282..b4d29eec04 100644 --- a/src/image/Image.classes.ts +++ b/src/image/Image.classes.ts @@ -1,55 +1,53 @@ import { define } from "../codegen/class-definitions"; export default [ + // A *lazy* image. define({ name: "Image", construct: true, finalize: true, configurable: false, - klass: { - // Static factory method - image: { - fn: "image", - length: 1, - }, - }, + klass: {}, proto: { // Properties encoding: { getter: "getEncoding", cache: true, }, - width: { - getter: "getWidth", - cache: true, - }, - height: { - getter: "getHeight", - cache: true, - }, name: { value: "Image", }, - + // Methods - size: { + // + dimensions: { fn: "size", length: 0, }, + resize: { fn: "resize", length: 2, }, + + // Promise bytes: { - fn: "bytes", + fn: "bytes", length: 0, }, + // Promise blob: { fn: "blob", length: 0, }, - + // Promise + arrayBuffer: { + fn: "arrayBuffer", + length: 0, + }, + // Format conversion methods + // Each of these return a Promise jpg: { fn: "toJPEG", length: 1, @@ -74,16 +72,6 @@ export default [ fn: "toHEIC", length: 1, }, - - // Utility methods - ["toString"]: { - fn: "toString", - length: 0, - }, - ["toJSON"]: { - fn: "toJSON", - length: 0, - }, }, }), -]; \ No newline at end of file +]; diff --git a/src/image/Image.zig b/src/image/Image.zig index 0e1f2ac796..e69de29bb2 100644 --- a/src/image/Image.zig +++ b/src/image/Image.zig @@ -1,671 +0,0 @@ -const bun = @import("root").bun; -const std = @import("std"); -const JSC = bun.JSC; -const JSGlobalObject = JSC.JSGlobalObject; -const ZigString = JSC.ZigString; -const default_allocator = bun.default_allocator; -const encoder = @import("encoder.zig"); -const ImageFormat = encoder.ImageFormat; -const EncodingOptions = encoder.EncodingOptions; -const EncodingQuality = encoder.EncodingQuality; -const pixel_format = @import("pixel_format.zig"); -const PixelFormat = pixel_format.PixelFormat; -const Allocator = std.mem.Allocator; -const lanczos3 = @import("lanczos3.zig"); -const bicubic = @import("bicubic.zig"); -const bilinear = @import("bilinear.zig"); -const box = @import("box.zig"); - -pub const ImageScalingAlgorithm = enum { - lanczos3, - bicubic, - bilinear, - box, - - pub fn scale( - self: ImageScalingAlgorithm, - allocator: Allocator, - src: []const u8, - src_width: usize, - src_height: usize, - dest_width: usize, - dest_height: usize, - format: PixelFormat, - ) ![]u8 { - return switch (self) { - .lanczos3 => try lanczos3.scale(allocator, src, src_width, src_height, dest_width, dest_height, format), - .bicubic => try bicubic.scale(allocator, src, src_width, src_height, dest_width, dest_height, format), - .bilinear => try bilinear.scale(allocator, src, src_width, src_height, dest_width, dest_height, format), - .box => try box.scale(allocator, src, src_width, src_height, dest_width, dest_height, format), - }; - } -}; - -pub const ResizeOptions = struct { - x: ?i32 = null, - y: ?i32 = null, - width: usize, - height: usize, - quality: ?u8 = null, - algorithm: ImageScalingAlgorithm = .lanczos3, -}; - -// Image represents the main image object exposed to JavaScript -pub const Image = struct { - allocator: Allocator, - data: []u8, - width: usize, - height: usize, - format: PixelFormat, - encoding: ImageFormat, - - pub usingnamespace JSC.Codegen.JSImage; - - pub fn constructor(globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!*Image { - return globalThis.throw("Invalid constructor. Use Bun.image() instead", .{}); - } - - // Static factory method to create a new Image instance - pub fn image(globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const args = callFrame.arguments(1); - if (args.len == 0) { - return globalThis.throw("Missing data argument", .{}); - } - - // Support ArrayBuffer or TypedArray - const bytes = try args[0].toBytesUnsafe(globalThis); - if (bytes.len == 0) { - return globalThis.throw("Invalid image data", .{}); - } - - // Determine image format and dimensions - // Here you would typically parse the image header to get this information - // For simplicity, we'll create a stub that assumes it's a valid image - - // Create an Image instance - var image_obj = default_allocator.create(Image) catch { - return globalThis.throwOutOfMemoryError(); - }; - - image_obj.* = Image{ - .allocator = default_allocator, - .data = try default_allocator.dupe(u8, bytes), - .width = 0, // To be determined later - .height = 0, // To be determined later - .format = .RGBA, // Default format - .encoding = .JPEG, // Default encoding, to be determined - }; - - return image_obj.toJS(globalThis); - } - - // Get the current encoding format as a string - pub fn getEncoding(this: *Image, globalThis: *JSGlobalObject) JSC.JSValue { - const encoding_str = switch (this.encoding) { - .JPEG => "jpg", - .PNG => "png", - .WEBP => "webp", - .AVIF => "avif", - .TIFF => "tiff", - .HEIC => "heic", - }; - - return ZigString.init(encoding_str).toJS(globalThis); - } - - // Get the image width - pub fn getWidth(this: *Image, _: *JSGlobalObject) JSC.JSValue { - return JSC.JSValue.jsNumber(@intCast(this.width)); - } - - // Get the image height - pub fn getHeight(this: *Image, _: *JSGlobalObject) JSC.JSValue { - return JSC.JSValue.jsNumber(@intCast(this.height)); - } - - // Get the image dimensions asynchronously - pub fn size( - this: *Image, - globalThis: *JSGlobalObject, - _: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - var object = JSC.JSValue.createEmptyObject(globalThis, 2); - object.put(globalThis, ZigString.static("width"), JSC.JSValue.jsNumber(@intCast(this.width))); - object.put(globalThis, ZigString.static("height"), JSC.JSValue.jsNumber(@intCast(this.height))); - - // Create a resolved promise with the dimensions object - return JSC.JSPromise.createResolved(globalThis, object); - } - - // Resize the image with options - pub fn resize( - this: *Image, - globalThis: *JSGlobalObject, - callFrame: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - const args = callFrame.arguments(2); - if (args.len == 0) { - return globalThis.throw("Missing resize parameters", .{}); - } - - var options = ResizeOptions{ - .width = 0, - .height = 0, - }; - - // Process arguments: either (width, height, [quality]) or (options object) - if (args[0].isObject()) { - // Parse from options object - var options_obj = args[0].asObjectRef(); - - // Get width and height (required) - const width_val = options_obj.get(globalThis, ZigString.static("width")); - const height_val = options_obj.get(globalThis, ZigString.static("height")); - - if (!width_val.isNumber() or !height_val.isNumber()) { - return globalThis.throw("Width and height are required and must be numbers", .{}); - } - - options.width = @intFromFloat(width_val.asNumber()); - options.height = @intFromFloat(height_val.asNumber()); - - // Get optional parameters - const x_val = options_obj.get(globalThis, ZigString.static("x")); - if (x_val.isNumber()) { - options.x = @intFromFloat(x_val.asNumber()); - } - - const y_val = options_obj.get(globalThis, ZigString.static("y")); - if (y_val.isNumber()) { - options.y = @intFromFloat(y_val.asNumber()); - } - - const quality_val = options_obj.get(globalThis, ZigString.static("quality")); - if (quality_val.isNumber()) { - options.quality = @intFromFloat(quality_val.asNumber()); - } - } else if (args[0].isNumber() and args.len > 1 and args[1].isNumber()) { - // Parse from width, height arguments - options.width = @intFromFloat(args[0].asNumber()); - options.height = @intFromFloat(args[1].asNumber()); - - if (args.len > 2 and args[2].isNumber()) { - options.quality = @intFromFloat(args[2].asNumber()); - } - } else { - return globalThis.throw("Invalid resize parameters", .{}); - } - - // Validate dimensions - if (options.width == 0 or options.height == 0) { - return globalThis.throw("Width and height must be greater than 0", .{}); - } - - // Create a new Image instance with the resized data - var resized_image = default_allocator.create(Image) catch { - return globalThis.throwOutOfMemoryError(); - }; - - // Apply the resize operation - var scaled_data = options.algorithm.scale( - this.allocator, - this.data, - this.width, - this.height, - options.width, - options.height, - this.format, - ) catch |err| { - defer default_allocator.destroy(resized_image); - return globalThis.throw("Failed to resize image: {s}", .{@errorName(err)}); - }; - - // Update the image properties - resized_image.* = Image{ - .allocator = default_allocator, - .data = scaled_data, - .width = options.width, - .height = options.height, - .format = this.format, - .encoding = this.encoding, - }; - - return resized_image.toJS(globalThis); - } - - // Get the raw bytes of the image - pub fn bytes( - this: *Image, - globalThis: *JSGlobalObject, - _: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - const arrayBuffer = JSC.ArrayBuffer.create(globalThis, this.data.len) orelse - return globalThis.throwOutOfMemoryError(); - - var bytes_copy = arrayBuffer.slice(); - @memcpy(bytes_copy, this.data); - - // Create a resolved promise with the buffer - return JSC.JSPromise.createResolved(globalThis, arrayBuffer.toJS()); - } - - // Get the image as a blob - pub fn blob( - this: *Image, - globalThis: *JSGlobalObject, - _: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - // Create a blob with the image data - const mime_type = this.encoding.mimeType(); - const options = JSC.JSValue.createEmptyObject(globalThis, 1); - options.put(globalThis, ZigString.static("type"), ZigString.init(mime_type).toJS(globalThis)); - - const array = JSC.JSC_JSArray.create(globalThis, 1); - const arrayBuffer = JSC.ArrayBuffer.create(globalThis, this.data.len) orelse - return globalThis.throwOutOfMemoryError(); - - var bytes_copy = arrayBuffer.slice(); - @memcpy(bytes_copy, this.data); - - array.setIndex(globalThis, 0, arrayBuffer.toJS()); - - const blob = try globalThis.blobFrom(array.toJS(), options); - - // Create a resolved promise with the blob - return JSC.JSPromise.createResolved(globalThis, blob); - } - - // Convert to JPEG format - pub fn toJPEG( - this: *Image, - globalThis: *JSGlobalObject, - callFrame: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - const args = callFrame.arguments(1); - var quality: u8 = 80; // Default quality - - if (args.len > 0 and args[0].isObject()) { - var options_obj = args[0].asObjectRef(); - const quality_val = options_obj.get(globalThis, ZigString.static("quality")); - if (quality_val.isNumber()) { - quality = @intFromFloat(quality_val.asNumber()); - } - } - - // Clamp quality to 0-100 - quality = @min(@max(quality, 0), 100); - - if (this.encoding == .JPEG) { - // If already JPEG, just update quality if needed - if (quality == 80) { - return this.toJS(globalThis); // Return this if no change - } - } - - // Create a new Image instance for the converted format - var jpeg_image = default_allocator.create(Image) catch { - return globalThis.throwOutOfMemoryError(); - }; - - // Convert to JPEG - var jpeg_data: []u8 = undefined; - - if (this.encoding == .JPEG) { - // If already JPEG, just make a copy - jpeg_data = try this.allocator.dupe(u8, this.data); - } else { - // Convert from other format to JPEG - const encoding_options = EncodingOptions{ - .format = .JPEG, - .quality = .{ .quality = quality }, - }; - - // Try to transcode directly if possible - jpeg_data = encoder.transcode( - this.allocator, - this.data, - this.encoding, - .JPEG, - encoding_options, - ) catch |err| { - defer default_allocator.destroy(jpeg_image); - return globalThis.throw("Failed to convert to JPEG: {s}", .{@errorName(err)}); - }; - } - - // Update the image properties - jpeg_image.* = Image{ - .allocator = default_allocator, - .data = jpeg_data, - .width = this.width, - .height = this.height, - .format = this.format, - .encoding = .JPEG, - }; - - return jpeg_image.toJS(globalThis); - } - - // Convert to PNG format - pub fn toPNG( - this: *Image, - globalThis: *JSGlobalObject, - callFrame: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - // PNG doesn't use quality, but we'll parse options for consistency - _ = callFrame.arguments(1); - - if (this.encoding == .PNG) { - return this.toJS(globalThis); // Return this if already PNG - } - - // Create a new Image instance for the converted format - var png_image = default_allocator.create(Image) catch { - return globalThis.throwOutOfMemoryError(); - }; - - // Convert to PNG - const encoding_options = EncodingOptions{ - .format = .PNG, - }; - - // Try to transcode directly if possible - var png_data = encoder.transcode( - this.allocator, - this.data, - this.encoding, - .PNG, - encoding_options, - ) catch |err| { - defer default_allocator.destroy(png_image); - return globalThis.throw("Failed to convert to PNG: {s}", .{@errorName(err)}); - }; - - // Update the image properties - png_image.* = Image{ - .allocator = default_allocator, - .data = png_data, - .width = this.width, - .height = this.height, - .format = this.format, - .encoding = .PNG, - }; - - return png_image.toJS(globalThis); - } - - // Convert to WebP format - pub fn toWEBP( - this: *Image, - globalThis: *JSGlobalObject, - callFrame: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - const args = callFrame.arguments(1); - var quality: u8 = 80; // Default quality - - if (args.len > 0 and args[0].isObject()) { - var options_obj = args[0].asObjectRef(); - const quality_val = options_obj.get(globalThis, ZigString.static("quality")); - if (quality_val.isNumber()) { - quality = @intFromFloat(quality_val.asNumber()); - } - } - - // Clamp quality to 0-100 - quality = @min(@max(quality, 0), 100); - - if (this.encoding == .WEBP) { - // If already WebP, just update quality if needed - if (quality == 80) { - return this.toJS(globalThis); // Return this if no change - } - } - - // Create a new Image instance for the converted format - var webp_image = default_allocator.create(Image) catch { - return globalThis.throwOutOfMemoryError(); - }; - - // Convert to WebP - const encoding_options = EncodingOptions{ - .format = .WEBP, - .quality = .{ .quality = quality }, - }; - - // Try to transcode directly if possible - var webp_data = encoder.transcode( - this.allocator, - this.data, - this.encoding, - .WEBP, - encoding_options, - ) catch |err| { - defer default_allocator.destroy(webp_image); - return globalThis.throw("Failed to convert to WebP: {s}", .{@errorName(err)}); - }; - - // Update the image properties - webp_image.* = Image{ - .allocator = default_allocator, - .data = webp_data, - .width = this.width, - .height = this.height, - .format = this.format, - .encoding = .WEBP, - }; - - return webp_image.toJS(globalThis); - } - - // Convert to AVIF format - pub fn toAVIF( - this: *Image, - globalThis: *JSGlobalObject, - callFrame: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - const args = callFrame.arguments(1); - var quality: u8 = 80; // Default quality - - if (args.len > 0 and args[0].isObject()) { - var options_obj = args[0].asObjectRef(); - const quality_val = options_obj.get(globalThis, ZigString.static("quality")); - if (quality_val.isNumber()) { - quality = @intFromFloat(quality_val.asNumber()); - } - } - - // Clamp quality to 0-100 - quality = @min(@max(quality, 0), 100); - - if (this.encoding == .AVIF) { - // If already AVIF, just update quality if needed - if (quality == 80) { - return this.toJS(globalThis); // Return this if no change - } - } - - // Create a new Image instance for the converted format - var avif_image = default_allocator.create(Image) catch { - return globalThis.throwOutOfMemoryError(); - }; - - // Convert to AVIF - const encoding_options = EncodingOptions{ - .format = .AVIF, - .quality = .{ .quality = quality }, - }; - - // Try to transcode directly if possible - var avif_data = encoder.transcode( - this.allocator, - this.data, - this.encoding, - .AVIF, - encoding_options, - ) catch |err| { - defer default_allocator.destroy(avif_image); - return globalThis.throw("Failed to convert to AVIF: {s}", .{@errorName(err)}); - }; - - // Update the image properties - avif_image.* = Image{ - .allocator = default_allocator, - .data = avif_data, - .width = this.width, - .height = this.height, - .format = this.format, - .encoding = .AVIF, - }; - - return avif_image.toJS(globalThis); - } - - // Convert to TIFF format - pub fn toTIFF( - this: *Image, - globalThis: *JSGlobalObject, - callFrame: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - // TIFF doesn't typically use quality, but we'll parse options for consistency - _ = callFrame.arguments(1); - - if (this.encoding == .TIFF) { - return this.toJS(globalThis); // Return this if already TIFF - } - - // Create a new Image instance for the converted format - var tiff_image = default_allocator.create(Image) catch { - return globalThis.throwOutOfMemoryError(); - }; - - // Convert to TIFF - const encoding_options = EncodingOptions{ - .format = .TIFF, - }; - - // Try to transcode directly if possible - var tiff_data = encoder.transcode( - this.allocator, - this.data, - this.encoding, - .TIFF, - encoding_options, - ) catch |err| { - defer default_allocator.destroy(tiff_image); - return globalThis.throw("Failed to convert to TIFF: {s}", .{@errorName(err)}); - }; - - // Update the image properties - tiff_image.* = Image{ - .allocator = default_allocator, - .data = tiff_data, - .width = this.width, - .height = this.height, - .format = this.format, - .encoding = .TIFF, - }; - - return tiff_image.toJS(globalThis); - } - - // Convert to HEIC format - pub fn toHEIC( - this: *Image, - globalThis: *JSGlobalObject, - callFrame: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - const args = callFrame.arguments(1); - var quality: u8 = 80; // Default quality - - if (args.len > 0 and args[0].isObject()) { - var options_obj = args[0].asObjectRef(); - const quality_val = options_obj.get(globalThis, ZigString.static("quality")); - if (quality_val.isNumber()) { - quality = @intFromFloat(quality_val.asNumber()); - } - } - - // Clamp quality to 0-100 - quality = @min(@max(quality, 0), 100); - - if (this.encoding == .HEIC) { - // If already HEIC, just update quality if needed - if (quality == 80) { - return this.toJS(globalThis); // Return this if no change - } - } - - // Create a new Image instance for the converted format - var heic_image = default_allocator.create(Image) catch { - return globalThis.throwOutOfMemoryError(); - }; - - // Convert to HEIC - const encoding_options = EncodingOptions{ - .format = .HEIC, - .quality = .{ .quality = quality }, - }; - - // Try to transcode directly if possible - var heic_data = encoder.transcode( - this.allocator, - this.data, - this.encoding, - .HEIC, - encoding_options, - ) catch |err| { - defer default_allocator.destroy(heic_image); - return globalThis.throw("Failed to convert to HEIC: {s}", .{@errorName(err)}); - }; - - // Update the image properties - heic_image.* = Image{ - .allocator = default_allocator, - .data = heic_data, - .width = this.width, - .height = this.height, - .format = this.format, - .encoding = .HEIC, - }; - - return heic_image.toJS(globalThis); - } - - // String representation of the image - pub fn toString( - this: *Image, - globalThis: *JSGlobalObject, - _: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - const description = std.fmt.allocPrint( - this.allocator, - "[object Image {{width: {d}, height: {d}, encoding: {s}}}]", - .{ this.width, this.height, @tagName(this.encoding) }, - ) catch { - return globalThis.throwOutOfMemoryError(); - }; - defer this.allocator.free(description); - - return ZigString.init(description).toJS(globalThis); - } - - // JSON representation of the image - pub fn toJSON( - this: *Image, - globalThis: *JSGlobalObject, - _: *JSC.CallFrame, - ) bun.JSError!JSC.JSValue { - var object = JSC.JSValue.createEmptyObject(globalThis, 3); - object.put(globalThis, ZigString.static("width"), JSC.JSValue.jsNumber(@intCast(this.width))); - object.put(globalThis, ZigString.static("height"), JSC.JSValue.jsNumber(@intCast(this.height))); - object.put(globalThis, ZigString.static("encoding"), this.getEncoding(globalThis)); - - return object; - } - - // Clean up resources - pub fn finalize(this: *Image) callconv(.C) void { - this.allocator.free(this.data); - this.allocator.destroy(this); - } -}; \ No newline at end of file diff --git a/src/image/encoder_darwin.zig b/src/image/encoder_darwin.zig index 4e8df95651..b32019a2a0 100644 --- a/src/image/encoder_darwin.zig +++ b/src/image/encoder_darwin.zig @@ -4,16 +4,111 @@ const PixelFormat = pixel_format.PixelFormat; const EncodingOptions = @import("encoder.zig").EncodingOptions; const ImageFormat = @import("encoder.zig").ImageFormat; -// Import the required macOS frameworks +// 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 c.CFStringCreateWithBytes( + return cf.CFStringCreateWithBytes( null, str.ptr, @as(c_long, @intCast(str.len)), @@ -43,44 +138,49 @@ pub fn transcode( 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 = c.CGDataProviderCreateWithData( + const data_provider = cf.CGDataProviderCreateWithData( null, // Info parameter (unused) source_data.ptr, source_data.len, null, // Release callback (we manage the memory ourselves) ); - defer c.CGDataProviderRelease(data_provider); + defer cf.CGDataProviderRelease(data_provider); // Create an image source from the data provider const source_type_id = getUTIForFormat(source_format); - defer c.CFRelease(source_type_id); - - const image_source = c.CGImageSourceCreateWithDataProvider(data_provider, null); + 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 c.CFRelease(image_source); + defer cf.CFRelease(image_source); // Get the image from the source - const cg_image = c.CGImageSourceCreateImageAtIndex(image_source, 0, null); + const cg_image = cf.CGImageSourceCreateImageAtIndex(image_source, 0, null); if (cg_image == null) { return error.ImageCreationFailed; } - defer c.CGImageRelease(cg_image); + defer cf.CGImageRelease(cg_image); // Create a mutable data object to hold the output - const data = c.CFDataCreateMutable(null, 0); + const data = cf.CFDataCreateMutable(null, 0); if (data == null) { return error.MemoryAllocationFailed; } - defer c.CFRelease(data); + defer cf.CFRelease(data); // Create a CGImageDestination for the requested format const type_id = getUTIForFormat(target_format); - defer c.CFRelease(type_id); - - const destination = c.CGImageDestinationCreateWithData( + 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) @@ -89,34 +189,34 @@ pub fn transcode( if (destination == null) { return error.DestinationCreationFailed; } - defer c.CFRelease(destination); + defer cf.CFRelease(destination); // Create properties dictionary with quality setting - const properties = c.CFDictionaryCreateMutable( + const properties = cf.CFDictionaryCreateMutable( null, 0, - &c.kCFTypeDictionaryKeyCallBacks, - &c.kCFTypeDictionaryValueCallBacks, + cf.kCFTypeDictionaryKeyCallBacks, + cf.kCFTypeDictionaryValueCallBacks, ); - defer c.CFRelease(properties); + defer cf.CFRelease(properties); // Set compression quality const quality_value = @as(f32, @floatFromInt(options.quality.quality)) / 100.0; - const quality_number = c.CFNumberCreate(null, c.kCFNumberFloat32Type, &quality_value); - defer c.CFRelease(quality_number); - c.CFDictionarySetValue(properties, c.kCGImageDestinationLossyCompressionQuality, quality_number); + 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 - c.CGImageDestinationAddImage(destination, cg_image, properties); + cf.CGImageDestinationAddImage(destination, cg_image, properties); // Finalize the destination - if (!c.CGImageDestinationFinalize(destination)) { + if (!cf.CGImageDestinationFinalize(destination)) { return error.EncodingFailed; } // Get the encoded data - const cf_data_len = c.CFDataGetLength(data); - const cf_data_ptr = c.CFDataGetBytePtr(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))); @@ -134,6 +234,9 @@ pub fn encode( 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; @@ -145,15 +248,15 @@ pub fn encode( // Create the color space const color_space = switch (format.getColorChannels()) { - 1 => c.CGColorSpaceCreateDeviceGray(), - 3 => c.CGColorSpaceCreateDeviceRGB(), + 1 => cf.CGColorSpaceCreateDeviceGray(), + 3 => cf.CGColorSpaceCreateDeviceRGB(), else => return error.UnsupportedColorSpace, }; - defer c.CGColorSpaceRelease(color_space); + 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, @@ -166,16 +269,16 @@ pub fn encode( } // Create a data provider from our buffer - const data_provider = c.CGDataProviderCreateWithData( + const data_provider = cf.CGDataProviderCreateWithData( null, // Info parameter (unused) source.ptr, source.len, null, // Release callback (we manage the memory ourselves) ); - defer c.CGDataProviderRelease(data_provider); + defer cf.CGDataProviderRelease(data_provider); // Create the CGImage - const cg_image = c.CGImageCreate( + const cg_image = cf.CGImageCreate( @as(usize, @intCast(width)), @as(usize, @intCast(height)), 8, // Bits per component @@ -191,18 +294,21 @@ pub fn encode( if (cg_image == null) { return error.ImageCreationFailed; } - defer c.CGImageRelease(cg_image); + defer cf.CGImageRelease(cg_image); // Create a CFMutableData to hold the output - const data = c.CFDataCreateMutable(null, 0); + const data = cf.CFDataCreateMutable(null, 0); if (data == null) { return error.MemoryAllocationFailed; } - defer c.CFRelease(data); + defer cf.CFRelease(data); // Create a CGImageDestination for the requested format const type_id = getUTIForFormat(options.format); - const destination = c.CGImageDestinationCreateWithData( + 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) @@ -211,38 +317,38 @@ pub fn encode( if (destination == null) { return error.DestinationCreationFailed; } - defer c.CFRelease(destination); + defer cf.CFRelease(destination); // Create properties dictionary with quality setting - const properties = c.CFDictionaryCreateMutable( + const properties = cf.CFDictionaryCreateMutable( null, 0, - &c.kCFTypeDictionaryKeyCallBacks, - &c.kCFTypeDictionaryValueCallBacks, + cf.kCFTypeDictionaryKeyCallBacks, + cf.kCFTypeDictionaryValueCallBacks, ); - defer c.CFRelease(properties); + defer cf.CFRelease(properties); // Set compression quality const quality_value = @as(f32, @floatFromInt(options.quality.quality)) / 100.0; - const quality_number = c.CFNumberCreate(null, c.kCFNumberFloat32Type, &quality_value); - defer c.CFRelease(quality_number); - c.CFDictionarySetValue(properties, c.kCGImageDestinationLossyCompressionQuality, quality_number); + 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 - c.CGImageDestinationAddImage(destination, cg_image, properties); + cf.CGImageDestinationAddImage(destination, cg_image, properties); // Finalize the destination - if (!c.CGImageDestinationFinalize(destination)) { + if (!cf.CGImageDestinationFinalize(destination)) { return error.EncodingFailed; } // Get the encoded data - const cf_data_len = c.CFDataGetLength(data); - const cf_data_ptr = c.CFDataGetBytePtr(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; -} \ No newline at end of file +} diff --git a/src/image/streaming.zig b/src/image/streaming.zig index ded4fba9f2..edb0c753f9 100644 --- a/src/image/streaming.zig +++ b/src/image/streaming.zig @@ -11,31 +11,31 @@ const bilinear = @import("bilinear.zig"); pub const ImageChunk = struct { /// Raw pixel data data: []u8, - + /// Starting row in the image start_row: usize, - + /// Number of rows in this chunk rows: usize, - + /// Image width (pixels per row) width: usize, - + /// Pixel format format: PixelFormat, - + /// Whether this is the last chunk is_last: bool, - + /// Allocator used for this chunk allocator: std.mem.Allocator, - + /// Free the chunk's data pub fn deinit(self: *ImageChunk) void { self.allocator.free(self.data); - self.* = undefined; + self.allocator.destroy(self); } - + /// Create a new chunk pub fn init( allocator: std.mem.Allocator, @@ -48,7 +48,7 @@ pub const ImageChunk = struct { const bytes_per_pixel = format.getBytesPerPixel(); const data_size = width * rows * bytes_per_pixel; const data = try allocator.alloc(u8, data_size); - + return ImageChunk{ .data = data, .start_row = start_row, @@ -59,13 +59,13 @@ pub const ImageChunk = struct { .allocator = allocator, }; } - + /// Calculate byte offset for a specific pixel pub fn pixelOffset(self: ImageChunk, x: usize, y: usize) usize { const bytes_per_pixel = self.format.getBytesPerPixel(); return ((y - self.start_row) * self.width + x) * bytes_per_pixel; } - + /// Get row size in bytes pub fn rowSize(self: ImageChunk) usize { return self.width * self.format.getBytesPerPixel(); @@ -76,15 +76,15 @@ pub const ImageChunk = struct { pub const StreamProcessor = struct { /// Process a chunk of image data processChunkFn: *const fn (self: *StreamProcessor, chunk: *ImageChunk) anyerror!void, - + /// Finalize processing and return result finalizeFn: *const fn (self: *StreamProcessor) anyerror![]u8, - + /// Process a chunk of image data pub fn processChunk(self: *StreamProcessor, chunk: *ImageChunk) !void { return self.processChunkFn(self, chunk); } - + /// Finalize processing and return result pub fn finalize(self: *StreamProcessor) ![]u8 { return self.finalizeFn(self); @@ -95,25 +95,25 @@ pub const StreamProcessor = struct { pub const StreamingEncoder = struct { /// Common interface processor: StreamProcessor, - + /// Allocator for internal storage allocator: std.mem.Allocator, - + /// Target image format options: EncodingOptions, - + /// Total image width width: usize, - + /// Total image height height: usize, - + /// Pixel format format: PixelFormat, - + /// Temporary storage for accumulated chunks buffer: std.ArrayList(u8), - + /// Number of rows received so far rows_processed: usize, @@ -126,7 +126,7 @@ pub const StreamingEncoder = struct { options: EncodingOptions, ) !*StreamingEncoder { var self = try allocator.create(StreamingEncoder); - + self.* = StreamingEncoder{ .processor = StreamProcessor{ .processChunkFn = processChunk, @@ -140,64 +140,57 @@ pub const StreamingEncoder = struct { .buffer = std.ArrayList(u8).init(allocator), .rows_processed = 0, }; - + // Pre-allocate buffer with estimated size const bytes_per_pixel = format.getBytesPerPixel(); const estimated_size = width * height * bytes_per_pixel; try self.buffer.ensureTotalCapacity(estimated_size); - + return self; } - + /// Free resources pub fn deinit(self: *StreamingEncoder) void { self.buffer.deinit(); self.allocator.destroy(self); } - + /// Process a chunk of image data fn processChunk(processor: *StreamProcessor, chunk: *ImageChunk) !void { const self: *StreamingEncoder = @ptrCast(@alignCast(processor)); - + // Validate chunk if (chunk.width != self.width) { return error.ChunkWidthMismatch; } - + if (chunk.start_row != self.rows_processed) { return error.ChunkOutOfOrder; } - + if (chunk.format != self.format) { return error.ChunkFormatMismatch; } - + // Append chunk data to buffer try self.buffer.appendSlice(chunk.data); - + // Update rows processed self.rows_processed += chunk.rows; } - + /// Finalize encoding and return compressed image data fn finalize(processor: *StreamProcessor) ![]u8 { const self: *StreamingEncoder = @ptrCast(@alignCast(processor)); - + // Verify we received all rows if (self.rows_processed != self.height) { return error.IncompleteImage; } - + // Encode the accumulated image data - const result = try encoder.encode( - self.allocator, - self.buffer.items, - self.width, - self.height, - self.format, - self.options - ); - + const result = try encoder.encode(self.allocator, self.buffer.items, self.width, self.height, self.format, self.options); + return result; } }; @@ -206,37 +199,37 @@ pub const StreamingEncoder = struct { pub const StreamingResizer = struct { /// Common interface processor: StreamProcessor, - + /// Allocator for internal storage allocator: std.mem.Allocator, - + /// Original image width src_width: usize, - + /// Original image height src_height: usize, - + /// Target image width dest_width: usize, - + /// Target image height dest_height: usize, - + /// Pixel format format: PixelFormat, - + /// Temporary buffer for source image source_buffer: std.ArrayList(u8), - + /// Number of source rows received rows_processed: usize, - + /// Next processor in the pipeline next_processor: ?*StreamProcessor, - + /// Algorithm to use for resizing algorithm: ResizeAlgorithm, - + /// Create a new streaming resizer pub fn init( allocator: std.mem.Allocator, @@ -249,7 +242,7 @@ pub const StreamingResizer = struct { next_processor: ?*StreamProcessor, ) !*StreamingResizer { var self = try allocator.create(StreamingResizer); - + self.* = StreamingResizer{ .processor = StreamProcessor{ .processChunkFn = processChunk, @@ -266,82 +259,82 @@ pub const StreamingResizer = struct { .next_processor = next_processor, .algorithm = algorithm, }; - + // Pre-allocate the source buffer const bytes_per_pixel = format.getBytesPerPixel(); const estimated_size = src_width * src_height * bytes_per_pixel; try self.source_buffer.ensureTotalCapacity(estimated_size); - + return self; } - + /// Free resources pub fn deinit(self: *StreamingResizer) void { self.source_buffer.deinit(); self.allocator.destroy(self); } - + /// Process a chunk of image data fn processChunk(processor: *StreamProcessor, chunk: *ImageChunk) !void { const self: *StreamingResizer = @ptrCast(@alignCast(processor)); - + // Validate chunk if (chunk.width != self.src_width) { return error.ChunkWidthMismatch; } - + if (chunk.start_row != self.rows_processed) { return error.ChunkOutOfOrder; } - + if (chunk.format != self.format) { return error.ChunkFormatMismatch; } - + // Append chunk data to buffer try self.source_buffer.appendSlice(chunk.data); - + // Update rows processed self.rows_processed += chunk.rows; - + // If we have enough rows or this is the last chunk, process a batch const min_rows_needed = calculateMinRowsNeeded(self.algorithm, self.src_height, self.dest_height); const can_process = self.rows_processed >= min_rows_needed or chunk.is_last; - + if (can_process and self.next_processor != null) { try self.processAvailableRows(); } } - + /// Calculate how many source rows we need to produce a destination row fn calculateMinRowsNeeded(algorithm: ResizeAlgorithm, src_height: usize, dest_height: usize) usize { _ = dest_height; return switch (algorithm) { .Lanczos3 => @min(src_height, 6), // Lanczos3 kernel is 6 pixels wide .Bilinear => @min(src_height, 2), // Bilinear needs 2 rows - .Bicubic => @min(src_height, 4), // Bicubic needs 4 rows - .Box => @min(src_height, 1), // Box/nearest neighbor needs 1 row + .Bicubic => @min(src_height, 4), // Bicubic needs 4 rows + .Box => @min(src_height, 1), // Box/nearest neighbor needs 1 row }; } - + /// Process available rows into resized chunks fn processAvailableRows(self: *StreamingResizer) !void { if (self.next_processor == null) return; - + // Calculate how many destination rows we can produce const src_rows = self.rows_processed; const total_dest_rows = self.dest_height; const dest_rows_possible = calculateDestRows(src_rows, self.src_height, total_dest_rows); - + if (dest_rows_possible == 0) return; - + // Create a chunk with the resized data // Calculate the destination row based on the ratio of processed source rows - const dest_row_start = if (dest_rows_possible > 0) + const dest_row_start = if (dest_rows_possible > 0) calculateDestRows(self.rows_processed - dest_rows_possible, self.src_height, self.dest_height) - else + else 0; - + var dest_chunk = try ImageChunk.init( self.allocator, self.dest_width, @@ -351,37 +344,37 @@ pub const StreamingResizer = struct { self.rows_processed == self.src_height, // Is last if we've processed all source rows ); defer dest_chunk.deinit(); - + // Perform the actual resize var mutable_dest_chunk = dest_chunk; try self.resizeChunk(&mutable_dest_chunk); - + // Pass to the next processor var mutable_chunk = dest_chunk; try self.next_processor.?.processChunk(&mutable_chunk); } - + /// Calculate how many destination rows we can produce from a given number of source rows fn calculateDestRows(src_rows: usize, src_height: usize, dest_height: usize) usize { const ratio = @as(f32, @floatFromInt(src_rows)) / @as(f32, @floatFromInt(src_height)); const dest_rows = @as(usize, @intFromFloat(ratio * @as(f32, @floatFromInt(dest_height)))); return dest_rows; } - + /// Resize the accumulated source rows to fill a destination chunk fn resizeChunk(self: *StreamingResizer, dest_chunk: *ImageChunk) !void { const bytes_per_pixel = self.format.getBytesPerPixel(); - + // Source data const src_data = self.source_buffer.items; const src_width = self.src_width; const src_height = self.rows_processed; // Use only rows we've received - + // Destination info const dest_data = dest_chunk.data; const dest_width = self.dest_width; const dest_rows = dest_chunk.rows; - + // Perform resize based on selected algorithm switch (self.algorithm) { .Lanczos3 => { @@ -437,27 +430,27 @@ pub const StreamingResizer = struct { }, } } - + /// Finalize resizing and pass to next processor fn finalize(processor: *StreamProcessor) ![]u8 { const self: *StreamingResizer = @ptrCast(@alignCast(processor)); - + // Verify we received all rows if (self.rows_processed != self.src_height) { return error.IncompleteImage; } - + // If we have a next processor, finalize it if (self.next_processor) |next| { return try next.finalize(); } - + // If no next processor, resize the complete image and return the result const bytes_per_pixel = self.format.getBytesPerPixel(); const dest_buffer_size = self.dest_width * self.dest_height * bytes_per_pixel; const dest_buffer = try self.allocator.alloc(u8, dest_buffer_size); errdefer self.allocator.free(dest_buffer); - + switch (self.algorithm) { .Lanczos3 => { _ = try lanczos3.Lanczos3.resize( @@ -506,7 +499,7 @@ pub const StreamingResizer = struct { ); }, } - + return dest_buffer; } }; @@ -524,7 +517,7 @@ pub const ImagePipeline = struct { allocator: std.mem.Allocator, first_processor: *StreamProcessor, last_processor: *StreamProcessor, - + /// Initialize a pipeline with a first processor pub fn init(allocator: std.mem.Allocator, first: *StreamProcessor) ImagePipeline { return .{ @@ -533,7 +526,7 @@ pub const ImagePipeline = struct { .last_processor = first, }; } - + /// Add a processor to the pipeline pub fn addProcessor(self: *ImagePipeline, processor: *StreamProcessor) void { // Connect the new processor to the pipeline @@ -546,16 +539,16 @@ pub const ImagePipeline = struct { resizer.next_processor = processor; } } - + // Update the last processor self.last_processor = processor; } - + /// Process a chunk of image data pub fn processChunk(self: *ImagePipeline, chunk: *ImageChunk) !void { return self.first_processor.processChunk(chunk); } - + /// Finalize the pipeline and get the result pub fn finalize(self: *ImagePipeline) ![]u8 { return self.first_processor.finalize(); @@ -571,7 +564,7 @@ pub const ChunkIterator = struct { format: PixelFormat, rows_per_chunk: usize, current_row: usize, - + pub fn init( allocator: std.mem.Allocator, data: []const u8, @@ -590,19 +583,19 @@ pub const ChunkIterator = struct { .current_row = 0, }; } - + /// Get the next chunk, or null if done pub fn next(self: *ChunkIterator) !?ImageChunk { if (self.current_row >= self.height) return null; - + const bytes_per_pixel = self.format.getBytesPerPixel(); const bytes_per_row = self.width * bytes_per_pixel; - + // Calculate how many rows to include in this chunk const rows_remaining = self.height - self.current_row; const rows_in_chunk = @min(self.rows_per_chunk, rows_remaining); const is_last = rows_in_chunk == rows_remaining; - + // Create the chunk const chunk = try ImageChunk.init( self.allocator, @@ -612,15 +605,15 @@ pub const ChunkIterator = struct { self.format, is_last, ); - + // Copy the data const start_offset = self.current_row * bytes_per_row; const end_offset = start_offset + (rows_in_chunk * bytes_per_row); @memcpy(chunk.data, self.data[start_offset..end_offset]); - + // Advance to the next row self.current_row += rows_in_chunk; - + return chunk; } }; @@ -630,7 +623,7 @@ pub fn createResizeEncodePipeline( allocator: std.mem.Allocator, src_width: usize, src_height: usize, - dest_width: usize, + dest_width: usize, dest_height: usize, format: PixelFormat, resize_algorithm: ResizeAlgorithm, @@ -644,7 +637,7 @@ pub fn createResizeEncodePipeline( format, encode_options, ); - + // Create the resizer, connecting to the encoder var resizer_instance = try StreamingResizer.init( allocator, @@ -656,7 +649,7 @@ pub fn createResizeEncodePipeline( resize_algorithm, &encoder_instance.processor, ); - + // Create and return the pipeline return ImagePipeline.init(allocator, &resizer_instance.processor); -} \ No newline at end of file +}