integratedhub/static/heic-to.min.js
2026-01-20 09:02:26 -06:00

291 lines
7.7 KiB
JavaScript

/**
* HEIC Image Converter - Refactored with improved error handling and ES6 modules
*/
// Error handling utility
class HeicConversionError extends Error {
constructor(message, context, originalError = null) {
super(`HEIC Conversion Error [${context}]: ${message}`);
this.name = "HeicConversionError";
this.context = context;
this.originalError = originalError;
}
}
// Logger utility for structured error reporting
const logger = {
error: (message, context, error = null) => {
const errorInfo = {
timestamp: new Date().toISOString(),
context,
message,
originalError: error?.message || error,
};
console.error(JSON.stringify(errorInfo));
},
};
// Async utility for generator functions
const asyncGenerator = (context, _args, generator) =>
new Promise((resolve, reject) => {
const step = (result) => {
try {
const next = generator.next(result);
if (next.done) {
resolve(next.value);
} else {
Promise.resolve(next.value).then(step, handleError);
}
} catch (error) {
handleError(error);
}
};
const handleError = (error) => {
logger.error("Async generator failed", context, error);
reject(new HeicConversionError(error.message, context, error));
};
try {
step(generator.next());
} catch (error) {
handleError(error);
}
});
// HEIC format detection
export const isHeic = (file) => {
return asyncGenerator("formatDetection", null, function* () {
try {
const buffer = (yield file.arrayBuffer()).slice(8, 12);
const decoder = new TextDecoder("utf-8");
const fileType = decoder.decode(buffer).replace("\0", " ").trim();
const supportedTypes = ["mif1", "msf1", "heic", "heix", "hevc", "hevx"];
if (!supportedTypes.includes(fileType)) {
logger.error(`Unsupported file type: ${fileType}`, "formatDetection");
return false;
}
return true;
} catch (error) {
throw new HeicConversionError(
"Failed to detect HEIC format",
"formatDetection",
error,
);
}
});
};
// Worker management
let heicWorker = null;
const getWorker = () => {
if (!heicWorker) {
try {
// The large worker code blob would be loaded from external file in production
const workerCode =
`// Worker code here - would be loaded from separate file`;
const blob = new Blob([workerCode], { type: "application/javascript" });
heicWorker = new Worker(URL.createObjectURL(blob));
heicWorker.onerror = (error) => {
logger.error("Worker initialization failed", "workerSetup", error);
throw new HeicConversionError(
"Worker failed to initialize",
"workerSetup",
error,
);
};
} catch (error) {
logger.error("Failed to create worker", "workerSetup", error);
throw new HeicConversionError(
"Worker creation failed",
"workerSetup",
error,
);
}
}
return heicWorker;
};
// Convert HEIC to image data using worker
const convertToImageData = (buffer) => {
return new Promise((resolve, reject) => {
try {
const worker = getWorker();
const messageId = (Math.random() * new Date().getTime()).toString();
const message = { id: messageId, buffer };
const handleMessage = (event) => {
if (event.data.id === messageId) {
worker.removeEventListener("message", handleMessage);
worker.removeEventListener("error", handleError);
if (event.data.error) {
reject(
new HeicConversionError(event.data.error, "workerConversion"),
);
} else {
resolve(event.data.imageData);
}
}
};
const handleError = (error) => {
worker.removeEventListener("message", handleMessage);
worker.removeEventListener("error", handleError);
reject(
new HeicConversionError(
"Worker communication failed",
"workerConversion",
error,
),
);
};
worker.addEventListener("message", handleMessage);
worker.postMessage(message);
worker.addEventListener("error", handleError);
} catch (error) {
reject(
new HeicConversionError(
"Failed to send message to worker",
"workerConversion",
error,
),
);
}
});
};
// Convert image data to canvas
const imageDataToCanvas = (buffer) => {
return asyncGenerator("canvasConversion", null, function* () {
try {
const imageData = yield convertToImageData(buffer);
const canvas = document.createElement("canvas");
canvas.width = imageData.width;
canvas.height = imageData.height;
const context = canvas.getContext("2d");
if (!context) {
throw new HeicConversionError(
"Failed to get canvas context",
"canvasConversion",
);
}
context.putImageData(imageData, 0, 0);
return canvas;
} catch (error) {
throw new HeicConversionError(
"Failed to convert image data to canvas",
"canvasConversion",
error,
);
}
});
};
// Clean up canvas resources
const cleanupCanvas = (canvas) => {
try {
canvas.width = 1;
canvas.height = 1;
const context = canvas.getContext("2d");
if (context) {
context.clearRect(0, 0, 1, 1);
}
} catch (error) {
logger.error("Canvas cleanup failed", "resourceCleanup", error);
}
};
// Main HEIC conversion function
export const heicTo = (options) => {
return asyncGenerator(
"mainConversion",
[options],
function* (
{
blob,
type = "image/jpeg",
quality = 0.8,
options: convertOptions = {},
},
) {
let canvas = null;
try {
// Validate inputs
if (!blob) {
throw new HeicConversionError("Blob is required", "inputValidation");
}
if (typeof quality !== "number" || quality < 0 || quality > 1) {
throw new HeicConversionError(
"Quality must be a number between 0 and 1",
"inputValidation",
);
}
const buffer = yield blob.arrayBuffer();
if (type === "bitmap") {
const imageData = yield convertToImageData(buffer);
return createImageBitmap(imageData, convertOptions);
} else {
canvas = yield imageDataToCanvas(buffer);
return yield new Promise((resolve, reject) => {
try {
canvas.toBlob(
(result) => {
if (result != null) {
resolve(result);
} else {
reject(
new HeicConversionError(
"Canvas to blob conversion failed",
"blobConversion",
),
);
}
},
type,
quality,
);
} catch (error) {
reject(
new HeicConversionError(
"Canvas to blob conversion error",
"blobConversion",
error,
),
);
}
});
}
} catch (error) {
// Re-throw HeicConversionError as-is, wrap others
if (error instanceof HeicConversionError) {
throw error;
}
throw new HeicConversionError(error.message, "mainConversion", error);
} finally {
// Always clean up canvas resources
if (canvas) {
cleanupCanvas(canvas);
}
}
},
);
};
// Export error class for external error handling
export { HeicConversionError };