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: () => `
Add 256 - bit AES password protection to your PDF.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Upload an encrypted PDF and provide its password to create an unlocked version.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Reorder, rotate, or delete pages.Drag and drop pages to reorder them.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Rotate all or specific pages in a PDF document.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Add customizable page numbers to your PDF file.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Convert each page of a PDF file into a high - quality JPG image.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Convert one or more JPG images into a single PDF file.
- ${ createFileInputHTML({ multiple: true, accept: 'image/jpeg', showControls: true }) } -Controls image compression when embedding into PDF
-Use your device's camera to scan documents and save them as a PDF. On desktop, this will open a file picker.
- ${ createFileInputHTML({ accept: 'image/*' }) } + ${createFileInputHTML({ accept: 'image/*' })}Click and drag to select a crop area on any page.You can set different crop areas for each page.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Reduce file size by choosing the compression method that best suits your document.Supports multiple PDFs.
- ${ createFileInputHTML({ multiple: true, showControls: true }) } + ${createFileInputHTML({ multiple: true, showControls: true })}Convert all pages of a PDF to greyscale.This is done by rendering each page, applying a filter, and rebuilding the PDF.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Select multiple PDF files to download them together in a single ZIP archive.
- ${ createFileInputHTML({ multiple: true, showControls: true }) } + ${createFileInputHTML({ multiple: true, showControls: true })}Completely remove identifying metadata from your PDF.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Make PDF forms and annotations non - editable by flattening them.
- ${ createFileInputHTML({ multiple: true, showControls: true }) } + ${createFileInputHTML({ multiple: true, showControls: true })}Convert each page of a PDF file into a high - quality PNG image.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Convert one or more PNG images into a single PDF file.
- ${ createFileInputHTML({ multiple: true, accept: 'image/png', showControls: true }) } + ${createFileInputHTML({ multiple: true, accept: 'image/png', showControls: true })}Convert each page of a PDF file into a modern WebP image.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Convert one or more WebP images into a single PDF file.
- ${ createFileInputHTML({ multiple: true, accept: 'image/webp', showControls: true }) } + ${createFileInputHTML({ multiple: true, accept: 'image/webp', showControls: true })}An all -in -one PDF workspace where you can annotate, draw, highlight, redact, add comments and shapes, take screenshots, and view PDFs.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Remove specific pages or ranges of pages from your PDF file.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Total Pages:
@@ -908,7 +891,7 @@ Right 'add-blank-page': () => `Insert one or more blank pages at a specific position in your document.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Total Pages:
@@ -922,7 +905,7 @@ Right 'extract-pages': () => `Extract specific pages from a PDF into separate files.Your files will download in a ZIP archive.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Total Pages:
@@ -935,7 +918,7 @@ Right 'add-watermark': () => `Apply a text or image watermark to every page of your PDF document.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Add custom text to the top and bottom margins of every page.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Modify passwords and permissions without losing quality.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Convert a PDF's text content into a structured Markdown file.
- ${ createFileInputHTML({ accept: '.pdf' }) } + ${createFileInputHTML({ accept: '.pdf' })}Note: This is a text-focused conversion. Tables and images will not be included.
@@ -1169,7 +1152,7 @@ RightConvert your PDF to a "dark mode" by inverting its colors.This works best on simple text and image documents.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Upload a PDF to view its internal properties, such as Title, Author, and Creation Date.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Flip the order of all pages in your document, making the last page the first.
- ${ createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true }) } + ${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}Convert one or more SVG vector images into a single PDF file.
- ${ createFileInputHTML({ multiple: true, accept: 'image/svg+xml', showControls: true }) } + ${createFileInputHTML({ multiple: true, accept: 'image/svg+xml', showControls: true })}Convert one or more BMP images into a single PDF file.
- ${ createFileInputHTML({ multiple: true, accept: 'image/bmp', showControls: true }) } + ${createFileInputHTML({ multiple: true, accept: 'image/bmp', showControls: true })}Convert one or more HEIC(High Efficiency) images from your iPhone or camera into a single PDF file.
- ${ createFileInputHTML({ multiple: true, accept: '.heic,.heif', showControls: true }) } + ${createFileInputHTML({ multiple: true, accept: '.heic,.heif', showControls: true })}Convert one or more single or multi - page TIFF images into a single PDF file.
- ${ createFileInputHTML({ multiple: true, accept: 'image/tiff', showControls: true }) } + ${createFileInputHTML({ multiple: true, accept: 'image/tiff', showControls: true })}Convert each page of a PDF file into a BMP image.Your files will be downloaded in a ZIP archive.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Convert each page of a PDF file into a high - quality TIFF image.Your files will be downloaded in a ZIP archive.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Choose a method to divide every page of your document into two separate pages.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Upload a PDF to see the precise dimensions, standard size, and orientation of every page.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Combine multiple pages from your PDF onto a single sheet.This is great for creating booklets or proof sheets.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Drag pages to reorder them.Use the icon to duplicate a page or the icon to delete it.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Stitch all pages of your PDF together vertically or horizontally to create one continuous page.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Convert all pages in your PDF to a uniform size.Choose a standard format or define a custom dimension.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Select a new background color for every page of your PDF.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Change the color of dark text in your PDF.This process converts pages to images, so text will not be selectable in the final file.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Selected: None < /span>
@@ -1831,7 +1813,7 @@ Right 'sign-pdf': () => `Upload a PDF to sign it using the built-in PDF.js viewer.Look for the signature / pen tool < /strong> in the toolbar to add your signature.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Select the types of annotations to remove from all pages or a specific range.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Upload a PDF to visually crop one or more pages.This tool offers a live preview and two distinct cropping modes.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Split pages into multiple smaller sheets to print as a poster.Navigate the preview and see the grid update based on your settings.
- ${ createFileInputHTML() } + ${createFileInputHTML()}Automatically detect and remove blank or nearly blank pages from your PDF.Adjust the sensitivity to control what is considered "blank".
- ${ createFileInputHTML() } + ${createFileInputHTML()}Combine pages from 2 or more documents, alternating between them.Drag the files to set the mixing order(e.g., Page 1 from Doc A, Page 1 from Doc B, Page 2 from Doc A, Page 2 from Doc B, etc.).
- ${ createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true }) } + ${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}Optimize multiple PDFs for faster loading over the web.Files will be downloaded in a ZIP archive.
- ${ createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true }) } + ${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}First, upload the PDF document you want to add files to.
- ${ createFileInputHTML({ accept: 'application/pdf' }) } + ${createFileInputHTML({ accept: 'application/pdf' })}Extract all embedded files from one or more PDFs.All attachments will be downloaded in a ZIP archive.
- ${ createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true }) } + ${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}View, remove, or replace attachments in your PDF.
- ${ createFileInputHTML({ accept: 'application/pdf' }) } + ${createFileInputHTML({ accept: 'application/pdf' })}Remove potentially sensitive or unnecessary information from your PDF before sharing.Select the items you want to remove.
- ${ createFileInputHTML() } + ${createFileInputHTML()} Embedded Fonts `
Remove PDF Restrictions
Remove security restrictions and unlock PDF permissions for editing and printing.
- ${ createFileInputHTML() }
+ ${createFileInputHTML()}
diff --git a/src/pages/jpg-to-pdf.html b/src/pages/jpg-to-pdf.html
new file mode 100644
index 0000000..fae13a1
--- /dev/null
+++ b/src/pages/jpg-to-pdf.html
@@ -0,0 +1,240 @@
+
+
+
+
+
+
+ JPG to PDF - BentoPDF
+
+
+
+
+
+
+
+
+
+
+
+
+ JPG to PDF
+
+ Convert one or more JPG images into a single PDF file.
+
+
+
+
+
+
+ Click to select a file or
+ drag and
+ drop
+ JPG Images
+ Your files never leave your device.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Controls image compression when embedding into PDF
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Processing...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index dfe9c92..d91bcdd 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -66,6 +66,8 @@ export default defineConfig(({ mode }) => ({
'split-pdf': resolve(__dirname, 'src/pages/split-pdf.html'),
'compress-pdf': resolve(__dirname, 'src/pages/compress-pdf.html'),
'edit-pdf': resolve(__dirname, 'src/pages/edit-pdf.html'),
+ 'jpg-to-pdf': resolve(__dirname, 'src/pages/jpg-to-pdf.html'),
+
},
},
},