From 0999163d3ca9e63f27915da8a296b0910d767fec Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Sat, 8 Nov 2025 15:19:56 +0530 Subject: [PATCH] feat: add JSON to PDF and PDF to JSON conversion tools - Introduced new pages and logic for converting JSON files to PDF and vice versa. - Implemented web workers for handling conversion processes in the background. - Updated Vite configuration to include new HTML pages for the conversion tools. - Enhanced user interface with file upload sections and status messages for conversion progress. - Added TypeScript definitions for worker communication and response handling. --- public/workers/json-to-pdf.worker.d.ts | 20 ++++ public/workers/json-to-pdf.worker.js | 63 ++++++++++ public/workers/pdf-to-json.worker.d.ts | 20 ++++ public/workers/pdf-to-json.worker.js | 51 ++++++++ src/js/config/tools.ts | 12 ++ src/js/logic/json-to-pdf.ts | 159 +++++++++++++++++++++++++ src/js/logic/pdf-to-json.ts | 155 ++++++++++++++++++++++++ src/js/logic/table-of-contents.ts | 30 +---- src/pages/json-to-pdf.html | 113 ++++++++++++++++++ src/pages/pdf-to-json.html | 108 +++++++++++++++++ vite.config.ts | 2 + 11 files changed, 706 insertions(+), 27 deletions(-) create mode 100644 public/workers/json-to-pdf.worker.d.ts create mode 100644 public/workers/json-to-pdf.worker.js create mode 100644 public/workers/pdf-to-json.worker.d.ts create mode 100644 public/workers/pdf-to-json.worker.js create mode 100644 src/js/logic/json-to-pdf.ts create mode 100644 src/js/logic/pdf-to-json.ts create mode 100644 src/pages/json-to-pdf.html create mode 100644 src/pages/pdf-to-json.html diff --git a/public/workers/json-to-pdf.worker.d.ts b/public/workers/json-to-pdf.worker.d.ts new file mode 100644 index 0000000..2da9dc4 --- /dev/null +++ b/public/workers/json-to-pdf.worker.d.ts @@ -0,0 +1,20 @@ +declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf; + +interface ConvertJSONToPDFMessage { + command: 'convert'; + fileBuffers: ArrayBuffer[]; + fileNames: string[]; +} + +interface JSONToPDFSuccessResponse { + status: 'success'; + pdfFiles: Array<{ name: string; data: ArrayBuffer }>; +} + +interface JSONToPDFErrorResponse { + status: 'error'; + message: string; +} + +type JSONToPDFResponse = JSONToPDFSuccessResponse | JSONToPDFErrorResponse; + diff --git a/public/workers/json-to-pdf.worker.js b/public/workers/json-to-pdf.worker.js new file mode 100644 index 0000000..b847d83 --- /dev/null +++ b/public/workers/json-to-pdf.worker.js @@ -0,0 +1,63 @@ +self.importScripts('/coherentpdf.browser.min.js'); + +function convertJSONsToPDFInWorker(fileBuffers, fileNames) { + try { + const pdfFiles = []; + const transferBuffers = []; + + for (let i = 0; i < fileBuffers.length; i++) { + const buffer = fileBuffers[i]; + const fileName = fileNames[i]; + const uint8Array = new Uint8Array(buffer); + + let pdf; + try { + pdf = coherentpdf.fromJSONMemory(uint8Array); + } catch (error) { + const errorMsg = error && error.message + ? error.message + : 'Unknown error'; + throw new Error( + `Failed to convert "${fileName}" to PDF. ` + + `The JSON file must be in the format produced by cpdf's outputJSONMemory. ` + + `Error: ${errorMsg}` + ); + } + + const pdfData = coherentpdf.toMemory(pdf, false, false); + + const pdfBuffer = pdfData.buffer.slice(0); + pdfFiles.push({ + name: fileName, + data: pdfBuffer, + }); + transferBuffers.push(pdfBuffer); + + coherentpdf.deletePdf(pdf); + } + + self.postMessage( + { + status: 'success', + pdfFiles: pdfFiles, + }, + transferBuffers + ); + } catch (error) { + self.postMessage({ + status: 'error', + message: + error instanceof Error + ? error.message + : 'Unknown error during JSON to PDF conversion.', + }); + return; + } +} + +self.onmessage = (e) => { + if (e.data.command === 'convert') { + convertJSONsToPDFInWorker(e.data.fileBuffers, e.data.fileNames); + } +}; + diff --git a/public/workers/pdf-to-json.worker.d.ts b/public/workers/pdf-to-json.worker.d.ts new file mode 100644 index 0000000..fe50026 --- /dev/null +++ b/public/workers/pdf-to-json.worker.d.ts @@ -0,0 +1,20 @@ +declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf; + +interface ConvertPDFToJSONMessage { + command: 'convert'; + fileBuffers: ArrayBuffer[]; + fileNames: string[]; +} + +interface PDFToJSONSuccessResponse { + status: 'success'; + jsonFiles: Array<{ name: string; data: ArrayBuffer }>; +} + +interface PDFToJSONErrorResponse { + status: 'error'; + message: string; +} + +type PDFToJSONResponse = PDFToJSONSuccessResponse | PDFToJSONErrorResponse; + diff --git a/public/workers/pdf-to-json.worker.js b/public/workers/pdf-to-json.worker.js new file mode 100644 index 0000000..b5e7d22 --- /dev/null +++ b/public/workers/pdf-to-json.worker.js @@ -0,0 +1,51 @@ +self.importScripts('/coherentpdf.browser.min.js'); + +function convertPDFsToJSONInWorker(fileBuffers, fileNames) { + try { + const jsonFiles = []; + const transferBuffers = []; + + for (let i = 0; i < fileBuffers.length; i++) { + const buffer = fileBuffers[i]; + const fileName = fileNames[i]; + const uint8Array = new Uint8Array(buffer); + const pdf = coherentpdf.fromMemory(uint8Array, ''); + + //TODO:@ALAM -> add options for users to select these settings + // parse_content: true, no_stream_data: false, decompress_streams: false + const jsonData = coherentpdf.outputJSONMemory(true, false, false, pdf); + + const jsonBuffer = jsonData.buffer.slice(0); + jsonFiles.push({ + name: fileName, + data: jsonBuffer, + }); + transferBuffers.push(jsonBuffer); + + coherentpdf.deletePdf(pdf); + } + + self.postMessage( + { + status: 'success', + jsonFiles: jsonFiles, + }, + transferBuffers + ); + } catch (error) { + self.postMessage({ + status: 'error', + message: + error instanceof Error + ? error.message + : 'Unknown error during PDF to JSON conversion.', + }); + } +} + +self.onmessage = (e) => { + if (e.data.command === 'convert') { + convertPDFsToJSONInWorker(e.data.fileBuffers, e.data.fileNames); + } +}; + diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index a85fb58..19c7b90 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -214,6 +214,12 @@ export const categories = [ icon: 'file-pen', subtitle: 'Convert a plain text file into a PDF.', }, + { + href: '/src/pages/json-to-pdf.html', + name: 'JSON to PDF', + icon: 'file-code', + subtitle: 'Convert JSON files to PDF format.', + }, // { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' }, // { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' }, // { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' }, @@ -258,6 +264,12 @@ export const categories = [ icon: 'palette', subtitle: 'Convert all colors to black and white.', }, + { + href: '/src/pages/pdf-to-json.html', + name: 'PDF to JSON', + icon: 'file-code', + subtitle: 'Convert PDF files to JSON format.', + }, // { id: 'pdf-to-markdown', name: 'PDF to Markdown', icon: 'file-pen', subtitle: 'Extract text into a Markdown file.' }, ], }, diff --git a/src/js/logic/json-to-pdf.ts b/src/js/logic/json-to-pdf.ts new file mode 100644 index 0000000..f2217d4 --- /dev/null +++ b/src/js/logic/json-to-pdf.ts @@ -0,0 +1,159 @@ +import JSZip from 'jszip' +import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers'; + +const worker = new Worker(new URL('/workers/json-to-pdf.worker.js', import.meta.url)); + +let selectedFiles: File[] = [] + +const jsonFilesInput = document.getElementById('jsonFiles') as HTMLInputElement +const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement +const statusMessage = document.getElementById('status-message') as HTMLDivElement +const fileListDiv = document.getElementById('fileList') as HTMLDivElement +const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement + +function showStatus( + message: string, + type: 'success' | 'error' | 'info' = 'info' +) { + statusMessage.textContent = message + statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${ + type === 'success' + ? 'bg-green-900 text-green-200' + : type === 'error' + ? 'bg-red-900 text-red-200' + : 'bg-blue-900 text-blue-200' + }` + statusMessage.classList.remove('hidden') +} + +function hideStatus() { + statusMessage.classList.add('hidden') +} + +function updateFileList() { + fileListDiv.innerHTML = '' + if (selectedFiles.length === 0) { + fileListDiv.classList.add('hidden') + return + } + + fileListDiv.classList.remove('hidden') + selectedFiles.forEach((file) => { + const fileDiv = document.createElement('div') + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm mb-2' + + 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 ml-4 text-gray-400' + sizeSpan.textContent = formatBytes(file.size) + + fileDiv.append(nameSpan, sizeSpan) + fileListDiv.appendChild(fileDiv) + }) +} + +jsonFilesInput.addEventListener('change', (e) => { + const target = e.target as HTMLInputElement + if (target.files && target.files.length > 0) { + selectedFiles = Array.from(target.files) + convertBtn.disabled = selectedFiles.length === 0 + updateFileList() + + if (selectedFiles.length === 0) { + showStatus('Please select at least 1 JSON file', 'info') + } else { + showStatus(`${selectedFiles.length} file(s) selected. Ready to convert!`, 'info') + } + } +}) + +async function convertJSONsToPDF() { + if (selectedFiles.length === 0) { + showStatus('Please select at least 1 JSON file', 'error') + return + } + + try { + convertBtn.disabled = true + showStatus('Reading files (Main Thread)...', 'info') + + const fileBuffers = await Promise.all( + selectedFiles.map(file => readFileAsArrayBuffer(file)) + ) + + showStatus('Converting JSONs to PDFs...', 'info') + + worker.postMessage({ + command: 'convert', + fileBuffers: fileBuffers, + fileNames: selectedFiles.map(f => f.name) + }, fileBuffers); + + } catch (error) { + console.error('Error reading files:', error) + showStatus(`❌ Error reading files: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error') + convertBtn.disabled = false + } +} + +worker.onmessage = async (e: MessageEvent) => { + convertBtn.disabled = false; + + if (e.data.status === 'success') { + const pdfFiles = e.data.pdfFiles as Array<{ name: string, data: ArrayBuffer }>; + + try { + showStatus('Creating ZIP file...', 'info') + + const zip = new JSZip() + pdfFiles.forEach(({ name, data }) => { + const pdfName = name.replace(/\.json$/i, '.pdf') + const uint8Array = new Uint8Array(data) + zip.file(pdfName, uint8Array) + }) + + const zipBlob = await zip.generateAsync({ type: 'blob' }) + const url = URL.createObjectURL(zipBlob) + const a = document.createElement('a') + a.href = url + a.download = 'jsons-to-pdf.zip' + downloadFile(zipBlob, 'jsons-to-pdf.zip') + + showStatus('✅ JSONs converted to PDF successfully! ZIP download started.', 'success') + + selectedFiles = [] + jsonFilesInput.value = '' + fileListDiv.innerHTML = '' + fileListDiv.classList.add('hidden') + convertBtn.disabled = true + + setTimeout(() => { + hideStatus() + }, 3000) + + } catch (error) { + console.error('Error creating ZIP:', error) + showStatus(`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error') + } + + } else if (e.data.status === 'error') { + const errorMessage = e.data.message || 'Unknown error occurred in worker.'; + console.error('Worker Error:', errorMessage); + showStatus(`❌ Worker Error: ${errorMessage}`, 'error'); + } +}; + +if (backToToolsBtn) { + backToToolsBtn.addEventListener('click', () => { + window.location.href = '../../index.html#tools-header' + }) +} + +convertBtn.addEventListener('click', convertJSONsToPDF) + +// Initialize +showStatus('Select JSON files to get started', 'info') + diff --git a/src/js/logic/pdf-to-json.ts b/src/js/logic/pdf-to-json.ts new file mode 100644 index 0000000..9b49ed4 --- /dev/null +++ b/src/js/logic/pdf-to-json.ts @@ -0,0 +1,155 @@ +import JSZip from 'jszip' +import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers'; + +const worker = new Worker(new URL('/workers/pdf-to-json.worker.js', import.meta.url)); + +let selectedFiles: File[] = [] + +const pdfFilesInput = document.getElementById('pdfFiles') as HTMLInputElement +const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement +const statusMessage = document.getElementById('status-message') as HTMLDivElement +const fileListDiv = document.getElementById('fileList') as HTMLDivElement +const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement + +function showStatus( + message: string, + type: 'success' | 'error' | 'info' = 'info' +) { + statusMessage.textContent = message + statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${ + type === 'success' + ? 'bg-green-900 text-green-200' + : type === 'error' + ? 'bg-red-900 text-red-200' + : 'bg-blue-900 text-blue-200' + }` + statusMessage.classList.remove('hidden') +} + +function hideStatus() { + statusMessage.classList.add('hidden') +} + +function updateFileList() { + fileListDiv.innerHTML = '' + if (selectedFiles.length === 0) { + fileListDiv.classList.add('hidden') + return + } + + fileListDiv.classList.remove('hidden') + selectedFiles.forEach((file) => { + const fileDiv = document.createElement('div') + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm mb-2' + + 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 ml-4 text-gray-400' + sizeSpan.textContent = formatBytes(file.size) + + fileDiv.append(nameSpan, sizeSpan) + fileListDiv.appendChild(fileDiv) + }) +} + +pdfFilesInput.addEventListener('change', (e) => { + const target = e.target as HTMLInputElement + if (target.files && target.files.length > 0) { + selectedFiles = Array.from(target.files) + convertBtn.disabled = selectedFiles.length === 0 + updateFileList() + + if (selectedFiles.length === 0) { + showStatus('Please select at least 1 PDF file', 'info') + } else { + showStatus(`${selectedFiles.length} file(s) selected. Ready to convert!`, 'info') + } + } +}) + +async function convertPDFsToJSON() { + if (selectedFiles.length === 0) { + showStatus('Please select at least 1 PDF file', 'error') + return + } + + try { + convertBtn.disabled = true + showStatus('Reading files (Main Thread)...', 'info') + + const fileBuffers = await Promise.all( + selectedFiles.map(file => readFileAsArrayBuffer(file)) + ) + + showStatus('Converting PDFs to JSON..', 'info') + + worker.postMessage({ + command: 'convert', + fileBuffers: fileBuffers, + fileNames: selectedFiles.map(f => f.name) + }, fileBuffers); + + } catch (error) { + console.error('Error reading files:', error) + showStatus(`❌ Error reading files: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error') + convertBtn.disabled = false + } +} + +worker.onmessage = async (e: MessageEvent) => { + convertBtn.disabled = false; + + if (e.data.status === 'success') { + const jsonFiles = e.data.jsonFiles as Array<{ name: string, data: ArrayBuffer }>; + + try { + showStatus('Creating ZIP file...', 'info') + + const zip = new JSZip() + jsonFiles.forEach(({ name, data }) => { + const jsonName = name.replace(/\.pdf$/i, '.json') + const uint8Array = new Uint8Array(data) + zip.file(jsonName, uint8Array) + }) + + const zipBlob = await zip.generateAsync({ type: 'blob' }) + downloadFile(zipBlob, 'pdfs-to-json.zip') + + showStatus('✅ PDFs converted to JSON successfully! ZIP download started.', 'success') + + selectedFiles = [] + pdfFilesInput.value = '' + fileListDiv.innerHTML = '' + fileListDiv.classList.add('hidden') + convertBtn.disabled = true + + setTimeout(() => { + hideStatus() + }, 3000) + + } catch (error) { + console.error('Error creating ZIP:', error) + showStatus(`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error') + } + + } else if (e.data.status === 'error') { + const errorMessage = e.data.message || 'Unknown error occurred in worker.'; + console.error('Worker Error:', errorMessage); + showStatus(`❌ Worker Error: ${errorMessage}`, 'error'); + } +}; + +if (backToToolsBtn) { + backToToolsBtn.addEventListener('click', () => { + window.location.href = '../../index.html#tools-header' + }) +} + +convertBtn.addEventListener('click', convertPDFsToJSON) + +// Initialize +showStatus('Select PDF files to get started', 'info') + diff --git a/src/js/logic/table-of-contents.ts b/src/js/logic/table-of-contents.ts index 8df7579..8f3313a 100644 --- a/src/js/logic/table-of-contents.ts +++ b/src/js/logic/table-of-contents.ts @@ -1,3 +1,5 @@ +import { downloadFile, formatBytes } from "../utils/helpers"; + const worker = new Worker('/workers/table-of-contents.worker.js'); let pdfFile: File | null = null; @@ -46,7 +48,6 @@ interface TOCErrorResponse { type TOCWorkerResponse = TOCSuccessResponse | TOCErrorResponse; -// Show status message function showStatus( message: string, type: 'success' | 'error' | 'info' = 'info' @@ -62,21 +63,10 @@ function showStatus( statusMessage.classList.remove('hidden'); } -// Hide status message function hideStatus() { statusMessage.classList.add('hidden'); } -// Format bytes helper -function formatBytes(bytes: number): string { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; -} - -// Render file display function renderFileDisplay(file: File) { fileDisplayArea.innerHTML = ''; fileDisplayArea.classList.remove('hidden'); @@ -97,7 +87,6 @@ function renderFileDisplay(file: File) { fileDisplayArea.appendChild(fileDiv); } -// Handle file selection function handleFileSelect(file: File) { if (file.type !== 'application/pdf') { showStatus('Please select a PDF file.', 'error'); @@ -107,10 +96,8 @@ function handleFileSelect(file: File) { pdfFile = file; generateBtn.disabled = false; renderFileDisplay(file); - // showStatus(`File selected: ${file.name}`, 'success'); } -// Drag and drop handlers dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-blue-500'); @@ -136,7 +123,6 @@ fileInput.addEventListener('change', (e) => { } }); -// Generate table of contents async function generateTableOfContents() { if (!pdfFile) { showStatus('Please select a PDF file first.', 'error'); @@ -176,7 +162,6 @@ async function generateTableOfContents() { } } -// Handle messages from worker worker.onmessage = (e: MessageEvent) => { generateBtn.disabled = false; @@ -185,15 +170,7 @@ worker.onmessage = (e: MessageEvent) => { const pdfBytes = new Uint8Array(pdfBytesBuffer); const blob = new Blob([pdfBytes], { type: 'application/pdf' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = - pdfFile?.name.replace('.pdf', '_with_toc.pdf') || 'output_with_toc.pdf'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + downloadFile(blob, pdfFile?.name.replace('.pdf', '_with_toc.pdf') || 'output_with_toc.pdf'); showStatus( 'Table of contents generated successfully! Download started.', @@ -219,7 +196,6 @@ worker.onerror = (error) => { generateBtn.disabled = false; }; -// Back to tools button if (backToToolsBtn) { backToToolsBtn.addEventListener('click', () => { window.location.href = '../../index.html#tools-header'; diff --git a/src/pages/json-to-pdf.html b/src/pages/json-to-pdf.html new file mode 100644 index 0000000..9f9b2de --- /dev/null +++ b/src/pages/json-to-pdf.html @@ -0,0 +1,113 @@ + + + + + + JSON to PDF Converter - BentoPDF + + + + + + +
+
+ +

JSON to PDF Converter

+

+ Upload multiple JSON files to convert them all to PDF format. Files will be downloaded as a ZIP archive. +

+
+

+ Note: Only JSON files created by the PDF-to-JSON converter tool are supported. Standard JSON files from other tools will not work. +

+
+ +
+
+
+ +

+ Click to select files or drag and drop +

+

Multiple JSON files

+

Your files never leave your device.

+
+ +
+ + + + + +
+ + + +
+
+ + + + + + + diff --git a/src/pages/pdf-to-json.html b/src/pages/pdf-to-json.html new file mode 100644 index 0000000..f3f2b6b --- /dev/null +++ b/src/pages/pdf-to-json.html @@ -0,0 +1,108 @@ + + + + + + PDF to JSON Converter - BentoPDF + + + + + + +
+
+ +

PDF to JSON Converter

+

+ Upload multiple PDF files to convert them all to JSON format. Files will be downloaded as a ZIP archive. +

+ +
+
+
+ +

+ Click to select files or drag and drop +

+

Multiple PDF files

+

Your files never leave your device.

+
+ +
+ + + + + +
+ + + +
+
+ + + + + + + diff --git a/vite.config.ts b/vite.config.ts index 7080fa0..8534013 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -48,6 +48,8 @@ export default defineConfig(({ mode }) => ({ __dirname, 'src/pages/table-of-contents.html' ), + 'pdf-to-json': resolve(__dirname, 'src/pages/pdf-to-json.html'), + 'json-to-pdf': resolve(__dirname, 'src/pages/json-to-pdf.html'), }, }, },