291 lines
7.7 KiB
JavaScript
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 };
|