mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
ok
This commit is contained in:
@@ -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,
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user