-
Notifications
You must be signed in to change notification settings - Fork 0
⚡ Bolt: Parallelized PDF to JPG conversion with Blob optimization #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Comment on lines
+54
to
+60
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep progress monotonic through the ZIP phase. Line 56 can already emit 💡 Minimal fix- onProgress((completed / numPages) * 100);
+ onProgress((completed / numPages) * 95);🤖 Prompt for AI Agents |
||
| const blob = await zip.generateAsync({ type: 'blob' }); | ||
| if (onProgress) onProgress(100); | ||
| downloadBlob(blob, file.name.replace('.pdf', '_images.zip')); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n src/services/pdf/pdfImageConverter.js | head -60Repository: Sumit-5002/Anyfileforge
Length of output: 2873
🏁 Script executed:
Repository: Sumit-5002/Anyfileforge
Length of output: 536
🏁 Script executed:
rg -B 5 -A 10 "zip.file" src/services/pdf/pdfImageConverter.jsRepository: Sumit-5002/Anyfileforge
Length of output: 647
🏁 Script executed:
cat src/services/pdf/pdfImageConverter.js | tail -20Repository: Sumit-5002/Anyfileforge
Length of output: 789
🌐 Web query:
HTMLCanvasElement.toBlob callback null specification💡 Result:
The HTMLCanvasElement.toBlob method invokes a BlobCallback with a Blob? (nullable Blob) parameter. The specification explicitly defines the callback as callback BlobCallback = undefined (Blob? blob);, allowing null to be passed to the callback if the image cannot be created for any reason, such as when the serialization result is null (e.g., unsupported type, zero-sized canvas, or other failure conditions). MDN documentation confirms: "null may be passed if the image cannot be created for any reason." The method processes asynchronously: it queues a task on the "canvas blob serialization task source" to invoke the callback with the result, which is set to a new Blob if non-null, or remains null otherwise. Developers must handle the null case in the callback, as seen in TypeScript usage where the parameter is typed as Blob | null.
Citations:
🌐 Web query:
JSZip file method null blob handling💡 Result:
In JSZip, the file(name, data, [options]) method explicitly supports passing null as the data parameter when creating directories. The official documentation provides this example: zip.file("folder/", null, { dir: true }); This creates an empty directory entry named "folder/" in the zip archive. The dir: true option is required to mark it as a directory rather than a regular file with null/empty content. Passing null for regular files (without dir: true) is not explicitly documented as supported and may result in an empty file (0 bytes) rather than an error, as JSZip handles various data types including promises, strings, ArrayBuffers, Blobs, etc., and converts them appropriately during generation. However, for Blob specifically, null is not a valid Blob constructor argument, so using null without dir: true likely creates an empty regular file. No errors or special null Blob handling (like rejection) are mentioned in the docs or issues for the file method. The folder(name) method always succeeds (creates if missing), unlike some type definitions suggesting null returns. Sources confirm this behavior in official API docs (file method, examples) and GitHub repo.
Citations:
🏁 Script executed:
rg "toBlob" src/ -B 2 -A 5Repository: Sumit-5002/Anyfileforge
Length of output: 9984
Add proper error handling to
canvas.toBlobto reject on null blob or encoding failures.Lines 46–51 silently accept null from
toBlobcallback and pass it tozip.file(), which creates empty 0-byte entries instead of rejecting the promise. This causes silent data corruption in the output ZIP. Similar code insrc/services/pdf/pdfSecurity.jsandsrc/features/tools/runners/image/HtmlToImageTool.jsxvalidates the blob and rejects on null; adopt the same pattern here.💡 Suggested fix
🤖 Prompt for AI Agents