From 388f2ee19584ad171027a9e6d8d523feb8a1a54f Mon Sep 17 00:00:00 2001 From: Sumit-5002 <173078713+Sumit-5002@users.noreply.github.com> Date: Sat, 28 Mar 2026 06:24:06 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Parallelized=20PDF=20to=20J?= =?UTF-8?q?PG=20conversion=20with=20Blob=20optimization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored the `pdfToJpg` service to use chunked parallel rendering (concurrency of 4) and replaced `canvas.toDataURL()` with `canvas.toBlob()`. Improvements: - Reduces total conversion time for multi-page PDFs by processing pages in parallel chunks. - Eliminates CPU and memory overhead of Base64 encoding by using binary Blobs directly with JSZip. - Maintains accurate progress reporting during concurrent processing. --- .jules/bolt.md | 4 +++ src/services/pdf/pdfImageConverter.js | 49 +++++++++++++++++++++------ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index b732f64..05d9d3c 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -25,3 +25,7 @@ ## 2026-03-26 - Optimized Line Number Rendering in TextToolRunner **Learning:** Rendering line numbers by mapping over an array of indices to create individual `div` elements (e.g., `{lines.map((_, i) =>
{i+1}
)}`) creates O(N) DOM nodes. This becomes a significant performance bottleneck for large text files, increasing memory usage and React reconciliation time. Using a single `div` with `white-space: pre` and a newline-separated string reduces DOM nodes to O(1) per editor. **Action:** Always prefer a single pre-formatted string for line numbers or similar repeating indicators instead of individual DOM nodes per line to ensure consistent performance with large inputs. + +## 2026-03-28 - Parallelized and Blob-Optimized PDF to JPG Conversion +**Learning:** Sequential page rendering in `pdfToJpg` creates a linear bottleneck for large PDFs. Furthermore, using `canvas.toDataURL()` incurs significant overhead due to Base64 encoding/decoding, which is unnecessary since `JSZip` can handle Blobs directly. +**Action:** Implement chunked parallel rendering (concurrency of 4) for PDF pages and switch from `toDataURL()` to `toBlob()` to minimize CPU and memory usage during batch image extraction. diff --git a/src/services/pdf/pdfImageConverter.js b/src/services/pdf/pdfImageConverter.js index 6fe8add..8b08ea9 100644 --- a/src/services/pdf/pdfImageConverter.js +++ b/src/services/pdf/pdfImageConverter.js @@ -15,20 +15,49 @@ export const imagesToPDF = async (files, onProgress) => { return pdf.save(); }; +/** + * Converts a PDF into individual JPG images and downloads them as a ZIP file. + * Optimized with chunked parallel rendering and Blob usage (Bolt ⚡). + */ export const pdfToJpg = async (file, onProgress) => { const pdf = await pdfjs.getDocument({ data: await file.arrayBuffer() }).promise; const zip = new JSZip(); - for (let i = 1; i <= pdf.numPages; i++) { - if (onProgress) onProgress((i / pdf.numPages) * 100); - const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale: 2.0 }); - const canvas = document.createElement('canvas'); - canvas.width = viewport.width; canvas.height = viewport.height; - await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise; - const b64 = canvas.toDataURL('image/jpeg', 0.9).split(',')[1]; - zip.file(`page_${i}.jpg`, b64, { base64: true }); + const numPages = pdf.numPages; + const CHUNK_SIZE = 4; + + for (let i = 1; i <= numPages; i += CHUNK_SIZE) { + const chunk = []; + for (let j = i; j < i + CHUNK_SIZE && j <= numPages; j++) { + chunk.push(j); + } + + // Render pages in parallel chunks to optimize performance while controlling memory usage + await Promise.all(chunk.map(async (pageNum) => { + const page = await pdf.getPage(pageNum); + const viewport = page.getViewport({ scale: 2.0 }); + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.width = viewport.width; + canvas.height = viewport.height; + + await page.render({ canvasContext: context, viewport }).promise; + + // Use toBlob instead of toDataURL to avoid Base64 encoding overhead (Bolt ⚡) + return new Promise((resolve) => { + canvas.toBlob((blob) => { + zip.file(`page_${pageNum}.jpg`, blob); + resolve(); + }, 'image/jpeg', 0.9); + }); + })); + + if (onProgress) { + const completed = Math.min(i + CHUNK_SIZE - 1, numPages); + onProgress((completed / numPages) * 100); + } } - if (onProgress) onProgress(95); // Zipping takes a bit + + if (onProgress) onProgress(95); // Zipping phase const blob = await zip.generateAsync({ type: 'blob' }); if (onProgress) onProgress(100); downloadBlob(blob, file.name.replace('.pdf', '_images.zip'));