From 3748463d3891709d004e5c023073744b0e2286a2 Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Mon, 9 Mar 2026 16:49:17 +0100 Subject: [PATCH] Add direct image download to pdf-to-webp --- src/js/logic/pdf-to-webp-page.ts | 338 +++++++++++++++++-------------- src/pages/pdf-to-webp.html | 2 +- 2 files changed, 192 insertions(+), 148 deletions(-) diff --git a/src/js/logic/pdf-to-webp-page.ts b/src/js/logic/pdf-to-webp-page.ts index cdafdf7..c2c0fec 100644 --- a/src/js/logic/pdf-to-webp-page.ts +++ b/src/js/logic/pdf-to-webp-page.ts @@ -1,193 +1,237 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { downloadFile, formatBytes, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; +import { + downloadFile, + formatBytes, + readFileAsArrayBuffer, + getPDFDocument, + getCleanPdfFilename, +} from '../utils/helpers.js'; import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; +import { PDFPageProxy } from 'pdfjs-dist'; -pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); +pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.min.mjs', + import.meta.url +).toString(); let files: File[] = []; const updateUI = () => { - const fileDisplayArea = document.getElementById('file-display-area'); - const optionsPanel = document.getElementById('options-panel'); - const dropZone = document.getElementById('drop-zone'); + const fileDisplayArea = document.getElementById('file-display-area'); + const optionsPanel = document.getElementById('options-panel'); + const dropZone = document.getElementById('drop-zone'); - if (!fileDisplayArea || !optionsPanel || !dropZone) return; + if (!fileDisplayArea || !optionsPanel || !dropZone) return; - fileDisplayArea.innerHTML = ''; + fileDisplayArea.innerHTML = ''; - if (files.length > 0) { - optionsPanel.classList.remove('hidden'); + if (files.length > 0) { + optionsPanel.classList.remove('hidden'); - files.forEach((file) => { - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + files.forEach((file) => { + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; - const infoContainer = document.createElement('div'); - infoContainer.className = 'flex flex-col overflow-hidden'; + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col overflow-hidden'; - const nameSpan = document.createElement('div'); - nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; - nameSpan.textContent = file.name; + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = file.name; - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state - infoContainer.append(nameSpan, metaSpan); + infoContainer.append(nameSpan, metaSpan); - const removeBtn = document.createElement('button'); - removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; - removeBtn.innerHTML = ''; - removeBtn.onclick = () => { - files = []; - updateUI(); - }; + const removeBtn = document.createElement('button'); + removeBtn.className = + 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => { + files = []; + updateUI(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); - // Fetch page count asynchronously - readFileAsArrayBuffer(file).then(buffer => { - return getPDFDocument(buffer).promise; - }).then(pdf => { - metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`; - }).catch(e => { - console.warn('Error loading PDF page count:', e); - metaSpan.textContent = formatBytes(file.size); - }); + // Fetch page count asynchronously + readFileAsArrayBuffer(file) + .then((buffer) => { + return getPDFDocument(buffer).promise; + }) + .then((pdf) => { + metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`; + }) + .catch((e) => { + console.warn('Error loading PDF page count:', e); + metaSpan.textContent = formatBytes(file.size); }); + }); - // Initialize icons immediately after synchronous render - createIcons({ icons }); - } else { - optionsPanel.classList.add('hidden'); - } + // Initialize icons immediately after synchronous render + createIcons({ icons }); + } else { + optionsPanel.classList.add('hidden'); + } }; const resetState = () => { - files = []; - const fileInput = document.getElementById('file-input') as HTMLInputElement; - if (fileInput) fileInput.value = ''; - const qualitySlider = document.getElementById('webp-quality') as HTMLInputElement; - const qualityValue = document.getElementById('webp-quality-value'); - if (qualitySlider) qualitySlider.value = '0.85'; - if (qualityValue) qualityValue.textContent = '85%'; - updateUI(); + files = []; + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; + const qualitySlider = document.getElementById( + 'webp-quality' + ) as HTMLInputElement; + const qualityValue = document.getElementById('webp-quality-value'); + if (qualitySlider) qualitySlider.value = '0.85'; + if (qualityValue) qualityValue.textContent = '85%'; + updateUI(); }; async function convert() { - if (files.length === 0) { - showAlert('No File', 'Please upload a PDF file first.'); - return; - } - showLoader('Converting to WebP...'); - try { - const pdf = await getPDFDocument( - await readFileAsArrayBuffer(files[0]) - ).promise; - const zip = new JSZip(); + if (files.length === 0) { + showAlert('No File', 'Please upload a PDF file first.'); + return; + } + showLoader('Converting to WebP...'); + try { + const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) + .promise; - const qualityInput = document.getElementById('webp-quality') as HTMLInputElement; - const quality = qualityInput ? parseFloat(qualityInput.value) : 0.85; + const qualityInput = document.getElementById( + 'webp-quality' + ) as HTMLInputElement; + const quality = qualityInput ? parseFloat(qualityInput.value) : 0.85; - for (let i = 1; i <= pdf.numPages; i++) { - const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale: 2.0 }); - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - canvas.height = viewport.height; - canvas.width = viewport.width; - - await page.render({ canvasContext: context!, viewport: viewport, canvas }).promise; - - const blob = await new Promise((resolve) => - canvas.toBlob(resolve, 'image/webp', quality) - ); - if (blob) { - zip.file(`page_${i}.webp`, blob); - } + if (pdf.numPages === 1) { + const page = await pdf.getPage(1); + const blob = await renderPage(page, quality); + downloadFile(blob, getCleanPdfFilename(files[0].name) + '.webp'); + } else { + const zip = new JSZip(); + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const blob = await renderPage(page, quality); + if (blob) { + zip.file(`page_${i}.webp`, blob); } + } - const zipBlob = await zip.generateAsync({ type: 'blob' }); - downloadFile(zipBlob, 'converted_images.zip'); - showAlert('Success', 'PDF converted to WebPs successfully!', 'success', () => { - resetState(); - }); - } catch (e) { - console.error(e); - showAlert( - 'Error', - 'Failed to convert PDF to WebP. The file might be corrupted.' - ); - } finally { - hideLoader(); + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, getCleanPdfFilename(files[0].name) + '_webps.zip'); } + + showAlert( + 'Success', + 'PDF converted to WebPs successfully!', + 'success', + () => { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert( + 'Error', + 'Failed to convert PDF to WebP. The file might be corrupted.' + ); + } finally { + hideLoader(); + } +} + +async function renderPage( + page: PDFPageProxy, + quality: number +): Promise { + const viewport = page.getViewport({ scale: 2.0 }); + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.height = viewport.height; + canvas.width = viewport.width; + + await page.render({ + canvasContext: context!, + viewport: viewport, + canvas, + }).promise; + + const blob = await new Promise((resolve) => + canvas.toBlob(resolve, 'image/webp', quality) + ); + return blob; } document.addEventListener('DOMContentLoaded', () => { - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const processBtn = document.getElementById('process-btn'); - const backBtn = document.getElementById('back-to-tools'); - const qualitySlider = document.getElementById('webp-quality') as HTMLInputElement; - const qualityValue = document.getElementById('webp-quality-value'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const processBtn = document.getElementById('process-btn'); + const backBtn = document.getElementById('back-to-tools'); + const qualitySlider = document.getElementById( + 'webp-quality' + ) as HTMLInputElement; + const qualityValue = document.getElementById('webp-quality-value'); - if (backBtn) { - backBtn.addEventListener('click', () => { - window.location.href = import.meta.env.BASE_URL; - }); + if (backBtn) { + backBtn.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); + } + + if (qualitySlider && qualityValue) { + qualitySlider.addEventListener('input', () => { + qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`; + }); + } + + const handleFileSelect = (newFiles: FileList | null) => { + if (!newFiles || newFiles.length === 0) return; + const validFiles = Array.from(newFiles).filter( + (file) => file.type === 'application/pdf' + ); + + if (validFiles.length === 0) { + showAlert('Invalid File', 'Please upload a PDF file.'); + return; } - if (qualitySlider && qualityValue) { - qualitySlider.addEventListener('input', () => { - qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`; - }); - } + files = [validFiles[0]]; + updateUI(); + }; - const handleFileSelect = (newFiles: FileList | null) => { - if (!newFiles || newFiles.length === 0) return; - const validFiles = Array.from(newFiles).filter( - (file) => file.type === 'application/pdf' - ); + if (fileInput && dropZone) { + fileInput.addEventListener('change', (e) => { + handleFileSelect((e.target as HTMLInputElement).files); + }); - if (validFiles.length === 0) { - showAlert('Invalid File', 'Please upload a PDF file.'); - return; - } + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); - files = [validFiles[0]]; - updateUI(); - }; + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); - if (fileInput && dropZone) { - fileInput.addEventListener('change', (e) => { - handleFileSelect((e.target as HTMLInputElement).files); - }); + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + handleFileSelect(e.dataTransfer?.files ?? null); + }); - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } - dropZone.addEventListener('dragleave', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - }); - - dropZone.addEventListener('drop', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - handleFileSelect(e.dataTransfer?.files ?? null); - }); - - fileInput.addEventListener('click', () => { - fileInput.value = ''; - }); - } - - if (processBtn) { - processBtn.addEventListener('click', convert); - } + if (processBtn) { + processBtn.addEventListener('click', convert); + } }); diff --git a/src/pages/pdf-to-webp.html b/src/pages/pdf-to-webp.html index 253d1f6..0501482 100644 --- a/src/pages/pdf-to-webp.html +++ b/src/pages/pdf-to-webp.html @@ -187,7 +187,7 @@