Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) => <div key={i}>{i+1}</div>)}`) 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.
49 changes: 39 additions & 10 deletions src/services/pdf/pdfImageConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Comment on lines +45 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/services/pdf/pdfImageConverter.js | head -60

Repository: Sumit-5002/Anyfileforge

Length of output: 2873


🏁 Script executed:

rg -A 15 "pdfToJpg" src/features/tools/runners/pdf/PdfToJpgTool.jsx | head -40

Repository: Sumit-5002/Anyfileforge

Length of output: 536


🏁 Script executed:

rg -B 5 -A 10 "zip.file" src/services/pdf/pdfImageConverter.js

Repository: Sumit-5002/Anyfileforge

Length of output: 647


🏁 Script executed:

cat src/services/pdf/pdfImageConverter.js | tail -20

Repository: 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 5

Repository: Sumit-5002/Anyfileforge

Length of output: 9984


Add proper error handling to canvas.toBlob to reject on null blob or encoding failures.

Lines 46–51 silently accept null from toBlob callback and pass it to zip.file(), which creates empty 0-byte entries instead of rejecting the promise. This causes silent data corruption in the output ZIP. Similar code in src/services/pdf/pdfSecurity.js and src/features/tools/runners/image/HtmlToImageTool.jsx validates the blob and rejects on null; adopt the same pattern here.

💡 Suggested fix
-            return new Promise((resolve) => {
+            return new Promise((resolve, reject) => {
                 canvas.toBlob((blob) => {
-                    zip.file(`page_${pageNum}.jpg`, blob);
-                    resolve();
+                    if (!blob) {
+                        reject(new Error(`Failed to encode page ${pageNum} as JPEG`));
+                        return;
+                    }
+
+                    try {
+                        zip.file(`page_${pageNum}.jpg`, blob);
+                        resolve();
+                    } catch (error) {
+                        reject(error);
+                    }
                 }, 'image/jpeg', 0.9);
             });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/pdf/pdfImageConverter.js` around lines 45 - 51, The promise
created around canvas.toBlob in pdfImageConverter.js must reject when toBlob
yields a null blob or fails instead of silently adding an empty entry; update
the anonymous callback passed to canvas.toBlob to check if blob is non-null and
call reject(new Error(...)) on null or on any encoding failure, otherwise call
zip.file(`page_${pageNum}.jpg`, blob) and resolve(); mirror the
null-check/reject pattern used in src/services/pdf/pdfSecurity.js and
src/features/tools/runners/image/HtmlToImageTool.jsx so consumers can handle
errors properly.

}));

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep progress monotonic through the ZIP phase.

Line 56 can already emit 100 on the last chunk, so Line 60 sends the consumer in src/features/tools/runners/pdf/PdfToJpgTool.jsx:19-33 backward to 95 right before completion. Reserve the last few percent for zipping so the UI only moves forward.

💡 Minimal fix
-            onProgress((completed / numPages) * 100);
+            onProgress((completed / numPages) * 95);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/pdf/pdfImageConverter.js` around lines 54 - 60, The progress
callback can move backward because the loop may emit 100 then the ZIP phase
unconditionally calls onProgress(95); fix by tracking the last emitted percent
inside the loop (compute lastProgress = (Math.min(i + CHUNK_SIZE - 1, numPages)
/ numPages) * 100 each iteration when onProgress is called) and after the loop
call onProgress(Math.max(lastProgress || 0, 95)) instead of unconditionally
onProgress(95), ensuring onProgress, CHUNK_SIZE, numPages and the loop index i
are used to compute and compare the last emitted percent so progress is
monotonic.

const blob = await zip.generateAsync({ type: 'blob' });
if (onProgress) onProgress(100);
downloadBlob(blob, file.name.replace('.pdf', '_images.zip'));
Expand Down
Loading