tools/media-tools/image-converter/assets/js/compression.js

273 lines
9.2 KiB
JavaScript

async function preProcessImage(file) {
let preProcessedImage = null;
let preProcessedNewFileType = null;
if (file.type === "image/heic" || file.type === "image/heif" || isHeicExt(file)) {
console.log('Pre-processing HEIC image...')
preProcessedImage = await HeicTo({
blob: file,
type: "image/jpeg",
quality: 0.9,
});
console.log("preProcessedImage: ", preProcessedImage);
preProcessedNewFileType = "image/jpeg";
}
if (file.type === "image/avif") {
console.log('Pre-processing AVIF image...')
setTimeout(() => {
ui.progress.text.innerHTML = `Please wait. AVIF files may take longer to prepare<span class="loading-dots">`;
}, 5000);
preProcessedImage = await imageCompression(file, {
quality: 0.8,
fileType: "image/jpeg",
useWebWorker: true,
preserveExif: false,
libURL: "./browser-image-compression.js",
alwaysKeepResolution: true,
});
preProcessedNewFileType = "image/jpeg";
}
return { preProcessedImage, preProcessedNewFileType };
}
async function createCompressionOptions(onProgress, file) {
const compressMethod = getCheckedValue(ui.inputs.compressMethod);
const dimensionMethod = getCheckedValue(ui.inputs.dimensionMethod);
const maxWeight = parseFloat(ui.inputs.limitWeight.value);
const { selectedFormat } = getFileType(file);
quality = Math.min(Math.max(parseFloat(ui.inputs.quality.value) / 100, 0), 1);
console.log("Input image file size: ", (file.size / 1024 / 1024).toFixed(3), "MB");
let maxWeightMB = ui.inputs.limitWeightUnit.value.toUpperCase() === "KB" ?
ui.inputs.limitWeight.value / 1024 :
ui.inputs.limitWeight.value;
let limitDimensionsValue = undefined;
if (file.type === "image/heif" || file.type === "image/heic" || isHeicExt(file)) {
if (getCheckedValue(ui.inputs.dimensionMethod) === "limit") {
limitDimensionsValue = (ui.inputs.limitDimensions.value > 50) ? ui.inputs.limitDimensions.value : 50;
}
else {
limitDimensionsValue = undefined;
}
}
else {
limitDimensionsValue = dimensionMethod === "limit" ?
await getAdjustedDimensions(file, ui.inputs.limitDimensions.value) :
undefined;
}
const options = {
maxSizeMB: maxWeight && compressMethod === "limitWeight" ? maxWeightMB : (file.size / 1024 / 1024).toFixed(3),
initialQuality: quality && compressMethod === "quality" ? quality : undefined,
maxWidthOrHeight: limitDimensionsValue,
useWebWorker: true,
onProgress,
preserveExif: false,
fileType: selectedFormat || undefined,
libURL: "./browser-image-compression.js",
alwaysKeepResolution: true,
};
if (state.controller) {
options.signal = state.controller.signal;
}
console.log("Settings:", options);
return options;
}
async function compressImageQueue() {
if (!state.compressQueue.length) {
resetCompressionState(true);
return;
}
const file = state.compressQueue[0];
const i = state.compressProcessedCount;
console.log('Input file: ', file);
if (!isFileTypeSupported(file.type, file)) {
console.error(`Unsupported file type: ${file.type}. Skipping "${file.name}".`);
ui.progress.text.innerHTML = `Unsupported file "<div class='progress-file-name'>${file.name}</div>"`;
state.compressQueue.shift();
await compressImageQueue();
return;
}
const options = await createCompressionOptions((p) => onProgress(p, i, file.name), file);
const { preProcessedImage, preProcessedNewFileType } = await preProcessImage(file);
if (preProcessedImage) {
options.fileType = preProcessedNewFileType;
}
imageCompression(preProcessedImage || file, options)
.then((output) => handleCompressionResult(file, output))
.catch((error) => console.error(error.message))
.finally(() => {
state.compressProcessedCount++;
state.compressQueue.shift();
resetCompressionState(state.compressProcessedCount === state.compressQueueTotal);
if (state.compressProcessedCount < state.compressQueueTotal) {
compressImageQueue();
}
});
function onProgress(p, index, fileName) {
const overallProgress = calculateOverallProgress(
state.fileProgressMap,
state.compressQueueTotal
);
const fileNameShort =
fileName.length > 15 ? fileName.slice(0, 12) + "..." : fileName;
state.fileProgressMap[index] = p;
ui.progress.queueCount.textContent = `${
state.compressProcessedCount + 1
} / ${state.compressQueueTotal}`;
ui.progress.text.dataset.progress = overallProgress;
ui.progress.text.innerHTML = `Optimizing "<div class='progress-file-name'>${fileName}</div>"`;
ui.progress.bar.style.width = overallProgress + "%";
console.log(`Optimizing "${fileNameShort}" (${overallProgress}%)`);
if (p === 100 && state.compressProcessedCount === state.compressQueueTotal - 1) {
ui.progress.text.innerHTML = `
<div class="badge badge--success pt-2xs pb-2xs bg:surface">
<div class="badge-text flex items-center gap-3xs">
<svg height="16" stroke-linejoin="round" viewBox="0 0 16 16" width="16" style="color: currentcolor;"><path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8ZM16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM11.5303 6.53033L12.0607 6L11 4.93934L10.4697 5.46967L6.5 9.43934L5.53033 8.46967L5 7.93934L3.93934 9L4.46967 9.53033L5.96967 11.0303C6.26256 11.3232 6.73744 11.3232 7.03033 11.0303L11.5303 6.53033Z" fill="currentColor"></path></svg>
<span>Done!</span>
</div>
<div>
`;
}
}
}
function compressImage(event) {
state.controller = new AbortController();
state.compressQueue = Array.from(event.target.files);
state.compressQueueTotal = state.compressQueue.length;
state.compressProcessedCount = 0;
state.fileProgressMap = {};
state.isCompressing = true;
document.body.classList.add("compressing--is-active");
ui.actions.dropZone.classList.add("hidden");
ui.actions.abort.classList.remove("hidden");
ui.progress.container.classList.remove("hidden");
ui.progress.text.innerHTML = `Preparing<span class="loading-dots">`;
compressImageQueue();
}
function handleCompressionResult(file, output) {
const { outputFileExtension, selectedFormat } = getFileType(file);
const outputImageBlob = URL.createObjectURL(output);
const { renamedFileName, isBrowserDefaultFileName } = renameBrowserDefaultFileName(file.name);
const outputFileNameText = updateFileExtension(
isBrowserDefaultFileName ? renamedFileName : file.name,
outputFileExtension,
selectedFormat
);
const inputFileSize = parseFloat((file.size / 1024 / 1024).toFixed(3));
const outputFileSize = parseFloat((output.size / 1024 / 1024).toFixed(3));
const fileSizeSaved = inputFileSize - outputFileSize;
const fileSizeSavedPercentage =
inputFileSize > 0
? Math.abs(((fileSizeSaved / inputFileSize) * 100).toFixed(2))
: "0.000";
const fileSizeSavedTrend =
fileSizeSaved < 0 ? "+" : fileSizeSaved > 0 ? "-" : "";
const fileSizeSavedClass =
fileSizeSaved <= 0 ? "badge--error" : "badge--success";
imageCompression(output, config.thumbnailOptions).then((thumbnailBlob) => {
const thumbnailDataURL = URL.createObjectURL(thumbnailBlob);
getImageDimensions(outputImageBlob, ({ width, height }) => {
const outputHTML = buildOutputItemHTML({
outputImageBlob,
thumbnailDataURL,
outputFileNameText,
outputFileExtension,
width,
height,
fileSize: output.size,
fileSizeSavedTrend,
fileSizeSavedPercentage,
fileSizeSavedClass,
});
const wrapper = document.createElement("div");
wrapper.innerHTML = outputHTML.trim();
ui.output.content.prepend(wrapper.firstChild);
state.outputImageCount++;
ui.output.container.dataset.count = state.outputImageCount;
ui.output.subpageOutput.dataset.count = state.outputImageCount;
ui.output.imageCount.dataset.count = state.outputImageCount;
ui.output.imageCount.textContent = state.outputImageCount;
if (state.compressProcessedCount === 1) {
selectSubpage("output");
}
});
});
}
function calculateOverallProgress(progressMap, totalFiles) {
const sum = Object.values(progressMap).reduce((acc, val) => acc + val, 0);
return Math.round(sum / totalFiles);
}
function resetCompressionState(isAllProcessed, aborted) {
const resetState = () => {
state.compressProcessedCount = 0;
state.compressQueueTotal = 0;
ui.progress.queueCount.textContent = "";
state.compressQueue = [];
state.isCompressing = false;
};
if (aborted) {
resetUI();
resetState();
return;
}
if (isAllProcessed) {
ui.actions.abort.classList.add("hidden");
ui.progress.bar.style.width = "100%";
setTimeout(() => {
// Delay state reset to allow "Done" message to remain
resetUI();
state.isCompressing = false;
}, 1000);
return;
}
if (state.isCompressing && state.compressProcessedCount === 0) {
ui.progress.text.dataset.progress = 0;
ui.progress.text.textContent = "Preparing 0%";
ui.progress.bar.style.width = "0%";
}
}