diff --git a/islands/ImagesCompressor.tsx b/islands/ImagesCompressor.tsx index a80c3e8..2d8e846 100644 --- a/islands/ImagesCompressor.tsx +++ b/islands/ImagesCompressor.tsx @@ -8,6 +8,7 @@ import { startCompressing, updateCompressionOptions, } from "@/signals/home.ts"; +import { CompressionOptions } from "@/lib/compress.ts"; function IdleSection() { const compressionOptions = getCompressionOptions(); @@ -55,12 +56,30 @@ function IdleSection() { disabled: getSelectedImages().length <= 1, }, }, { - name: "convertToJpeg", - label: "convert to JPEG", - inputCheckbox: { - checked: compressionOptions.convertToJpeg, - onChange: (value: boolean) => - updateCompressionOptions({ convertToJpeg: value }), + name: "convert", + label: "convert to", + select: { + onChange: (value: CompressionOptions["convert"]) => + updateCompressionOptions({ convert: value }), + options: [ + { + value: "none", + children: "none", + }, + { + value: "image/jpeg", + children: "jpg", + }, + { + value: "image/png", + children: "png", + }, + { + value: "image/webp", + children: "webp", + }, + ], + value: compressionOptions.convert, }, }]; @@ -69,7 +88,7 @@ function IdleSection() {
Settings {settingsInputs.map( - ({ name, label, inputRange, inputNumber, inputCheckbox }) => ( + ({ name, label, inputRange, inputNumber, inputCheckbox, select }) => (
@@ -112,6 +131,20 @@ function IdleSection() { )} /> )} + {select && ( + + )}
), diff --git a/lib/compress.ts b/lib/compress.ts index bb5b97a..3fdbe86 100644 --- a/lib/compress.ts +++ b/lib/compress.ts @@ -7,7 +7,7 @@ export interface CompressionOptions { maxHeight: number; zipFile: boolean; hardwareConcurrency: number; - convertToJpeg: boolean; + convert: "image/png" | "image/jpeg" | "image/webp" | "none"; } interface WorkerCompressOptions { @@ -34,8 +34,8 @@ export async function compress( const zipWriter = new ZipWriter(zipFileWriter); for (const file of imageFiles) { - const filename = options.convertToJpeg - ? replaceExtension(file.name, "jpg") + const filename = options.convert !== "none" + ? replaceExtension(file.name, fileTypeToExtension[options.convert]) : file.name; const p = (async () => { @@ -63,7 +63,7 @@ export async function compress( } else { // Download images directly. const blob = new Blob([compressedImg], { - type: options.convertToJpeg ? "image/jpeg" : file.type, + type: options.convert !== "none" ? options.convert : file.type, }); downloadBlob(blob, filename); } @@ -83,6 +83,12 @@ export async function compress( return results; } +const fileTypeToExtension = { + "image/png": "png", + "image/jpeg": "jpg", + "image/webp": "jpg", +}; + function downloadBlob(blob: Blob, filename: string) { const anchor = document.createElement("a"); anchor.href = window.URL.createObjectURL(blob); @@ -94,7 +100,7 @@ function replaceExtension(filename: string, ext: string) { const splitted = filename.split("."); if (splitted.length === 1) { splitted.push(ext); - } else if (splitted[splitted.length - 1].toLowerCase() !== "jpg") { + } else if (splitted[splitted.length - 1].toLowerCase() !== ext) { splitted[splitted.length - 1] = ext; } diff --git a/signals/home.ts b/signals/home.ts index 83edd03..187683c 100644 --- a/signals/home.ts +++ b/signals/home.ts @@ -22,7 +22,7 @@ const defaultCompressionOptions: CompressionOptions = { maxHeight: 4096, zipFile: true, hardwareConcurrency: (navigator.hardwareConcurrency ?? 8) / 4, - convertToJpeg: true, + convert: "none", }; const compressionOptions = signal({ diff --git a/static/worker_script.js b/static/worker_script.js index f65b375..f69a927 100644 --- a/static/worker_script.js +++ b/static/worker_script.js @@ -39,11 +39,20 @@ function workerProcedureHandler( }; } +function isLosslessImageFormat(format) { + switch (format) { + case "image/webp", "image/jpeg": + return false; + default: + return true; + } +} + self.onmessage = workerProcedureHandler({ setupWorker(workedId) { console.debug("worker", workedId, "setup"); }, - async compress(imageFile, { quality, maxWidth, maxHeight, convertToJpeg }) { + async compress(imageFile, { quality, maxWidth, maxHeight, convert }) { let bitmap = await createImageBitmap(imageFile); let drawWidth = bitmap.width; let drawHeight = bitmap.height; @@ -67,11 +76,13 @@ self.onmessage = workerProcedureHandler({ ctx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height); - // Image is not a JPEG images, apply JPEG encoding to compress it. - if (imageFile.type !== "image/jpeg" && !convertToJpeg) { + const convertToLossless = isLosslessImageFormat( + convert !== "none" ? convert : imageFile.type, + ); + if (convertToLossless) { ctx.drawImage(bitmap, 0, 0, drawWidth, drawHeight); const blob = await offscreenCanvas.convertToBlob({ - type: "image/jpeg", + type: "image/webp", quality: quality / 100, }); @@ -81,8 +92,8 @@ self.onmessage = workerProcedureHandler({ ctx.drawImage(bitmap, 0, 0, drawWidth, drawHeight); const blob = await offscreenCanvas.convertToBlob({ - type: convertToJpeg ? "image/jpeg" : imageFile.type, - quality: quality / 100, + type: convert === "none" ? imageFile.type : convert, + quality: convertToLossless ? undefined : quality / 100, }); return blob.arrayBuffer();