This commit is contained in:
Jarred Sumner
2025-03-23 22:15:56 -07:00
parent 18ae76bbd7
commit 1bbbd776ff
4 changed files with 275 additions and 859 deletions

View File

@@ -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<Uint8Array>
bytes: {
fn: "bytes",
fn: "bytes",
length: 0,
},
// Promise<Blob>
blob: {
fn: "blob",
length: 0,
},
// Promise<ArrayBuffer>
arrayBuffer: {
fn: "arrayBuffer",
length: 0,
},
// Format conversion methods
// Each of these return a Promise<Image>
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,
},
},
}),
];
];

View File

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

View File

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

View File

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