diff --git a/src/js/logic/pdf-to-bmp-page.ts b/src/js/logic/pdf-to-bmp-page.ts index d165851..1f71b77 100644 --- a/src/js/logic/pdf-to-bmp-page.ts +++ b/src/js/logic/pdf-to-bmp-page.ts @@ -1,179 +1,214 @@ 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); - // Add remove button - 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(); - }; + // Add remove button + 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 = ''; - updateUI(); + files = []; + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; + updateUI(); }; async function convert() { - if (files.length === 0) { - showAlert('No File', 'Please upload a PDF file first.'); - return; - } - showLoader('Converting to BMP...'); - 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 BMP...'); + try { + const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) + .promise; - 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/bmp') - ); - if (blob) { - zip.file(`page_${i}.bmp`, blob); - } + if (pdf.numPages === 1) { + const page = await pdf.getPage(1); + const blob = await renderPage(page); + downloadFile(blob, getCleanPdfFilename(files[0].name) + '.bmp'); + } else { + const zip = new JSZip(); + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const blob = await renderPage(page); + if (blob) { + zip.file(`page_${i}.bmp`, blob); } + } - const zipBlob = await zip.generateAsync({ type: 'blob' }); - downloadFile(zipBlob, 'converted_images.zip'); - showAlert('Success', 'PDF converted to BMPs successfully!', 'success', () => { - resetState(); - }); - } catch (e) { - console.error(e); - showAlert( - 'Error', - 'Failed to convert PDF to BMP. The file might be corrupted.' - ); - } finally { - hideLoader(); + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, getCleanPdfFilename(files[0].name) + '_bmps.zip'); } + + showAlert( + 'Success', + 'PDF converted to BMPs successfully!', + 'success', + () => { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert( + 'Error', + 'Failed to convert PDF to BMP. The file might be corrupted.' + ); + } finally { + hideLoader(); + } +} + +async function renderPage(page: PDFPageProxy): 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/bmp') + ); + 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 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'); - 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; + }); + } + + 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; } - const handleFileSelect = (newFiles: FileList | null) => { - if (!newFiles || newFiles.length === 0) return; - const validFiles = Array.from(newFiles).filter( - (file) => file.type === 'application/pdf' - ); + files = [validFiles[0]]; + updateUI(); + }; - if (validFiles.length === 0) { - showAlert('Invalid File', 'Please upload a PDF file.'); - return; - } + if (fileInput && dropZone) { + fileInput.addEventListener('change', (e) => { + handleFileSelect((e.target as HTMLInputElement).files); + }); - files = [validFiles[0]]; - updateUI(); - }; + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); - if (fileInput && dropZone) { - fileInput.addEventListener('change', (e) => { - handleFileSelect((e.target as HTMLInputElement).files); - }); + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + handleFileSelect(e.dataTransfer?.files ?? null); + }); - dropZone.addEventListener('dragleave', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - }); + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } - 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/js/logic/pdf-to-jpg-page.ts b/src/js/logic/pdf-to-jpg-page.ts index b01bdc3..dc43566 100644 --- a/src/js/logic/pdf-to-jpg-page.ts +++ b/src/js/logic/pdf-to-jpg-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('jpg-quality') as HTMLInputElement; - const qualityValue = document.getElementById('jpg-quality-value'); - if (qualitySlider) qualitySlider.value = '0.9'; - if (qualityValue) qualityValue.textContent = '90%'; - updateUI(); + files = []; + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; + const qualitySlider = document.getElementById( + 'jpg-quality' + ) as HTMLInputElement; + const qualityValue = document.getElementById('jpg-quality-value'); + if (qualitySlider) qualitySlider.value = '0.9'; + if (qualityValue) qualityValue.textContent = '90%'; + updateUI(); }; async function convert() { - if (files.length === 0) { - showAlert('No File', 'Please upload a PDF file first.'); - return; - } - showLoader('Converting to JPG...'); - 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 JPG...'); + try { + const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) + .promise; - const qualityInput = document.getElementById('jpg-quality') as HTMLInputElement; - const quality = qualityInput ? parseFloat(qualityInput.value) : 0.9; + const qualityInput = document.getElementById( + 'jpg-quality' + ) as HTMLInputElement; + const quality = qualityInput ? parseFloat(qualityInput.value) : 0.9; - 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/jpeg', quality) - ); - if (blob) { - zip.file(`page_${i}.jpg`, blob); - } + if (pdf.numPages === 1) { + const page = await pdf.getPage(1); + const blob = await renderPage(page, quality); + downloadFile(blob, getCleanPdfFilename(files[0].name) + '.jpg'); + } 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}.jpg`, blob); } + } - const zipBlob = await zip.generateAsync({ type: 'blob' }); - downloadFile(zipBlob, 'converted_images.zip'); - showAlert('Success', 'PDF converted to JPGs successfully!', 'success', () => { - resetState(); - }); - } catch (e) { - console.error(e); - showAlert( - 'Error', - 'Failed to convert PDF to JPG. The file might be corrupted.' - ); - } finally { - hideLoader(); + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, getCleanPdfFilename(files[0].name) + '_jpgs.zip'); } + + showAlert( + 'Success', + 'PDF converted to JPGs successfully!', + 'success', + () => { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert( + 'Error', + 'Failed to convert PDF to JPG. 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/jpeg', 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('jpg-quality') as HTMLInputElement; - const qualityValue = document.getElementById('jpg-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( + 'jpg-quality' + ) as HTMLInputElement; + const qualityValue = document.getElementById('jpg-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/js/logic/pdf-to-png-page.ts b/src/js/logic/pdf-to-png-page.ts index f453b65..8f4995d 100644 --- a/src/js/logic/pdf-to-png-page.ts +++ b/src/js/logic/pdf-to-png-page.ts @@ -1,193 +1,231 @@ 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 scaleSlider = document.getElementById('png-scale') as HTMLInputElement; - const scaleValue = document.getElementById('png-scale-value'); - if (scaleSlider) scaleSlider.value = '2.0'; - if (scaleValue) scaleValue.textContent = '2.0x'; - updateUI(); + files = []; + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; + const scaleSlider = document.getElementById('png-scale') as HTMLInputElement; + const scaleValue = document.getElementById('png-scale-value'); + if (scaleSlider) scaleSlider.value = '2.0'; + if (scaleValue) scaleValue.textContent = '2.0x'; + updateUI(); }; async function convert() { - if (files.length === 0) { - showAlert('No File', 'Please upload a PDF file first.'); - return; - } - showLoader('Converting to PNG...'); - 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 PNG...'); + try { + const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) + .promise; - const scaleInput = document.getElementById('png-scale') as HTMLInputElement; - const scale = scaleInput ? parseFloat(scaleInput.value) : 2.0; + const scaleInput = document.getElementById('png-scale') as HTMLInputElement; + const scale = scaleInput ? parseFloat(scaleInput.value) : 2.0; - for (let i = 1; i <= pdf.numPages; i++) { - const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale }); - 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/png') - ); - if (blob) { - zip.file(`page_${i}.png`, blob); - } + if (pdf.numPages === 1) { + const page = await pdf.getPage(1); + const blob = await renderPage(page, scale); + downloadFile(blob, getCleanPdfFilename(files[0].name) + '.png'); + } else { + const zip = new JSZip(); + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const blob = await renderPage(page, scale); + if (blob) { + zip.file(`page_${i}.png`, blob); } + } - const zipBlob = await zip.generateAsync({ type: 'blob' }); - downloadFile(zipBlob, 'converted_images.zip'); - showAlert('Success', 'PDF converted to PNGs successfully!', 'success', () => { - resetState(); - }); - } catch (e) { - console.error(e); - showAlert( - 'Error', - 'Failed to convert PDF to PNG. The file might be corrupted.' - ); - } finally { - hideLoader(); + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, getCleanPdfFilename(files[0].name) + '_pngs.zip'); } + + showAlert( + 'Success', + 'PDF converted to PNGs successfully!', + 'success', + () => { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert( + 'Error', + 'Failed to convert PDF to PNG. The file might be corrupted.' + ); + } finally { + hideLoader(); + } +} + +async function renderPage( + page: PDFPageProxy, + scale: number +): Promise { + const viewport = page.getViewport({ scale }); + 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/png') + ); + 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 scaleSlider = document.getElementById('png-scale') as HTMLInputElement; - const scaleValue = document.getElementById('png-scale-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 scaleSlider = document.getElementById('png-scale') as HTMLInputElement; + const scaleValue = document.getElementById('png-scale-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 (scaleSlider && scaleValue) { + scaleSlider.addEventListener('input', () => { + scaleValue.textContent = `${parseFloat(scaleSlider.value).toFixed(1)}x`; + }); + } + + 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 (scaleSlider && scaleValue) { - scaleSlider.addEventListener('input', () => { - scaleValue.textContent = `${parseFloat(scaleSlider.value).toFixed(1)}x`; - }); - } + 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/js/logic/pdf-to-tiff-page.ts b/src/js/logic/pdf-to-tiff-page.ts index d92734d..10ad314 100644 --- a/src/js/logic/pdf-to-tiff-page.ts +++ b/src/js/logic/pdf-to-tiff-page.ts @@ -1,190 +1,251 @@ 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 UTIF from 'utif'; +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 = ''; - updateUI(); + files = []; + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; + updateUI(); }; async function convert() { - if (files.length === 0) { - showAlert('No File', 'Please upload a PDF file first.'); - return; - } - showLoader('Converting to TIFF...'); - 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 TIFF...'); + try { + const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) + .promise; - 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 imageData = context!.getImageData(0, 0, canvas.width, canvas.height); - const rgba = imageData.data; - - try { - const tiffData = UTIF.encodeImage(new Uint8Array(rgba), canvas.width, canvas.height); - const tiffBlob = new Blob([tiffData], { type: 'image/tiff' }); - zip.file(`page_${i}.tiff`, tiffBlob); - } catch (encodeError: any) { - console.warn(`TIFF encoding failed for page ${i}, using PNG fallback:`, encodeError); - // Fallback to PNG if TIFF encoding fails (e.g., PackBits compression issues) - const pngBlob = await new Promise((resolve) => - canvas.toBlob(resolve, 'image/png') - ); - if (pngBlob) { - zip.file(`page_${i}.png`, pngBlob); - } - } + if (pdf.numPages === 1) { + const page = await pdf.getPage(1); + const blob = await renderPage(page, 1); + downloadFile( + blob.blobData, + getCleanPdfFilename(files[0].name) + '.' + blob.ending + ); + } else { + const zip = new JSZip(); + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const blob = await renderPage(page, i); + if (blob.blobData) { + zip.file(`page_${i}.` + blob.ending, blob.blobData); } + } - const zipBlob = await zip.generateAsync({ type: 'blob' }); - downloadFile(zipBlob, 'converted_images.zip'); - showAlert('Success', 'PDF converted to TIFFs successfully!', 'success', () => { - resetState(); - }); - } catch (e) { - console.error(e); - showAlert( - 'Error', - 'Failed to convert PDF to TIFF. The file might be corrupted.' - ); - } finally { - hideLoader(); + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, getCleanPdfFilename(files[0].name) + '_tiffs.zip'); } + + showAlert( + 'Success', + 'PDF converted to TIFFs successfully!', + 'success', + () => { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert( + 'Error', + 'Failed to convert PDF to TIFF. The file might be corrupted.' + ); + } finally { + hideLoader(); + } +} + +async function renderPage( + page: PDFPageProxy, + pageNumber: number +): Promise<{ blobData: Blob | null; ending: string }> { + 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 imageData = context!.getImageData(0, 0, canvas.width, canvas.height); + const rgba = imageData.data; + + try { + const tiffData = UTIF.encodeImage( + new Uint8Array(rgba), + canvas.width, + canvas.height + ); + const tiffBlob = new Blob([tiffData], { type: 'image/tiff' }); + return { + blobData: tiffBlob, + ending: 'tiff', + }; + } catch (encodeError: any) { + console.warn( + `TIFF encoding failed for page ${pageNumber}, using PNG fallback:`, + encodeError + ); + // Fallback to PNG if TIFF encoding fails (e.g., PackBits compression issues) + const pngBlob = await new Promise((resolve) => + canvas.toBlob(resolve, 'image/png') + ); + if (pngBlob) { + return { + blobData: pngBlob, + ending: 'png', + }; + } + } + + return { + blobData: null, + ending: 'tiff', + }; } 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 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'); - 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; + }); + } + + 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; } - const handleFileSelect = (newFiles: FileList | null) => { - if (!newFiles || newFiles.length === 0) return; - const validFiles = Array.from(newFiles).filter( - (file) => file.type === 'application/pdf' - ); + files = [validFiles[0]]; + updateUI(); + }; - if (validFiles.length === 0) { - showAlert('Invalid File', 'Please upload a PDF file.'); - return; - } + if (fileInput && dropZone) { + fileInput.addEventListener('change', (e) => { + handleFileSelect((e.target as HTMLInputElement).files); + }); - files = [validFiles[0]]; - updateUI(); - }; + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); - if (fileInput && dropZone) { - fileInput.addEventListener('change', (e) => { - handleFileSelect((e.target as HTMLInputElement).files); - }); + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + handleFileSelect(e.dataTransfer?.files ?? null); + }); - dropZone.addEventListener('dragleave', (e) => { - e.preventDefault(); - dropZone.classList.remove('bg-gray-700'); - }); + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } - 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/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/js/utils/helpers.ts b/src/js/utils/helpers.ts index b5afd5f..7ec12f2 100644 --- a/src/js/utils/helpers.ts +++ b/src/js/utils/helpers.ts @@ -29,7 +29,7 @@ export function getStandardPageName(width: any, height: any) { } export function convertPoints(points: any, unit: any) { - let result = 0; + let result: number; switch (unit) { case 'in': result = points / 72; @@ -460,3 +460,22 @@ export function formatRawDate(raw: string): string { } return raw; } + +/** + * Returns a sanitized PDF filename. + * + * The provided filename is processed as follows: + * - Removes a trailing `.pdf` file extension (case-insensitive) + * - Trims leading and trailing whitespace + * - Truncates the name to a maximum of 80 characters + * + * @param filename The original filename (including extension) + * @returns The sanitized filename without the `.pdf` extension, limited to 80 characters + */ +export function getCleanPdfFilename(filename: string): string { + let clean = filename.replace(/\.pdf$/i, '').trim(); + if (clean.length > 80) { + clean = clean.slice(0, 80); + } + return clean; +} diff --git a/src/pages/pdf-to-bmp.html b/src/pages/pdf-to-bmp.html index da12340..31c8c87 100644 --- a/src/pages/pdf-to-bmp.html +++ b/src/pages/pdf-to-bmp.html @@ -160,7 +160,7 @@ diff --git a/src/pages/pdf-to-jpg.html b/src/pages/pdf-to-jpg.html index 9b6aec2..dcee91c 100644 --- a/src/pages/pdf-to-jpg.html +++ b/src/pages/pdf-to-jpg.html @@ -190,7 +190,7 @@ diff --git a/src/pages/pdf-to-png.html b/src/pages/pdf-to-png.html index 964026a..0853293 100644 --- a/src/pages/pdf-to-png.html +++ b/src/pages/pdf-to-png.html @@ -187,7 +187,7 @@ diff --git a/src/pages/pdf-to-tiff.html b/src/pages/pdf-to-tiff.html index 75bec6c..f4292dc 100644 --- a/src/pages/pdf-to-tiff.html +++ b/src/pages/pdf-to-tiff.html @@ -155,7 +155,7 @@
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 @@