From 19425d98f9a735789deee4d4508ef1d9d77ebc79 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Thu, 4 Dec 2025 21:21:34 +0530 Subject: [PATCH] feat: Add JPG to PDF conversion tool with dedicated page and logic. --- src/js/config/tools.ts | 4 +- src/js/handlers/fileHandler.ts | 2 +- src/js/logic/jpg-to-pdf-page.ts | 248 ++++++++++++++++++++++++++++++++ src/js/ui.ts | 150 +++++++++---------- src/pages/jpg-to-pdf.html | 240 +++++++++++++++++++++++++++++++ vite.config.ts | 2 + 6 files changed, 559 insertions(+), 87 deletions(-) create mode 100644 src/js/logic/jpg-to-pdf-page.ts create mode 100644 src/pages/jpg-to-pdf.html diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index d8aadf6..5c88043 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -35,7 +35,7 @@ export const categories = [ 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs', }, { - id: 'jpg-to-pdf', + href: '/src/pages/jpg-to-pdf.html', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.', @@ -185,7 +185,7 @@ export const categories = [ subtitle: 'Convert JPG, PNG, WebP, BMP, TIFF, SVG, HEIC to PDF.', }, { - id: 'jpg-to-pdf', + href: '/src/pages/jpg-to-pdf.html', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.', diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index 3be82b5..9e809a3 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -743,7 +743,7 @@ async function handleMultiFileUpload(toolId) { } } - if (toolId === 'jpg-to-pdf' || toolId === 'png-to-pdf') { + if (toolId === 'png-to-pdf') { const optionsDiv = document.getElementById(`${toolId}-options`); if (optionsDiv) { optionsDiv.classList.remove('hidden'); diff --git a/src/js/logic/jpg-to-pdf-page.ts b/src/js/logic/jpg-to-pdf-page.ts new file mode 100644 index 0000000..523d310 --- /dev/null +++ b/src/js/logic/jpg-to-pdf-page.ts @@ -0,0 +1,248 @@ +import { createIcons, icons } from 'lucide'; +import { showAlert, showLoader, hideLoader } from '../ui.js'; +import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js'; +import { PDFDocument as PDFLibDocument } from 'pdf-lib'; + +let files: File[] = []; + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializePage); +} else { + initializePage(); +} + +function initializePage() { + createIcons({ icons }); + + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const addMoreBtn = document.getElementById('add-more-btn'); + const clearFilesBtn = document.getElementById('clear-files-btn'); + const processBtn = document.getElementById('process-btn'); + + if (fileInput) { + fileInput.addEventListener('change', handleFileUpload); + } + + if (dropZone) { + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); + + dropZone.addEventListener('dragleave', () => { + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + const droppedFiles = e.dataTransfer?.files; + if (droppedFiles && droppedFiles.length > 0) { + handleFiles(droppedFiles); + } + }); + + dropZone.addEventListener('click', () => { + fileInput?.click(); + }); + } + + if (addMoreBtn) { + addMoreBtn.addEventListener('click', () => { + fileInput?.click(); + }); + } + + if (clearFilesBtn) { + clearFilesBtn.addEventListener('click', () => { + files = []; + updateUI(); + }); + } + + if (processBtn) { + processBtn.addEventListener('click', convertToPdf); + } + + document.getElementById('back-to-tools')?.addEventListener('click', () => { + window.location.href = '/'; + }); +} + +function handleFileUpload(e: Event) { + const input = e.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + handleFiles(input.files); + } + input.value = ''; +} + +function handleFiles(newFiles: FileList) { + const validFiles = Array.from(newFiles).filter(file => + file.type === 'image/jpeg' || file.type === 'image/jpg' || file.name.toLowerCase().endsWith('.jpg') || file.name.toLowerCase().endsWith('.jpeg') + ); + + if (validFiles.length < newFiles.length) { + showAlert('Invalid Files', 'Some files were skipped. Only JPG/JPEG images are allowed.'); + } + + if (validFiles.length > 0) { + files = [...files, ...validFiles]; + updateUI(); + } +} + +const resetState = () => { + files = []; + updateUI(); +}; + +function updateUI() { + const fileDisplayArea = document.getElementById('file-display-area'); + const fileControls = document.getElementById('file-controls'); + const optionsDiv = document.getElementById('jpg-to-pdf-options'); + + if (!fileDisplayArea || !fileControls || !optionsDiv) return; + + fileDisplayArea.innerHTML = ''; + + if (files.length > 0) { + fileControls.classList.remove('hidden'); + optionsDiv.classList.remove('hidden'); + + files.forEach((file, index) => { + 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 items-center gap-2 overflow-hidden'; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'truncate font-medium text-gray-200'; + nameSpan.textContent = file.name; + + const sizeSpan = document.createElement('span'); + sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs'; + sizeSpan.textContent = `(${formatBytes(file.size)})`; + + infoContainer.append(nameSpan, sizeSpan); + + const removeBtn = document.createElement('button'); + removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => { + files = files.filter((_, i) => i !== index); + updateUI(); + }; + + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + }); + createIcons({ icons }); + } else { + fileControls.classList.add('hidden'); + optionsDiv.classList.add('hidden'); + } +} + +function sanitizeImageAsJpeg(imageBytes: any) { + return new Promise((resolve, reject) => { + const blob = new Blob([imageBytes]); + const imageUrl = URL.createObjectURL(blob); + const img = new Image(); + + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + + canvas.toBlob( + async (jpegBlob) => { + if (!jpegBlob) { + return reject(new Error('Canvas toBlob conversion failed.')); + } + const arrayBuffer = await jpegBlob.arrayBuffer(); + resolve(new Uint8Array(arrayBuffer)); + }, + 'image/jpeg', + 0.9 + ); + URL.revokeObjectURL(imageUrl); + }; + + img.onerror = () => { + URL.revokeObjectURL(imageUrl); + reject( + new Error( + 'The provided file could not be loaded as an image. It may be corrupted.' + ) + ); + }; + + img.src = imageUrl; + }); +} + +async function convertToPdf() { + if (files.length === 0) { + showAlert('No Files', 'Please select at least one JPG file.'); + return; + } + + showLoader('Creating PDF from JPGs...'); + + try { + const pdfDoc = await PDFLibDocument.create(); + + for (const file of files) { + const originalBytes = await readFileAsArrayBuffer(file); + let jpgImage; + + try { + jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array); + } catch (e) { + showAlert( + 'Warning', + `Direct JPG embedding failed for ${file.name}, attempting to sanitize...` + ); + try { + const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes); + jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array); + } catch (fallbackError) { + console.error( + `Failed to process ${file.name} after sanitization:`, + fallbackError + ); + throw new Error( + `Could not process "${file.name}". The file may be corrupted.` + ); + } + } + + const page = pdfDoc.addPage([jpgImage.width, jpgImage.height]); + page.drawImage(jpgImage, { + x: 0, + y: 0, + width: jpgImage.width, + height: jpgImage.height, + }); + } + + const pdfBytes = await pdfDoc.save(); + downloadFile( + new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), + 'from_jpgs.pdf' + ); + showAlert('Success', 'PDF created successfully!', 'success', () => { + resetState(); + }); + } catch (e: any) { + console.error(e); + showAlert('Conversion Error', e.message); + } finally { + hideLoader(); + } +} diff --git a/src/js/ui.ts b/src/js/ui.ts index f0fab97..62320f0 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -449,7 +449,7 @@ export const toolTemplates = { encrypt: () => `

Encrypt PDF

Add 256 - bit AES password protection to your PDF.

- ${ createFileInputHTML() } + ${createFileInputHTML()}