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`; }, 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 "
${file.name}
"`; 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 "
${fileName}
"`; ui.progress.bar.style.width = overallProgress + "%"; console.log(`Optimizing "${fileNameShort}" (${overallProgress}%)`); if (p === 100 && state.compressProcessedCount === state.compressQueueTotal - 1) { ui.progress.text.innerHTML = `
Done!
`; } } } 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`; 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%"; } }