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/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 @@