97 lines
3.8 KiB
JavaScript
97 lines
3.8 KiB
JavaScript
import { Worker } from "node:worker_threads";
|
|
import { resizeImageInProcess } from "./image-resize-core.js";
|
|
function toTransferableBytes(input) {
|
|
// Transfer detaches the buffer, so transfer a worker-owned copy and leave the
|
|
// caller's bytes intact.
|
|
return new Uint8Array(input);
|
|
}
|
|
function isResizeImageWorkerResponse(value) {
|
|
return value !== null && typeof value === "object";
|
|
}
|
|
function createResizeWorker(workerSpecifier) {
|
|
return new Worker(workerSpecifier);
|
|
}
|
|
async function resizeImageInWorker(workerSpecifier, inputBytes, mimeType, options) {
|
|
const worker = createResizeWorker(workerSpecifier);
|
|
try {
|
|
const inputBytesForWorker = toTransferableBytes(inputBytes);
|
|
return await new Promise((resolve, reject) => {
|
|
let settled = false;
|
|
const settle = (result) => {
|
|
if (settled)
|
|
return;
|
|
settled = true;
|
|
resolve(result);
|
|
};
|
|
const fail = (error) => {
|
|
if (settled)
|
|
return;
|
|
settled = true;
|
|
reject(error);
|
|
};
|
|
worker.once("message", (message) => {
|
|
if (!isResizeImageWorkerResponse(message)) {
|
|
fail(new Error("Invalid image resize worker response"));
|
|
return;
|
|
}
|
|
if (message.error) {
|
|
fail(new Error(message.error));
|
|
return;
|
|
}
|
|
settle(message.result ?? null);
|
|
});
|
|
worker.once("error", fail);
|
|
worker.once("exit", (code) => {
|
|
if (!settled) {
|
|
fail(new Error(`Image resize worker exited with code ${code}`));
|
|
}
|
|
});
|
|
worker.postMessage({
|
|
inputBytes: inputBytesForWorker,
|
|
mimeType,
|
|
options,
|
|
}, [inputBytesForWorker.buffer]);
|
|
});
|
|
}
|
|
finally {
|
|
void worker.terminate().catch(() => undefined);
|
|
}
|
|
}
|
|
/**
|
|
* Resize an image to fit within the specified max dimensions and encoded file size.
|
|
* Runs Photon in a worker thread so WASM decoding, resizing, and encoding do not
|
|
* block the TUI event loop. If the worker cannot be loaded (for example in some
|
|
* Bun compiled executable layouts), fall back to in-process resizing so image
|
|
* reads still work.
|
|
*/
|
|
export async function resizeImage(inputBytes, mimeType, options) {
|
|
const isTypeScriptRuntime = import.meta.url.endsWith(".ts");
|
|
const workerUrl = new URL(isTypeScriptRuntime ? "./image-resize-worker.ts" : "./image-resize-worker.js", import.meta.url);
|
|
// Bun compiled executables resolve worker entrypoints by string path, not via
|
|
// new URL(..., import.meta.url). Try the string path first under Bun so the
|
|
// release binary uses the embedded worker instead of falling back in-process.
|
|
if (typeof process.versions.bun === "string") {
|
|
try {
|
|
return await resizeImageInWorker("./src/utils/image-resize-worker.ts", inputBytes, mimeType, options);
|
|
}
|
|
catch { }
|
|
}
|
|
try {
|
|
return await resizeImageInWorker(workerUrl, inputBytes, mimeType, options);
|
|
}
|
|
catch {
|
|
return resizeImageInProcess(inputBytes, mimeType, options);
|
|
}
|
|
}
|
|
/**
|
|
* Format a dimension note for resized images.
|
|
* This helps the model understand the coordinate mapping.
|
|
*/
|
|
export function formatDimensionNote(result) {
|
|
if (!result.wasResized) {
|
|
return undefined;
|
|
}
|
|
const scale = result.originalWidth / result.width;
|
|
return `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${result.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;
|
|
}
|
|
//# sourceMappingURL=image-resize.js.map
|