Compare commits

..
Author SHA1 Message Date
dependabot[bot] f223d1edbe build(deps): bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-17 21:34:34 +00:00
40 changed files with 572 additions and 1881 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: "20" node-version: "20"
+1 -1
View File
@@ -180,7 +180,7 @@ The easiest way to self-host is to download the pre-built distribution file from
**Navigate to the extracted Folder:** **Navigate to the extracted Folder:**
```bash ```bash
# Navigate to the extracted folder # Navigate to the extracted folder
cd dist-1.7.3 # Replace with your version cd dist-1.6.2 # Replace with your version
``` ```
**Serve with Python:** **Serve with Python:**
+2 -2
View File
@@ -261,7 +261,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -331,7 +331,7 @@
</footer> </footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+2 -2
View File
@@ -133,7 +133,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -203,7 +203,7 @@
</footer> </footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+2 -2
View File
@@ -319,7 +319,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -389,7 +389,7 @@
</footer> </footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+1 -2
View File
@@ -594,7 +594,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -664,7 +664,6 @@
</footer> </footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/main.ts"></script> <script type="module" src="src/js/main.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
-2
View File
@@ -34,7 +34,5 @@ http {
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always; add_header X-XSS-Protection "1; mode=block" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
} }
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "bento-pdf", "name": "bento-pdf",
"version": "1.7.3", "version": "1.6.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bento-pdf", "name": "bento-pdf",
"version": "1.7.3", "version": "1.6.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@fontsource/cedarville-cursive": "^5.2.7", "@fontsource/cedarville-cursive": "^5.2.7",
+2 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "bento-pdf", "name": "bento-pdf",
"private": true, "private": true,
"version": "1.7.6", "version": "1.7.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -16,12 +16,10 @@
"test:watch": "vitest watch", "test:watch": "vitest watch",
"build:docker": "vite build", "build:docker": "vite build",
"format": "prettier --write .", "format": "prettier --write .",
"update-version": "node scripts/update-version.js",
"release": "node scripts/release.js patch", "release": "node scripts/release.js patch",
"release:minor": "node scripts/release.js minor", "release:minor": "node scripts/release.js minor",
"release:major": "node scripts/release.js major", "release:major": "node scripts/release.js major",
"serve:simple": "SIMPLE_MODE=true npm run build && npm run preview -- --port 3000", "serve:simple": "SIMPLE_MODE=true npm run build && npx serve dist -p 3000",
"serve": "npm run build && npm run preview -- --port 3000",
"package": "node scripts/package-dist.js" "package": "node scripts/package-dist.js"
}, },
"devDependencies": { "devDependencies": {
+2 -2
View File
@@ -244,7 +244,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -313,7 +313,7 @@
</div> </div>
</footer> </footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
@@ -12372,10 +12372,7 @@ const PDFViewerApplication = {
let file; let file;
const queryString = document.location.search.substring(1); const queryString = document.location.search.substring(1);
const params = parseQueryString(queryString); const params = parseQueryString(queryString);
file = params.get("file"); file = params.get("file") ?? AppOptions.get("defaultUrl");
if (!file) {
file = "";
}
validateFileURL(file); validateFileURL(file);
const fileInput = this._openFileInput = document.createElement("input"); const fileInput = this._openFileInput = document.createElement("input");
fileInput.id = "fileInput"; fileInput.id = "fileInput";
+1 -1
View File
@@ -207,7 +207,7 @@
viewer: document.getElementById('viewer'), viewer: document.getElementById('viewer'),
eventBus, eventBus,
linkService, linkService,
annotationEditorMode: 1, annotationEditorMode: 0,
enableScripting: true, enableScripting: true,
renderer: 'canvas' renderer: 'canvas'
}); });
+2 -5
View File
@@ -1512,7 +1512,7 @@ class BaseExternalServices {
class BasePreferences { class BasePreferences {
#defaults = Object.freeze({ #defaults = Object.freeze({
altTextLearnMoreUrl: "", altTextLearnMoreUrl: "",
annotationEditorMode: 1, annotationEditorMode: 0,
annotationMode: 2, annotationMode: 2,
capCanvasAreaFactor: 200, capCanvasAreaFactor: 200,
commentLearnMoreUrl: "", commentLearnMoreUrl: "",
@@ -16429,10 +16429,7 @@ const PDFViewerApplication = {
let file; let file;
const queryString = document.location.search.substring(1); const queryString = document.location.search.substring(1);
const params = parseQueryString(queryString); const params = parseQueryString(queryString);
file = params.get("file"); file = params.get("file") ?? AppOptions.get("defaultUrl");
if (!file) {
file = "";
}
try { try {
file = new URL(decodeURIComponent(file)).href; file = new URL(decodeURIComponent(file)).href;
} catch { } catch {
-20
View File
@@ -1,20 +0,0 @@
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
interface AddAttachmentsMessage {
command: 'add-attachments';
pdfBuffer: ArrayBuffer;
attachmentBuffers: ArrayBuffer[];
attachmentNames: string[];
}
interface AddAttachmentsSuccessResponse {
status: 'success';
modifiedPDF: ArrayBuffer;
}
interface AddAttachmentsErrorResponse {
status: 'error';
message: string;
}
type AddAttachmentsResponse = AddAttachmentsSuccessResponse | AddAttachmentsErrorResponse;
-132
View File
@@ -1,132 +0,0 @@
self.importScripts('/coherentpdf.browser.min.js');
function parsePageRange(rangeString, totalPages) {
const pages = new Set();
const parts = rangeString.split(',').map(s => s.trim());
for (const part of parts) {
if (part.includes('-')) {
const [start, end] = part.split('-').map(s => parseInt(s.trim(), 10));
if (isNaN(start) || isNaN(end)) continue;
for (let i = Math.max(1, start); i <= Math.min(totalPages, end); i++) {
pages.add(i);
}
} else {
const pageNum = parseInt(part, 10);
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) {
pages.add(pageNum);
}
}
}
return Array.from(pages).sort((a, b) => a - b);
}
function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNames, attachmentLevel, pageRange) {
try {
const uint8Array = new Uint8Array(pdfBuffer);
let pdf;
try {
pdf = coherentpdf.fromMemory(uint8Array, '');
} catch (error) {
const errorMsg = error.message || error.toString();
if (errorMsg.includes('Failed to read PDF') ||
errorMsg.includes('Could not read object') ||
errorMsg.includes('No /Root entry') ||
errorMsg.includes('PDFError')) {
self.postMessage({
status: 'error',
message: 'The PDF file has structural issues and cannot be processed. The file may be corrupted, incomplete, or created with non-standard tools. Please try:\n\n• Opening and re-saving the PDF in another PDF viewer\n• Using a different PDF file\n• Repairing the PDF with a PDF repair tool'
});
} else {
self.postMessage({
status: 'error',
message: `Failed to load PDF: ${errorMsg}`
});
}
return;
}
const totalPages = coherentpdf.pages(pdf);
let targetPages = [];
if (attachmentLevel === 'page') {
if (!pageRange) {
self.postMessage({
status: 'error',
message: 'Page range is required for page-level attachments.'
});
coherentpdf.deletePdf(pdf);
return;
}
targetPages = parsePageRange(pageRange, totalPages);
if (targetPages.length === 0) {
self.postMessage({
status: 'error',
message: 'Invalid page range specified.'
});
coherentpdf.deletePdf(pdf);
return;
}
}
for (let i = 0; i < attachmentBuffers.length; i++) {
try {
const attachmentData = new Uint8Array(attachmentBuffers[i]);
const attachmentName = attachmentNames[i];
if (attachmentLevel === 'document') {
coherentpdf.attachFileFromMemory(attachmentData, attachmentName, pdf);
} else {
for (const pageNum of targetPages) {
coherentpdf.attachFileToPageFromMemory(attachmentData, attachmentName, pdf, pageNum);
}
}
} catch (error) {
console.warn(`Failed to attach file ${attachmentNames[i]}:`, error);
self.postMessage({
status: 'error',
message: `Failed to attach file ${attachmentNames[i]}: ${error.message || error}`
});
coherentpdf.deletePdf(pdf);
return;
}
}
// Save the modified PDF
const modifiedBytes = coherentpdf.toMemory(pdf, false, false);
coherentpdf.deletePdf(pdf);
const buffer = modifiedBytes.buffer.slice(
modifiedBytes.byteOffset,
modifiedBytes.byteOffset + modifiedBytes.byteLength
);
self.postMessage({
status: 'success',
modifiedPDF: buffer
}, [buffer]);
} catch (error) {
self.postMessage({
status: 'error',
message: error instanceof Error
? error.message
: 'Unknown error occurred while adding attachments.'
});
}
}
self.onmessage = (e) => {
if (e.data.command === 'add-attachments') {
addAttachmentsToPDFInWorker(
e.data.pdfBuffer,
e.data.attachmentBuffers,
e.data.attachmentNames,
e.data.attachmentLevel || 'document',
e.data.pageRange || ''
);
}
};
+5 -9
View File
@@ -75,24 +75,20 @@ function main() {
const newVersion = updateVersion(type); const newVersion = updateVersion(type);
console.log(`📦 Updated version to ${newVersion}`); console.log(`📦 Updated version to ${newVersion}`);
// 2. Update version in HTML files // 2. Add and commit changes
console.log(`📝 Updating version in HTML files...`); execSync('git add package.json', { stdio: 'inherit' });
execSync('npm run update-version', { stdio: 'inherit' });
// 3. Add and commit changes
execSync('git add package.json *.html src/pages/*.html', { stdio: 'inherit' });
execSync(`git commit -m "Release v${newVersion}"`, { stdio: 'inherit' }); execSync(`git commit -m "Release v${newVersion}"`, { stdio: 'inherit' });
console.log(`💾 Committed version change`); console.log(`💾 Committed version change`);
// 4. Create git tag // 3. Create git tag
const tagName = createGitTag(newVersion); const tagName = createGitTag(newVersion);
// 5. Build and package the distribution files // 4. Build and package the distribution files
console.log(`📦 Building and packaging distribution files...`); console.log(`📦 Building and packaging distribution files...`);
execSync('npm run package', { stdio: 'inherit' }); execSync('npm run package', { stdio: 'inherit' });
console.log(`📦 Distribution files packaged successfully`); console.log(`📦 Distribution files packaged successfully`);
// 6. Push everything to main // 5. Push everything to main
console.log(`📤 Pushing to main...`); console.log(`📤 Pushing to main...`);
execSync('git push origin main', { stdio: 'inherit' }); execSync('git push origin main', { stdio: 'inherit' });
execSync(`git push origin ${tagName}`, { stdio: 'inherit' }); execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
-64
View File
@@ -1,64 +0,0 @@
#!/usr/bin/env node
/**
* Script to update version numbers in HTML files from package.json
* Run this script whenever you need to sync HTML versions with package.json
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Read version from package.json
const packageJson = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
);
const version = packageJson.version;
// HTML files to update
const htmlFiles = [
'index.html',
'about.html',
'contact.html',
'faq.html',
'privacy.html',
'terms.html',
'src/pages/add-stamps.html',
'src/pages/bookmark.html',
'src/pages/json-to-pdf.html',
'src/pages/pdf-multi-tool.html',
'src/pages/pdf-to-json.html',
'src/pages/table-of-contents.html',
];
console.log(`Updating version to ${version} in HTML files...`);
let updatedCount = 0;
htmlFiles.forEach((file) => {
const filePath = path.join(__dirname, '..', file);
if (!fs.existsSync(filePath)) {
console.log(`⚠️ Skipping ${file} (not found)`);
return;
}
let content = fs.readFileSync(filePath, 'utf8');
// Replace version in <span id="app-version">X.X.X</span>
const regex = /(<span id="app-version">)[^<]+(<\/span>)/g;
const newContent = content.replace(regex, `$1${version}$2`);
if (content !== newContent) {
fs.writeFileSync(filePath, newContent, 'utf8');
console.log(`✓ Updated ${file}`);
updatedCount++;
} else {
console.log(`- ${file} (already up to date)`);
}
});
console.log(`\nDone! Updated ${updatedCount} file(s) to version ${version}`);
-25
View File
@@ -1,25 +0,0 @@
{
"headers": [
{
"source": "**/*",
"headers": [
{
"key": "Cross-Origin-Opener-Policy",
"value": "same-origin"
},
{
"key": "Cross-Origin-Embedder-Policy",
"value": "require-corp"
},
{
"key": "X-Frame-Options",
"value": "SAMEORIGIN"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
}
]
}
]
}
+1 -1
View File
@@ -176,7 +176,7 @@ export const categories = [
id: 'image-to-pdf', id: 'image-to-pdf',
name: 'Image to PDF', name: 'Image to PDF',
icon: 'images', icon: 'images',
subtitle: 'Convert JPG, PNG, WebP, BMP, TIFF, SVG, HEIC to PDF.', subtitle: 'Combine various images into one PDF.',
}, },
{ {
id: 'jpg-to-pdf', id: 'jpg-to-pdf',
+9 -83
View File
@@ -21,16 +21,6 @@ import {
} from '../config/pdf-tools.js'; } from '../config/pdf-tools.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
// Global state for rotation tracking (used by Rotate tool)
const rotationState: number[] = [];
let imageSortableInstance: Sortable | null = null;
const activeImageUrls = new Map<File, string>();
// Export getter for rotation state (used by ui.ts)
export function getRotationState(): readonly number[] {
return rotationState;
}
async function handleSinglePdfUpload(toolId, file) { async function handleSinglePdfUpload(toolId, file) {
showLoader('Loading PDF...'); showLoader('Loading PDF...');
try { try {
@@ -111,12 +101,6 @@ async function handleSinglePdfUpload(toolId, file) {
await renderPageThumbnails(toolId, state.pdfDoc); await renderPageThumbnails(toolId, state.pdfDoc);
if (toolId === 'rotate') { if (toolId === 'rotate') {
// Initialize rotation state for all pages
rotationState.length = 0;
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
rotationState.push(0);
}
const rotateAllControls = document.getElementById( const rotateAllControls = document.getElementById(
'rotate-all-controls' 'rotate-all-controls'
); );
@@ -129,15 +113,11 @@ async function handleSinglePdfUpload(toolId, file) {
createIcons({ icons }); createIcons({ icons });
const rotateAll = (direction) => { const rotateAll = (direction) => {
// Update rotation state for ALL pages (including unrendered ones)
for (let i = 0; i < rotationState.length; i++) {
rotationState[i] = (rotationState[i] + direction * 90 + 360) % 360;
}
// Update DOM for currently rendered pages
document.querySelectorAll('.page-rotator-item').forEach((item) => { document.querySelectorAll('.page-rotator-item').forEach((item) => {
const pageIndex = parseInt((item as HTMLElement).dataset.pageIndex || '0'); const currentRotation = parseInt(
const newRotation = rotationState[pageIndex]; (item as HTMLElement).dataset.rotation || '0'
);
const newRotation = (currentRotation + direction * 90 + 360) % 360;
(item as HTMLElement).dataset.rotation = newRotation.toString(); (item as HTMLElement).dataset.rotation = newRotation.toString();
const thumbnail = item.querySelector('canvas, img'); const thumbnail = item.querySelector('canvas, img');
if (thumbnail) { if (thumbnail) {
@@ -488,8 +468,6 @@ async function handleMultiFileUpload(toolId) {
toolId === 'alternate-merge' || toolId === 'alternate-merge' ||
toolId === 'reverse-pages' toolId === 'reverse-pages'
) { ) {
showLoader('Loading PDF documents...');
const pdfFilesUnloaded: File[] = []; const pdfFilesUnloaded: File[] = [];
state.files.forEach((file) => { state.files.forEach((file) => {
@@ -524,7 +502,6 @@ async function handleMultiFileUpload(toolId) {
const errorMessage = `PDFs found that are password-protected\n\nPlease use the Decrypt or Change Permissions tool on these files first:\n\n${encryptedPDFFileNames.join('\n')}`; const errorMessage = `PDFs found that are password-protected\n\nPlease use the Decrypt or Change Permissions tool on these files first:\n\n${encryptedPDFFileNames.join('\n')}`;
hideLoader(); // Hide loader before showing alert
showAlert('Protected PDFs', errorMessage); showAlert('Protected PDFs', errorMessage);
switchView('grid'); switchView('grid');
@@ -549,27 +526,9 @@ async function handleMultiFileUpload(toolId) {
toolLogic['alternate-merge'].setup(); toolLogic['alternate-merge'].setup();
} else if (toolId === 'image-to-pdf') { } else if (toolId === 'image-to-pdf') {
const imageList = document.getElementById('image-list'); const imageList = document.getElementById('image-list');
imageList.textContent = '';
const renderedFiles = new Set(
Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName)
);
state.files.forEach((file) => { state.files.forEach((file) => {
if (!file) { const url = URL.createObjectURL(file);
console.error('Invalid file encountered in state.files');
return;
}
if (renderedFiles.has(file.name)) {
return;
}
let url = activeImageUrls.get(file);
if (!url) {
url = URL.createObjectURL(file);
activeImageUrls.set(file, url);
}
const li = document.createElement('li'); const li = document.createElement('li');
li.className = 'relative group cursor-move'; li.className = 'relative group cursor-move';
li.dataset.fileName = file.name; li.dataset.fileName = file.name;
@@ -591,28 +550,10 @@ async function handleMultiFileUpload(toolId) {
imageList.appendChild(li); imageList.appendChild(li);
}); });
const syncStateWithDOM = () => { Sortable.create(imageList);
const domOrder = Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName); // Show image-to-pdf options and wire up slider text
state.files.sort((a, b) => {
const aIndex = domOrder.indexOf(a.name);
const bIndex = domOrder.indexOf(b.name);
return aIndex - bIndex;
});
};
if (!imageSortableInstance) {
imageSortableInstance = Sortable.create(imageList, {
animation: 150,
onEnd: () => {
syncStateWithDOM();
}
});
}
syncStateWithDOM();
const opts = document.getElementById('image-to-pdf-options'); const opts = document.getElementById('image-to-pdf-options');
if (opts && opts.classList.contains('hidden')) { if (opts) {
opts.classList.remove('hidden'); opts.classList.remove('hidden');
const slider = document.getElementById('image-pdf-quality') as HTMLInputElement; const slider = document.getElementById('image-pdf-quality') as HTMLInputElement;
const value = document.getElementById('image-pdf-quality-value'); const value = document.getElementById('image-pdf-quality-value');
@@ -676,18 +617,6 @@ export function setupFileInputHandler(toolId) {
const processFiles = async (newFiles) => { const processFiles = async (newFiles) => {
if (newFiles.length === 0) return; if (newFiles.length === 0) return;
if (toolId === 'image-to-pdf') {
const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/bmp', 'image/tiff'];
const validFiles = newFiles.filter(file => validTypes.includes(file.type));
if (validFiles.length < newFiles.length) {
showAlert('Invalid Files', 'Some files were skipped because they are not supported images.');
}
newFiles = validFiles;
if (newFiles.length === 0) return;
}
if (!isMultiFileTool || isFirstUpload) { if (!isMultiFileTool || isFirstUpload) {
state.files = newFiles; state.files = newFiles;
} else { } else {
@@ -802,9 +731,6 @@ export function setupFileInputHandler(toolId) {
const clearBtn = document.getElementById('clear-files-btn'); const clearBtn = document.getElementById('clear-files-btn');
if (clearBtn) { if (clearBtn) {
clearBtn.addEventListener('click', () => { clearBtn.addEventListener('click', () => {
activeImageUrls.forEach(url => URL.revokeObjectURL(url));
activeImageUrls.clear();
state.files = []; state.files = [];
isFirstUpload = true; isFirstUpload = true;
(fileInput as HTMLInputElement).value = ''; (fileInput as HTMLInputElement).value = '';
+26 -113
View File
@@ -1,40 +1,10 @@
import { showLoader, hideLoader, showAlert } from '../ui'; import { showLoader, hideLoader, showAlert } from '../ui';
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers'; import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
import { state } from '../state'; import { state } from '../state';
const worker = new Worker('/workers/add-attachments.worker.js');
let attachments: File[] = []; let attachments: File[] = [];
worker.onmessage = (e) => {
const data = e.data;
if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
downloadFile(
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
`attached-${state.files[0].name}`
);
showAlert('Success', `${attachments.length} file(s) attached successfully.`);
clearAttachments();
} else if (data.status === 'error') {
hideLoader();
showAlert('Error', data.message || 'Unknown error occurred.');
clearAttachments();
}
};
worker.onerror = (error) => {
hideLoader();
console.error('Worker error:', error);
showAlert('Error', 'Worker error occurred. Check console for details.');
clearAttachments();
};
export async function addAttachments() { export async function addAttachments() {
if (!state.files || state.files.length === 0) { if (!state.pdfDoc) {
showAlert('Error', 'Main PDF is not loaded.'); showAlert('Error', 'Main PDF is not loaded.');
return; return;
} }
@@ -43,57 +13,39 @@ export async function addAttachments() {
return; return;
} }
const attachmentLevel = (
document.querySelector('input[name="attachment-level"]:checked') as HTMLInputElement
)?.value || 'document';
let pageRange: string = '';
if (attachmentLevel === 'page') {
const pageRangeInput = document.getElementById('attachment-page-range') as HTMLInputElement;
pageRange = pageRangeInput?.value?.trim() || '';
if (!pageRange) {
showAlert('Error', 'Please specify a page range for page-level attachments.');
return;
}
}
showLoader('Embedding files into PDF...'); showLoader('Embedding files into PDF...');
try { try {
const pdfFile = state.files[0]; const pdfDoc = state.pdfDoc;
const pdfBuffer = (await readFileAsArrayBuffer(pdfFile)) as ArrayBuffer;
const attachmentBuffers: ArrayBuffer[] = [];
const attachmentNames: string[] = [];
for (let i = 0; i < attachments.length; i++) { for (let i = 0; i < attachments.length; i++) {
const file = attachments[i]; const file = attachments[i];
showLoader(`Reading ${file.name} (${i + 1}/${attachments.length})...`); showLoader(`Attaching ${file.name} (${i + 1}/${attachments.length})...`);
const fileBuffer = (await readFileAsArrayBuffer(file)) as ArrayBuffer; const fileBytes = await readFileAsArrayBuffer(file);
attachmentBuffers.push(fileBuffer);
attachmentNames.push(file.name); await pdfDoc.attach(fileBytes as ArrayBuffer, file.name, {
mimeType: file.type || 'application/octet-stream',
description: `Attached file: ${file.name}`,
creationDate: new Date(),
modificationDate: new Date(file.lastModified),
});
} }
showLoader('Attaching files to PDF...'); const pdfBytes = await pdfDoc.save();
downloadFile(
const message = { new Blob([pdfBytes], { type: 'application/pdf' }),
command: 'add-attachments', `attached-${state.files[0].name}`
pdfBuffer: pdfBuffer, );
attachmentBuffers: attachmentBuffers,
attachmentNames: attachmentNames,
attachmentLevel: attachmentLevel,
pageRange: pageRange
};
const transferables = [pdfBuffer, ...attachmentBuffers];
worker.postMessage(message, transferables);
showAlert(
'Success',
`${attachments.length} file(s) attached successfully.`
);
} catch (error: any) { } catch (error: any) {
console.error('Error attaching files:', error); console.error('Error attaching files:', error);
hideLoader();
showAlert('Error', `Failed to attach files: ${error.message}`); showAlert('Error', `Failed to attach files: ${error.message}`);
} finally {
hideLoader();
clearAttachments(); clearAttachments();
} }
} }
@@ -107,8 +59,6 @@ function clearAttachments() {
const processBtn = document.getElementById( const processBtn = document.getElementById(
'process-btn' 'process-btn'
) as HTMLButtonElement; ) as HTMLButtonElement;
const attachmentLevelOptions = document.getElementById('attachment-level-options');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
if (fileListDiv) fileListDiv.innerHTML = ''; if (fileListDiv) fileListDiv.innerHTML = '';
if (attachmentInput) attachmentInput.value = ''; if (attachmentInput) attachmentInput.value = '';
@@ -116,20 +66,14 @@ function clearAttachments() {
processBtn.disabled = true; processBtn.disabled = true;
processBtn.classList.add('hidden'); processBtn.classList.add('hidden');
} }
if (attachmentLevelOptions) {
attachmentLevelOptions.classList.add('hidden');
}
if (pageRangeWrapper) {
pageRangeWrapper.classList.add('hidden');
} }
const documentRadio = document.querySelector('input[name="attachment-level"][value="document"]') as HTMLInputElement; let isSetup = false; // Prevent duplicate setup
if (documentRadio) {
documentRadio.checked = true;
}
}
export function setupAddAttachmentsTool() { export function setupAddAttachmentsTool() {
if (isSetup) return; // Already set up
isSetup = true;
const optionsDiv = document.getElementById('attachment-options'); const optionsDiv = document.getElementById('attachment-options');
const attachmentInput = document.getElementById( const attachmentInput = document.getElementById(
'attachment-files-input' 'attachment-files-input'
@@ -138,29 +82,14 @@ export function setupAddAttachmentsTool() {
const processBtn = document.getElementById( const processBtn = document.getElementById(
'process-btn' 'process-btn'
) as HTMLButtonElement; ) as HTMLButtonElement;
const attachmentLevelOptions = document.getElementById('attachment-level-options');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
const totalPagesSpan = document.getElementById('attachment-total-pages');
if (!optionsDiv || !attachmentInput || !fileListDiv || !processBtn) { if (!optionsDiv || !attachmentInput || !fileListDiv || !processBtn) {
console.error('Attachment tool UI elements not found.'); console.error('Attachment tool UI elements not found.');
return; return;
} }
if (!state.files || state.files.length === 0) {
console.error('No PDF file loaded for adding attachments.');
return;
}
optionsDiv.classList.remove('hidden'); optionsDiv.classList.remove('hidden');
if (totalPagesSpan && state.pdfDoc) {
totalPagesSpan.textContent = state.pdfDoc.getPageCount().toString();
}
if (attachmentInput.dataset.listenerAttached) return;
attachmentInput.dataset.listenerAttached = 'true';
attachmentInput.addEventListener('change', (e) => { attachmentInput.addEventListener('change', (e) => {
const files = (e.target as HTMLInputElement).files; const files = (e.target as HTMLInputElement).files;
if (files && files.length > 0) { if (files && files.length > 0) {
@@ -185,10 +114,6 @@ export function setupAddAttachmentsTool() {
fileListDiv.appendChild(div); fileListDiv.appendChild(div);
}); });
if (attachmentLevelOptions) {
attachmentLevelOptions.classList.remove('hidden');
}
processBtn.disabled = false; processBtn.disabled = false;
processBtn.classList.remove('hidden'); processBtn.classList.remove('hidden');
} else { } else {
@@ -196,17 +121,5 @@ export function setupAddAttachmentsTool() {
} }
}); });
const attachmentLevelRadios = document.querySelectorAll('input[name="attachment-level"]');
attachmentLevelRadios.forEach((radio) => {
radio.addEventListener('change', (e) => {
const value = (e.target as HTMLInputElement).value;
if (value === 'page' && pageRangeWrapper) {
pageRangeWrapper.classList.remove('hidden');
} else if (pageRangeWrapper) {
pageRangeWrapper.classList.add('hidden');
}
});
});
processBtn.onclick = addAttachments; processBtn.onclick = addAttachments;
} }
+5 -21
View File
@@ -58,23 +58,11 @@ async function loadPdfInViewer(file: File) {
const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' }) const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' })
currentBlobUrl = URL.createObjectURL(blob) currentBlobUrl = URL.createObjectURL(blob)
try {
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences')
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {}
delete (existingPrefs as any).annotationEditorMode
const newPrefs = {
...existingPrefs,
enablePermissions: false,
}
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs))
} catch {}
const iframe = document.createElement('iframe') const iframe = document.createElement('iframe')
iframe.className = 'w-full h-full border-0' iframe.className = 'w-full h-full border-0'
iframe.allowFullscreen = true iframe.allowFullscreen = true
const viewerUrl = new URL('/pdfjs-annotation-viewer/web/viewer.html', window.location.origin) iframe.src = `/pdfjs-annotation-viewer/web/viewer.html?file=${encodeURIComponent(currentBlobUrl)}`
iframe.src = `${viewerUrl.toString()}?file=${encodeURIComponent(currentBlobUrl)}`
iframe.addEventListener('load', () => { iframe.addEventListener('load', () => {
setupAnnotationViewer(iframe) setupAnnotationViewer(iframe)
@@ -97,14 +85,10 @@ function setupAnnotationViewer(iframe: HTMLIFrameElement) {
await app.initializedPromise await app.initializedPromise
} }
const eventBus = app?.eventBus const stampBtn = doc.getElementById('editorStamp') as HTMLButtonElement | null
if (eventBus && typeof eventBus._on === 'function') { if (stampBtn) {
eventBus._on('annotationeditoruimanager', () => { stampBtn.classList.remove('hidden')
try { stampBtn.disabled = false
const stampBtn = doc.getElementById('editorStampButton') as HTMLButtonElement | null
stampBtn?.click()
} catch {}
})
} }
const root = doc.querySelector('.PdfjsAnnotationExtension') as HTMLElement | null const root = doc.querySelector('.PdfjsAnnotationExtension') as HTMLElement | null
+18 -57
View File
@@ -1,7 +1,6 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js'; import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
@@ -83,9 +82,6 @@ export async function renderDuplicateOrganizeThumbnails() {
const grid = document.getElementById('page-grid'); const grid = document.getElementById('page-grid');
if (!grid) return; if (!grid) return;
// Cleanup any previous lazy loading observers
cleanupLazyRendering();
showLoader('Rendering page previews...'); showLoader('Rendering page previews...');
const pdfData = await state.pdfDoc.save(); const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'. // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
@@ -93,13 +89,20 @@ export async function renderDuplicateOrganizeThumbnails() {
grid.textContent = ''; grid.textContent = '';
// Function to create wrapper element for each page for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.5 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: canvas.getContext('2d'), viewport })
.promise;
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = wrapper.className =
'page-thumbnail relative cursor-move flex flex-col items-center gap-2'; 'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.originalPageIndex = pageNumber - 1; wrapper.dataset.originalPageIndex = i - 1;
const imgContainer = document.createElement('div'); const imgContainer = document.createElement('div');
imgContainer.className = imgContainer.className =
@@ -113,7 +116,7 @@ export async function renderDuplicateOrganizeThumbnails() {
const pageNumberSpan = document.createElement('span'); const pageNumberSpan = document.createElement('span');
pageNumberSpan.className = pageNumberSpan.className =
'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1'; 'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
pageNumberSpan.textContent = pageNumber.toString(); pageNumberSpan.textContent = i.toString();
const controlsDiv = document.createElement('div'); const controlsDiv = document.createElement('div');
controlsDiv.className = 'flex items-center justify-center gap-4'; controlsDiv.className = 'flex items-center justify-center gap-4';
@@ -138,39 +141,14 @@ export async function renderDuplicateOrganizeThumbnails() {
controlsDiv.append(duplicateBtn, deleteBtn); controlsDiv.append(duplicateBtn, deleteBtn);
wrapper.append(imgContainer, pageNumberSpan, controlsDiv); wrapper.append(imgContainer, pageNumberSpan, controlsDiv);
grid.appendChild(wrapper);
attachEventListeners(wrapper); attachEventListeners(wrapper);
return wrapper;
};
try {
// Render pages progressively with lazy loading
await renderPagesProgressively(
pdfjsDoc,
grid,
createWrapper,
{
batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '400px',
onProgress: (current, total) => {
showLoader(`Rendering page previews: ${current}/${total}`);
},
onBatchComplete: () => {
createIcons({ icons });
} }
}
);
initializePageGridSortable(); initializePageGridSortable();
} catch (error) { createIcons({ icons });
console.error('Error rendering thumbnails:', error);
showAlert('Error', 'Failed to render page previews');
} finally {
hideLoader(); hideLoader();
} }
}
export async function processAndSave() { export async function processAndSave() {
showLoader('Building new PDF...'); showLoader('Building new PDF...');
@@ -178,28 +156,11 @@ export async function processAndSave() {
const grid = document.getElementById('page-grid'); const grid = document.getElementById('page-grid');
const finalPageElements = grid.querySelectorAll('.page-thumbnail'); const finalPageElements = grid.querySelectorAll('.page-thumbnail');
const finalIndices = Array.from(finalPageElements) const finalIndices = Array.from(finalPageElements).map((el) =>
.map((el) => parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10)) parseInt((el as HTMLElement).dataset.originalPageIndex)
.filter(index => !isNaN(index) && index >= 0); );
console.log('Saving PDF with indices:', finalIndices);
console.log('Original PDF Page Count:', state.pdfDoc?.getPageCount());
if (finalIndices.length === 0) {
showAlert('Error', 'No valid pages to save.');
return;
}
const newPdfDoc = await PDFLibDocument.create(); const newPdfDoc = await PDFLibDocument.create();
const totalPages = state.pdfDoc.getPageCount();
const invalidIndices = finalIndices.filter(i => i >= totalPages);
if (invalidIndices.length > 0) {
console.error('Found invalid indices:', invalidIndices);
showAlert('Error', 'Some pages could not be processed. Please try again.');
return;
}
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices); const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
copiedPages.forEach((page: any) => newPdfDoc.addPage(page)); copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
@@ -209,8 +170,8 @@ export async function processAndSave() {
'organized.pdf' 'organized.pdf'
); );
} catch (e) { } catch (e) {
console.error('Save error:', e); console.error(e);
showAlert('Error', 'Failed to save the new PDF. Check console for details.'); showAlert('Error', 'Failed to save the new PDF.');
} finally { } finally {
hideLoader(); hideLoader();
} }
+5 -60
View File
@@ -1,6 +1,5 @@
import { downloadFile, formatBytes } from '../utils/helpers.js'; import { downloadFile, formatBytes } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { showAlert } from '../ui.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
const worker = new Worker('/workers/extract-attachments.worker.js'); const worker = new Worker('/workers/extract-attachments.worker.js');
@@ -67,30 +66,6 @@ worker.onmessage = (e: MessageEvent<ExtractAttachmentResponse>) => {
if (e.data.status === 'success') { if (e.data.status === 'success') {
const attachments = e.data.attachments; const attachments = e.data.attachments;
if (attachments.length === 0) {
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
state.files = [];
state.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
}
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) {
fileInput.value = '';
}
const fileControls = document.getElementById('file-controls');
if (fileControls) {
fileControls.classList.add('hidden');
}
return;
}
const zip = new JSZip(); const zip = new JSZip();
let totalSize = 0; let totalSize = 0;
@@ -101,60 +76,29 @@ worker.onmessage = (e: MessageEvent<ExtractAttachmentResponse>) => {
zip.generateAsync({ type: 'blob' }).then((zipBlob) => { zip.generateAsync({ type: 'blob' }).then((zipBlob) => {
downloadFile(zipBlob, 'extracted-attachments.zip'); downloadFile(zipBlob, 'extracted-attachments.zip');
showAlert('Success', `${attachments.length} attachment(s) extracted successfully!`);
showStatus( showStatus(
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`, `Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
'success' 'success'
); );
state.files = []; state.files = [];
state.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) { if (fileDisplayArea) {
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
fileDisplayArea.classList.add('hidden');
} }
const fileInput = document.getElementById('file-input') as HTMLInputElement; const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) { if (fileInput) {
fileInput.value = ''; fileInput.value = '';
} }
document.getElementById('process-btn')?.classList.add('opacity-50', 'cursor-not-allowed');
const fileControls = document.getElementById('file-controls'); document.getElementById('process-btn')?.setAttribute('disabled', 'true');
if (fileControls) {
fileControls.classList.add('hidden');
}
}); });
} else if (e.data.status === 'error') { } else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.'; const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage); console.error('Worker Error:', errorMessage);
if (errorMessage.includes('No attachments were found')) {
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
state.files = [];
state.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
}
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) {
fileInput.value = '';
}
const fileControls = document.getElementById('file-controls');
if (fileControls) {
fileControls.classList.add('hidden');
}
} else {
showStatus(`Error: ${errorMessage}`, 'error'); showStatus(`Error: ${errorMessage}`, 'error');
} }
}
}; };
worker.onerror = (error) => { worker.onerror = (error) => {
@@ -169,7 +113,8 @@ function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info'
if (!statusMessage) return; if (!statusMessage) return;
statusMessage.textContent = message; statusMessage.textContent = message;
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success' statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
type === 'success'
? 'bg-green-900 text-green-200' ? 'bg-green-900 text-green-200'
: type === 'error' : type === 'error'
? 'bg-red-900 text-red-200' ? 'bg-red-900 text-red-200'
+33 -53
View File
@@ -1,9 +1,7 @@
import { showLoader, hideLoader, showAlert } from '../ui.ts'; import { showLoader, hideLoader, showAlert } from '../ui.ts';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.ts'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.ts';
import { state } from '../state.ts'; import { state } from '../state.ts';
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.ts';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
@@ -131,24 +129,44 @@ async function renderPageMergeThumbnails() {
mergeState.isRendering = true; mergeState.isRendering = true;
container.textContent = ''; container.textContent = '';
// Cleanup any previous lazy loading observers let currentPageNumber = 0;
cleanupLazyRendering();
let totalPages = state.files.reduce((sum, file) => { let totalPages = state.files.reduce((sum, file) => {
const pdfDoc = mergeState.pdfDocs[file.name]; const pdfDoc = mergeState.pdfDocs[file.name];
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0); return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
}, 0); }, 0);
try { try {
let currentPageNumber = 0; const thumbnailsHTML = [];
for (const file of state.files) {
const pdfDoc = mergeState.pdfDocs[file.name];
if (!pdfDoc) continue;
const pdfData = await pdfDoc.save();
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
currentPageNumber++;
showLoader(
`Rendering page previews: ${currentPageNumber}/${totalPages}`
);
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.3 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d')!;
await page.render({
canvasContext: context,
canvas: canvas,
viewport,
}).promise;
// Function to create wrapper element for each page
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = wrapper.className =
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors'; 'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
wrapper.dataset.fileName = fileName || ''; wrapper.dataset.fileName = file.name;
wrapper.dataset.pageIndex = (pageNumber - 1).toString(); wrapper.dataset.pageIndex = (i - 1).toString();
const imgContainer = document.createElement('div'); const imgContainer = document.createElement('div');
imgContainer.className = 'relative'; imgContainer.className = 'relative';
@@ -160,60 +178,22 @@ async function renderPageMergeThumbnails() {
const pageNumDiv = document.createElement('div'); const pageNumDiv = document.createElement('div');
pageNumDiv.className = pageNumDiv.className =
'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg'; 'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
pageNumDiv.textContent = pageNumber.toString(); pageNumDiv.textContent = i.toString();
imgContainer.append(img, pageNumDiv); imgContainer.append(img, pageNumDiv);
const fileNamePara = document.createElement('p'); const fileNamePara = document.createElement('p');
fileNamePara.className = fileNamePara.className =
'text-xs text-gray-400 truncate w-full text-center'; 'text-xs text-gray-400 truncate w-full text-center';
const fullTitle = fileName ? `${fileName} (page ${pageNumber})` : `Page ${pageNumber}`; const fullTitle = `${file.name} (page ${i})`;
fileNamePara.title = fullTitle; fileNamePara.title = fullTitle;
fileNamePara.textContent = fileName fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
? `${fileName.substring(0, 10)}... (p${pageNumber})`
: `Page ${pageNumber}`;
wrapper.append(imgContainer, fileNamePara); wrapper.append(imgContainer, fileNamePara);
return wrapper; container.appendChild(wrapper);
};
// Render pages from all files progressively
for (const file of state.files) {
const pdfDoc = mergeState.pdfDocs[file.name];
if (!pdfDoc) continue;
const pdfData = await pdfDoc.save();
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
// Create a wrapper function that includes the file name
const createWrapperWithFileName = (canvas: HTMLCanvasElement, pageNumber: number) => {
return createWrapper(canvas, pageNumber, file.name);
};
// Render pages progressively with lazy loading
await renderPagesProgressively(
pdfjsDoc,
container,
createWrapperWithFileName,
{
batchSize: 6,
useLazyLoading: true,
lazyLoadMargin: '300px',
onProgress: (current, total) => {
currentPageNumber++;
showLoader(
`Rendering page previews: ${currentPageNumber}/${totalPages}`
);
},
onBatchComplete: () => {
createIcons({ icons });
} }
}
);
// TODO@ALAM - DON'T destroy the PDF.js document here - lazy loading still needs it! pdfjsDoc.destroy();
// It will be garbage collected automatically when no longer referenced
// pdfjsDoc.destroy(); // REMOVED
} }
mergeState.cachedThumbnails = true; mergeState.cachedThumbnails = true;
+109 -348
View File
@@ -1,10 +1,12 @@
// @TODO:@ALAM- sometimes I think... and then I forget...
//
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib'; import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import JSZip from 'jszip'; import JSZip from 'jszip';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { downloadFile } from '../utils/helpers'; import { downloadFile } from '../utils/helpers';
import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, createPlaceholder } from '../utils/render-utils';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -12,20 +14,15 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
).toString(); ).toString();
interface PageData { interface PageData {
id: string; // Unique ID for DOM reconciliation
pdfIndex: number; pdfIndex: number;
pageIndex: number; pageIndex: number;
rotation: number; rotation: number;
visualRotation: number; visualRotation: number;
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement;
pdfDoc: PDFLibDocument; pdfDoc: PDFLibDocument;
originalPageIndex: number; originalPageIndex: number;
} }
function generateId(): string {
return Math.random().toString(36).substr(2, 9) + Date.now().toString(36);
}
let allPages: PageData[] = []; let allPages: PageData[] = [];
let selectedPages: Set<number> = new Set(); let selectedPages: Set<number> = new Set();
let currentPdfDocs: PDFLibDocument[] = []; let currentPdfDocs: PDFLibDocument[] = [];
@@ -93,8 +90,6 @@ function hideModal() {
} }
function showLoading(current: number, total: number) { function showLoading(current: number, total: number) {
// renderPagesProgressively handles loading UI via onProgress callback
// but we can keep this for compatibility if needed
const loader = document.getElementById('loading-overlay'); const loader = document.getElementById('loading-overlay');
const progress = document.getElementById('loading-progress'); const progress = document.getElementById('loading-progress');
const text = document.getElementById('loading-text'); const text = document.getElementById('loading-text');
@@ -107,43 +102,16 @@ function showLoading(current: number, total: number) {
text.textContent = `Rendering pages... ${current} of ${total}`; text.textContent = `Rendering pages... ${current} of ${total}`;
} }
async function withButtonLoading(buttonId: string, action: () => Promise<void>) {
const button = document.getElementById(buttonId) as HTMLButtonElement;
if (!button) return;
const originalContent = button.innerHTML;
const originalPointerEvents = button.style.pointerEvents;
try {
button.disabled = true;
button.style.pointerEvents = 'none';
button.innerHTML = '<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
await action();
} finally {
button.disabled = false;
button.style.pointerEvents = originalPointerEvents;
button.innerHTML = originalContent;
}
}
function hideLoading() { function hideLoading() {
const loader = document.getElementById('loading-overlay'); const loader = document.getElementById('loading-overlay');
if (loader) loader.classList.add('hidden'); if (loader) loader.classList.add('hidden');
} }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
console.log('PDF Multi Tool: DOMContentLoaded');
initializeTool(); initializeTool();
}); });
} else {
console.log('PDF Multi Tool: DOMContentLoaded already fired, initializing immediately');
initializeTool();
}
function initializeTool() { function initializeTool() {
console.log('PDF Multi Tool: Initializing...');
createIcons({ icons }); createIcons({ icons });
document.getElementById('close-tool-btn')?.addEventListener('click', () => { document.getElementById('close-tool-btn')?.addEventListener('click', () => {
@@ -151,7 +119,6 @@ function initializeTool() {
}); });
document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => { document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => {
console.log('Upload button clicked, isRendering:', isRendering);
if (isRendering) { if (isRendering) {
showModal('Please Wait', 'Pages are still being rendered. Please wait...', 'info'); showModal('Please Wait', 'Pages are still being rendered. Please wait...', 'info');
return; return;
@@ -189,24 +156,19 @@ function initializeTool() {
}); });
document.getElementById('bulk-download-btn')?.addEventListener('click', () => { document.getElementById('bulk-download-btn')?.addEventListener('click', () => {
if (isRendering) return; if (isRendering) return;
if (selectedPages.size === 0) { bulkDownload();
showModal('No Pages Selected', 'Please select at least one page to download.', 'info');
return;
}
withButtonLoading('bulk-download-btn', async () => {
await downloadPagesAsPdf(Array.from(selectedPages).sort((a, b) => a - b), 'selected-pages.pdf');
}); });
document.getElementById('select-all-btn')?.addEventListener('click', () => {
if (isRendering) return;
selectAll();
});
document.getElementById('deselect-all-btn')?.addEventListener('click', () => {
if (isRendering) return;
deselectAll();
}); });
document.getElementById('export-pdf-btn')?.addEventListener('click', () => { document.getElementById('export-pdf-btn')?.addEventListener('click', () => {
if (isRendering) return; if (isRendering) return;
if (allPages.length === 0) { downloadAll();
showModal('No Pages', 'There are no pages to export.', 'info');
return;
}
withButtonLoading('export-pdf-btn', async () => {
await downloadAll();
});
}); });
document.getElementById('add-blank-page-btn')?.addEventListener('click', () => { document.getElementById('add-blank-page-btn')?.addEventListener('click', () => {
if (isRendering) return; if (isRendering) return;
@@ -278,25 +240,21 @@ function initializeTool() {
} }
function resetAll() { function resetAll() {
renderCancelled = true;
isRendering = false;
snapshot(); snapshot();
allPages = []; allPages = [];
selectedPages.clear(); selectedPages.clear();
splitMarkers.clear(); splitMarkers.clear();
currentPdfDocs = []; currentPdfDocs = [];
pageCanvasCache.clear(); pageCanvasCache.clear();
cleanupLazyRendering(); renderCancelled = false;
isRendering = false;
// Destroy sortable instance
if (sortableInstance) { if (sortableInstance) {
sortableInstance.destroy(); sortableInstance.destroy();
sortableInstance = null; sortableInstance = null;
} }
// Force clear DOM to prevent ghost pages
const pagesContainer = document.getElementById('pages-container');
if (pagesContainer) pagesContainer.innerHTML = '';
updatePageDisplay(); updatePageDisplay();
document.getElementById('upload-area')?.classList.remove('hidden'); document.getElementById('upload-area')?.classList.remove('hidden');
} }
@@ -318,79 +276,43 @@ async function loadPdfs(files: File[]) {
const uploadArea = document.getElementById('upload-area'); const uploadArea = document.getElementById('upload-area');
if (uploadArea) uploadArea.classList.add('hidden'); if (uploadArea) uploadArea.classList.add('hidden');
const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return;
isRendering = true; isRendering = true;
console.log('PDF Multi Tool: Starting render, isRendering set to true');
renderCancelled = false; renderCancelled = false;
let totalPages = 0;
// Cleanup previous observers let currentPage = 0;
cleanupLazyRendering();
showLoading(0, 100);
try { try {
// First pass: count total pages
const pdfDocs: PDFLibDocument[] = [];
for (const file of files) { for (const file of files) {
if (renderCancelled) break;
try { try {
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLibDocument.load(arrayBuffer); const pdfDoc = await PDFLibDocument.load(arrayBuffer);
currentPdfDocs.push(pdfDoc); pdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1; totalPages += pdfDoc.getPageCount();
const pdfBytes = await pdfDoc.save();
const pdfjsDoc = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
const numPages = pdfjsDoc.numPages;
// Pre-fill allPages with placeholders to maintain order/state
const startIndex = allPages.length;
for (let i = 0; i < numPages; i++) {
allPages.push({
id: generateId(),
pdfIndex,
pageIndex: i,
rotation: 0,
visualRotation: 0,
canvas: null, // Will be filled when rendered
pdfDoc,
originalPageIndex: i,
});
}
await renderPagesProgressively(
pdfjsDoc,
pagesContainer,
(canvas, pageNumber) => {
const globalIndex = startIndex + pageNumber - 1;
if (allPages[globalIndex]) {
allPages[globalIndex].canvas = canvas;
}
return createPageElement(canvas, globalIndex);
},
{
batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '400px',
onProgress: (current, total) => {
showLoading(current, total);
},
onBatchComplete: () => {
createIcons({ icons });
},
shouldCancel: () => renderCancelled, // Pass cancellation check
}
);
} catch (e) { } catch (e) {
console.error(`Failed to load PDF ${file.name}:`, e); console.error(`Failed to load PDF ${file.name}:`, e);
showModal('Error', `Failed to load ${file.name}. The file may be corrupted.`, 'error'); showModal('Error', `Failed to load ${file.name}. The file may be corrupted.`, 'error');
} }
} }
// Second pass: render pages
for (const pdfDoc of pdfDocs) {
if (renderCancelled) break;
currentPdfDocs.push(pdfDoc);
const numPages = pdfDoc.getPageCount();
for (let i = 0; i < numPages; i++) {
if (renderCancelled) break;
currentPage++;
showLoading(currentPage, totalPages);
await renderPage(pdfDoc, i, currentPdfDocs.length - 1);
}
}
if (!renderCancelled) { if (!renderCancelled) {
setupSortable(); setupSortable();
createIcons({ icons }); createIcons({ icons });
@@ -398,7 +320,6 @@ async function loadPdfs(files: File[]) {
} finally { } finally {
hideLoading(); hideLoading();
isRendering = false; isRendering = false;
console.log('PDF Multi Tool: Render finished/cancelled, isRendering set to false');
if (renderCancelled) { if (renderCancelled) {
renderCancelled = false; renderCancelled = false;
} }
@@ -409,55 +330,79 @@ function getCacheKey(pdfIndex: number, pageIndex: number): string {
return `${pdfIndex}-${pageIndex}`; return `${pdfIndex}-${pageIndex}`;
} }
// Wrapper for compatibility with updatePageDisplay async function renderPage(pdfDoc: PDFLibDocument, pageIndex: number, pdfIndex: number) {
const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return;
// Check cache first
const cacheKey = getCacheKey(pdfIndex, pageIndex);
let canvas: HTMLCanvasElement;
if (pageCanvasCache.has(cacheKey)) {
canvas = pageCanvasCache.get(cacheKey)!;
} else {
const pdfBytes = await pdfDoc.save();
const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
const page = await pdf.getPage(pageIndex + 1);
const viewport = page.getViewport({ scale: 0.5, rotation: 0 });
canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d');
if (!context) return;
await page.render({
canvasContext: context,
viewport,
background: 'white',
canvas
}).promise;
// Cache the canvas
pageCanvasCache.set(cacheKey, canvas);
}
const pageData: PageData = {
pdfIndex,
pageIndex,
rotation: 0, // Actual rotation to apply when saving PDF
visualRotation: 0, // Visual rotation for display only
canvas,
pdfDoc,
originalPageIndex: pageIndex,
};
allPages.push(pageData);
createPageCard(pageData, allPages.length - 1);
}
function createPageCard(pageData: PageData, index: number) { function createPageCard(pageData: PageData, index: number) {
const pagesContainer = document.getElementById('pages-container'); const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return; if (!pagesContainer) return;
const card = createPageElement(pageData.canvas, index);
pagesContainer.appendChild(card);
createIcons({ icons });
}
// Modified to return the element instead of appending it
function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTMLElement {
const pageData = allPages[index];
if (!pageData) {
console.error(`Page data not found for index ${index}`);
return document.createElement('div');
}
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'bg-gray-800 rounded-lg border-2 border-gray-700 p-2 relative group cursor-move'; card.className = 'bg-gray-800 rounded-lg border-2 border-gray-700 p-2 relative group cursor-move';
card.dataset.pageIndex = index.toString(); card.dataset.pageIndex = index.toString();
card.dataset.pageId = pageData.id; // Set ID for reconciliation
if (selectedPages.has(index)) { if (selectedPages.has(index)) {
card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500'); card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500');
} }
// Page preview
const preview = document.createElement('div'); const preview = document.createElement('div');
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative'; preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative';
preview.style.minHeight = '160px'; preview.style.minHeight = '160px';
preview.style.height = '250px'; preview.style.height = '250px';
if (canvas) { const previewCanvas = pageData.canvas;
const previewCanvas = canvas;
previewCanvas.className = 'max-w-full max-h-full object-contain'; previewCanvas.className = 'max-w-full max-h-full object-contain';
// Apply visual rotation using CSS transform
previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`; previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
previewCanvas.style.transition = 'transform 0.2s ease'; previewCanvas.style.transition = 'transform 0.2s ease';
preview.appendChild(previewCanvas); preview.appendChild(previewCanvas);
} else {
// Show loading placeholder if canvas is null
const loading = document.createElement('div');
loading.className = 'flex flex-col items-center justify-center text-gray-400';
loading.innerHTML = `
<i data-lucide="loader" class="w-8 h-8 animate-spin mb-2"></i>
<span class="text-xs">Loading...</span>
`;
preview.appendChild(loading);
preview.classList.add('bg-gray-700'); // Darker background for loading
}
// Page info // Page info
const info = document.createElement('div'); const info = document.createElement('div');
@@ -546,16 +491,9 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn); actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn);
card.append(preview, info, actions, selectBtn); card.append(preview, info, actions, selectBtn);
pagesContainer.appendChild(card);
// Check for split marker createIcons({ icons });
if (splitMarkers.has(index)) {
const marker = document.createElement('div');
marker.className = 'split-marker absolute -right-3 top-0 bottom-0 w-6 flex items-center justify-center z-20 pointer-events-none';
marker.innerHTML = '<div class="h-full w-0.5 border-l-2 border-dashed border-blue-400"></div>';
card.appendChild(marker);
}
return card;
} }
function setupSortable() { function setupSortable() {
@@ -662,7 +600,6 @@ function duplicatePage(index: number) {
const newPageData: PageData = { const newPageData: PageData = {
...originalPageData, ...originalPageData,
id: generateId(), // New ID for the duplicate
canvas: newCanvas, canvas: newCanvas,
}; };
@@ -706,66 +643,18 @@ async function handleInsertPdf(e: Event) {
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLibDocument.load(arrayBuffer); const pdfDoc = await PDFLibDocument.load(arrayBuffer);
currentPdfDocs.push(pdfDoc); currentPdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1;
// Load PDF.js document for rendering
const pdfBytes = await pdfDoc.save();
const pdfjsDoc = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
const numPages = pdfjsDoc.numPages;
const numPages = pdfDoc.getPageCount();
const newPages: PageData[] = []; const newPages: PageData[] = [];
for (let i = 0; i < numPages; i++) { for (let i = 0; i < numPages; i++) {
newPages.push({ // Use the existing renderPage function, which adds to allPages
id: generateId(), await renderPage(pdfDoc, i, currentPdfDocs.length - 1);
pdfIndex, // Move the newly added page data to the temporary array
pageIndex: i, newPages.push(allPages.pop()!);
rotation: 0,
visualRotation: 0,
canvas: null, // Placeholder
pdfDoc,
originalPageIndex: i,
});
} }
// Insert new pages into allPages
allPages.splice(insertAfterIndex + 1, 0, ...newPages); allPages.splice(insertAfterIndex + 1, 0, ...newPages);
// Update display to show placeholders immediately
updatePageDisplay(); updatePageDisplay();
// Render pages progressively
for (let i = 0; i < numPages; i++) {
const globalIndex = insertAfterIndex + 1 + i;
// Render page
const canvas = await renderPageToCanvas(pdfjsDoc, i + 1);
// Update data
if (allPages[globalIndex]) {
allPages[globalIndex].canvas = canvas;
// Update UI if card exists
const pagesContainer = document.getElementById('pages-container');
const card = pagesContainer?.querySelector(`div[data-page-index="${globalIndex}"]`);
if (card) {
const preview = card.querySelector('.bg-gray-700') || card.querySelector('.bg-white');
if (preview) {
// Re-create the preview content
preview.innerHTML = '';
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative';
(preview as HTMLElement).style.minHeight = '160px';
(preview as HTMLElement).style.height = '250px';
const previewCanvas = canvas;
previewCanvas.className = 'max-w-full max-h-full object-contain';
previewCanvas.style.transform = `rotate(${allPages[globalIndex].visualRotation}deg)`;
previewCanvas.style.transition = 'transform 0.2s ease';
preview.appendChild(previewCanvas);
}
}
}
}
} catch (e) { } catch (e) {
console.error('Failed to insert PDF:', e); console.error('Failed to insert PDF:', e);
showModal('Error', 'Failed to insert PDF. The file may be corrupted.', 'error'); showModal('Error', 'Failed to insert PDF. The file may be corrupted.', 'error');
@@ -806,7 +695,6 @@ function addBlankPage() {
} }
const blankPageData: PageData = { const blankPageData: PageData = {
id: generateId(),
pdfIndex: -1, pdfIndex: -1,
pageIndex: -1, pageIndex: -1,
rotation: 0, rotation: 0,
@@ -828,26 +716,11 @@ function bulkRotate(delta: number) {
selectedPages.forEach(index => { selectedPages.forEach(index => {
const pageData = allPages[index]; const pageData = allPages[index];
if (pageData) {
// Update state
pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360; pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360;
pageData.rotation = (pageData.rotation + delta + 360) % 360; pageData.rotation = (pageData.rotation + delta + 360) % 360;
// Update DOM immediately if it exists
const pagesContainer = document.getElementById('pages-container');
const card = pagesContainer?.querySelector(`div[data-page-index="${index}"]`);
if (card) {
const canvas = card.querySelector('canvas');
if (canvas) {
canvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
}
// If no canvas (placeholder), the state update is enough.
// When it eventually renders, createPageElement will use the new rotation.
}
}
}); });
// TODO@ALAM - Do NOT call updatePageDisplay() as it destroys lazy loading observers updatePageDisplay();
} }
function bulkDelete() { function bulkDelete() {
@@ -896,6 +769,14 @@ function bulkSplit() {
updatePageDisplay(); updatePageDisplay();
} }
async function bulkDownload() {
if (selectedPages.size === 0) {
showModal('No Selection', 'Please select pages to download.', 'info');
return;
}
const indices = Array.from(selectedPages);
await downloadPagesAsPdf(indices, 'selected-pages.pdf');
}
async function downloadAll() { async function downloadAll() {
if (allPages.length === 0) { if (allPages.length === 0) {
@@ -945,10 +826,6 @@ async function downloadSplitPdfs() {
for (const index of segment) { for (const index of segment) {
const pageData = allPages[index]; const pageData = allPages[index];
if (!pageData) {
console.warn(`Page data missing for index ${index}`);
continue;
}
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) { if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]); const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]);
const page = newPdf.addPage(copiedPage); const page = newPdf.addPage(copiedPage);
@@ -974,8 +851,6 @@ async function downloadSplitPdfs() {
} catch (e) { } catch (e) {
console.error('Failed to create split PDFs:', e); console.error('Failed to create split PDFs:', e);
showModal('Error', 'Failed to create split PDFs.', 'error'); showModal('Error', 'Failed to create split PDFs.', 'error');
} finally {
hideLoading(); // Ensure loader is hidden if we used it (though showModal replaces it)
} }
} }
@@ -985,10 +860,6 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
for (const index of indices) { for (const index of indices) {
const pageData = allPages[index]; const pageData = allPages[index];
if (!pageData) {
console.warn(`Page data missing for index ${index}`);
continue;
}
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) { if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
// Copy page from original PDF // Copy page from original PDF
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]); const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]);
@@ -1007,7 +878,6 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }); const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
downloadFile(blob, filename); downloadFile(blob, filename);
showModal('Success', 'PDF downloaded successfully.', 'success');
} catch (e) { } catch (e) {
console.error('Failed to create PDF:', e); console.error('Failed to create PDF:', e);
showModal('Error', 'Failed to create PDF.', 'error'); showModal('Error', 'Failed to create PDF.', 'error');
@@ -1018,124 +888,15 @@ function updatePageDisplay() {
const pagesContainer = document.getElementById('pages-container'); const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return; if (!pagesContainer) return;
// 1. Create a map of existing elements by ID pagesContainer.innerHTML = '';
const existingElements = new Map<string, HTMLElement>();
Array.from(pagesContainer.children).forEach((child) => {
const el = child as HTMLElement;
if (el.dataset.pageId) {
existingElements.set(el.dataset.pageId, el);
}
});
// 2. Iterate through allPages and reconcile DOM
allPages.forEach((pageData, index) => { allPages.forEach((pageData, index) => {
let card = existingElements.get(pageData.id); createPageCard(pageData, index);
if (card) {
// Element exists, update it
existingElements.delete(pageData.id); // Remove from map so we know it's still in use
// Check if position changed
if (pagesContainer.children[index] !== card) {
if (index < pagesContainer.children.length) {
pagesContainer.insertBefore(card, pagesContainer.children[index]);
} else {
pagesContainer.appendChild(card);
}
}
// Update index-dependent attributes
card.dataset.pageIndex = index.toString();
const info = card.querySelector('.text-xs.text-gray-400.text-center.mb-2');
if (info) info.textContent = `Page ${index + 1} `;
// Update selection state
const selectBtn = card.querySelector('button[class*="absolute top-2 right-2"]');
if (selectBtn) {
if (selectedPages.has(index)) {
card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500');
selectBtn.innerHTML = '<i data-lucide="check-square" class="w-4 h-4 text-indigo-400"></i>';
} else {
card.classList.remove('border-indigo-500', 'ring-2', 'ring-indigo-500');
selectBtn.innerHTML = '<i data-lucide="square" class="w-4 h-4 text-gray-200"></i>';
}
// Update click handler to use new index
(selectBtn as HTMLElement).onclick = (e) => {
e.stopPropagation();
toggleSelectOptimized(index);
};
}
// Update action buttons
const actionsInner = card.querySelector('.flex.items-center.gap-1.bg-gray-900\\/90');
if (actionsInner) {
const buttons = actionsInner.querySelectorAll('button');
if (buttons[0]) (buttons[0] as HTMLElement).onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); };
if (buttons[1]) (buttons[1] as HTMLElement).onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); };
if (buttons[2]) (buttons[2] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); duplicatePage(index); };
if (buttons[3]) (buttons[3] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); insertPdfAfter(index); };
if (buttons[4]) (buttons[4] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); toggleSplitMarker(index); renderSplitMarkers(); };
if (buttons[5]) (buttons[5] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); deletePage(index); };
}
} else {
// Element doesn't exist, create it
card = createPageElement(pageData.canvas, index);
card.dataset.pageId = pageData.id; // IMPORTANT: Set the ID
if (index < pagesContainer.children.length) {
pagesContainer.insertBefore(card, pagesContainer.children[index]);
} else {
pagesContainer.appendChild(card);
}
}
}); });
// 3. Remove remaining elements (deleted pages)
existingElements.forEach((el) => el.remove());
setupSortable(); setupSortable();
renderSplitMarkers(); renderSplitMarkers();
createIcons({ icons }); createIcons({ icons });
} }
function updatePageNumbers() { function updatePageNumbers() {
const pagesContainer = document.getElementById('pages-container'); updatePageDisplay();
if (!pagesContainer) return;
const cards = Array.from(pagesContainer.children) as HTMLElement[];
cards.forEach((card, index) => {
// Update data attribute
card.dataset.pageIndex = index.toString();
// Update visible page number text
const info = card.querySelector('.text-xs.text-gray-400.text-center.mb-2');
if (info) {
info.textContent = `Page ${index + 1} `;
}
// Re-attach event listeners for buttons
// We need to find the buttons and update their onclick handlers
// This is necessary because the original handlers captured the old index
const selectBtn = card.querySelector('button[class*="absolute top-2 right-2"]') as HTMLButtonElement;
if (selectBtn) {
selectBtn.onclick = (e) => {
e.stopPropagation();
toggleSelectOptimized(index);
};
}
const actionsInner = card.querySelector('.flex.items-center.gap-1.bg-gray-900\\/90');
if (actionsInner) {
const buttons = actionsInner.querySelectorAll('button');
// Order: Rotate Left, Rotate Right, Duplicate, Insert, Split, Delete
if (buttons[0]) buttons[0].onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); };
if (buttons[1]) buttons[1].onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); };
if (buttons[2]) buttons[2].onclick = (e) => { e.stopPropagation(); snapshot(); duplicatePage(index); };
if (buttons[3]) buttons[3].onclick = (e) => { e.stopPropagation(); snapshot(); insertPdfAfter(index); };
if (buttons[4]) buttons[4].onclick = (e) => { e.stopPropagation(); snapshot(); toggleSplitMarker(index); renderSplitMarkers(); };
if (buttons[5]) buttons[5].onclick = (e) => { e.stopPropagation(); snapshot(); deletePage(index); };
}
});
} }
+6 -6
View File
@@ -1,7 +1,6 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js'; import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { getRotationState } from '../handlers/fileHandler.js';
import { degrees } from 'pdf-lib'; import { degrees } from 'pdf-lib';
@@ -9,11 +8,12 @@ export async function rotate() {
showLoader('Applying rotations...'); showLoader('Applying rotations...');
try { try {
const pages = state.pdfDoc.getPages(); const pages = state.pdfDoc.getPages();
const rotationStateArray = getRotationState(); document.querySelectorAll('.page-rotator-item').forEach((item) => {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
// Apply rotations from state (not DOM) to ensure all pages including lazy-loaded ones are rotated const pageIndex = parseInt(item.dataset.pageIndex);
rotationStateArray.forEach((rotation, pageIndex) => { // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
if (rotation !== 0 && pages[pageIndex]) { const rotation = parseInt(item.dataset.rotation || '0');
if (rotation !== 0) {
const currentRotation = pages[pageIndex].getRotation().angle; const currentRotation = pages[pageIndex].getRotation().angle;
pages[pageIndex].setRotation(degrees(currentRotation + rotation)); pages[pageIndex].setRotation(degrees(currentRotation + rotation));
} }
+5 -21
View File
@@ -40,21 +40,9 @@ export async function setupSignTool() {
const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }); const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob); const blobUrl = URL.createObjectURL(blob);
try { const viewerBase = '/pdfjs-viewer/viewer.html';
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
delete (existingPrefs as any).annotationEditorMode;
const newPrefs = {
...existingPrefs,
enableSignatureEditor: true,
enablePermissions: false,
};
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs));
} catch {}
const viewerUrl = new URL('/pdfjs-viewer/viewer.html', window.location.origin);
const query = new URLSearchParams({ file: blobUrl }); const query = new URLSearchParams({ file: blobUrl });
iframe.src = `${viewerUrl.toString()}?${query.toString()}`; iframe.src = `${viewerBase}?${query.toString()}`;
iframe.onload = () => { iframe.onload = () => {
hideLoader(); hideLoader();
@@ -64,27 +52,23 @@ export async function setupSignTool() {
if (viewerWindow && viewerWindow.PDFViewerApplication) { if (viewerWindow && viewerWindow.PDFViewerApplication) {
const app = viewerWindow.PDFViewerApplication; const app = viewerWindow.PDFViewerApplication;
const doc = viewerWindow.document; const doc = viewerWindow.document;
const eventBus = app.eventBus;
eventBus?._on('annotationeditoruimanager', () => {
const editorModeButtons = doc.getElementById('editorModeButtons'); const editorModeButtons = doc.getElementById('editorModeButtons');
editorModeButtons?.classList.remove('hidden'); editorModeButtons?.classList.remove('hidden');
const editorSignature = doc.getElementById('editorSignature'); const editorSignature = doc.getElementById('editorSignature');
editorSignature?.removeAttribute('hidden'); editorSignature?.removeAttribute('hidden');
const editorSignatureButton = doc.getElementById('editorSignatureButton') as HTMLButtonElement | null; const editorSignatureButton = doc.getElementById('editorSignatureButton') as HTMLButtonElement | null;
if (editorSignatureButton) { if (editorSignatureButton) {
editorSignatureButton.disabled = false; editorSignatureButton.disabled = false;
} }
const editorStamp = doc.getElementById('editorStamp'); const editorStamp = doc.getElementById('editorStamp');
editorStamp?.removeAttribute('hidden'); editorStamp?.removeAttribute('hidden');
const editorStampButton = doc.getElementById('editorStampButton') as HTMLButtonElement | null; const editorStampButton = doc.getElementById('editorStampButton') as HTMLButtonElement | null;
if (editorStampButton) { if (editorStampButton) {
editorStampButton.disabled = false; editorStampButton.disabled = false;
} }
try {
const highlightBtn = doc.getElementById('editorHighlightButton') as HTMLButtonElement | null;
highlightBtn?.click();
} catch {}
});
} }
} catch (e) { } catch (e) {
console.error('Could not initialize base PDF.js viewer for signing:', e); console.error('Could not initialize base PDF.js viewer for signing:', e);
+16 -32
View File
@@ -1,9 +1,6 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { createIcons, icons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
import { downloadFile } from '../utils/helpers.js'; import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
@@ -20,29 +17,35 @@ async function renderVisualSelector() {
container.textContent = ''; container.textContent = '';
// Cleanup any previous lazy loading observers
cleanupLazyRendering();
showLoader('Rendering page previews...'); showLoader('Rendering page previews...');
try { try {
const pdfData = await state.pdfDoc.save(); const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise; const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
// Function to create wrapper element for each page for (let i = 1; i <= pdf.numPages; i++) {
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 0.4 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: canvas.getContext('2d'),
viewport: viewport,
}).promise;
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = wrapper.className =
'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500'; 'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.pageIndex = pageNumber - 1; wrapper.dataset.pageIndex = i - 1;
const img = document.createElement('img'); const img = document.createElement('img');
img.src = canvas.toDataURL(); img.src = canvas.toDataURL();
img.className = 'rounded-md w-full h-auto'; img.className = 'rounded-md w-full h-auto';
const p = document.createElement('p'); const p = document.createElement('p');
p.className = 'text-center text-xs mt-1 text-gray-300'; p.className = 'text-center text-xs mt-1 text-gray-300';
p.textContent = `Page ${pageNumber}`; p.textContent = `Page ${i}`;
wrapper.append(img, p); wrapper.append(img, p);
const handleSelection = (e: any) => { const handleSelection = (e: any) => {
@@ -66,31 +69,12 @@ async function renderVisualSelector() {
wrapper.addEventListener('touchstart', (e) => { wrapper.addEventListener('touchstart', (e) => {
e.preventDefault(); e.preventDefault();
}); });
container.appendChild(wrapper);
return wrapper;
};
// Render pages progressively with lazy loading
await renderPagesProgressively(
pdf,
container,
createWrapper,
{
batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '400px',
onProgress: (current, total) => {
showLoader(`Rendering page previews: ${current}/${total}`);
},
onBatchComplete: () => {
createIcons({ icons });
} }
}
);
} catch (error) { } catch (error) {
console.error('Error rendering visual selector:', error); console.error('Error rendering visual selector:', error);
showAlert('Error', 'Failed to render page previews.'); showAlert('Error', 'Failed to render page previews.');
// Reset the flag on error so the user can try again. // 4. ADDED: Reset the flag on error so the user can try again.
visualSelectorRendered = false; visualSelectorRendered = false;
} finally { } finally {
hideLoader(); hideLoader();
+3 -6
View File
@@ -5,7 +5,6 @@ import { createIcons, icons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import '../css/styles.css'; import '../css/styles.css';
import { formatStars } from './utils/helpers.js'; import { formatStars } from './utils/helpers.js';
import { APP_VERSION, injectVersion } from '../version.js';
const init = () => { const init = () => {
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
@@ -29,11 +28,9 @@ const init = () => {
simpleNav.innerHTML = ` simpleNav.innerHTML = `
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-start items-center h-16"> <div class="flex justify-start items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div class="flex-shrink-0 flex items-center">
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8"> <img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">BentoPDF</span>
<a href="index.html">BentoPDF</a>
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -97,7 +94,7 @@ const init = () => {
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version-simple">${APP_VERSION}</span> Version <span id="app-version-simple">1.6.2</span>
</p> </p>
</div> </div>
`; `;
+19 -97
View File
@@ -1,10 +1,8 @@
import { resetState } from './state.js'; import { resetState } from './state.js';
import { formatBytes } from './utils/helpers.js'; import { formatBytes } from './utils/helpers.js';
import { tesseractLanguages } from './config/tesseract-languages.js'; import { tesseractLanguages } from './config/tesseract-languages.js';
import { renderPagesProgressively, cleanupLazyRendering } from './utils/render-utils.js';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { getRotationState } from './handlers/fileHandler.js';
// Centralizing DOM element selection // Centralizing DOM element selection
export const dom = { export const dom = {
@@ -113,21 +111,25 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
if (!container) return; if (!container) return;
container.innerHTML = ''; container.innerHTML = '';
// Cleanup any previous lazy loading observers
cleanupLazyRendering();
showLoader('Rendering page previews...'); showLoader('Rendering page previews...');
const pdfData = await pdfDoc.save(); const pdfData = await pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'. // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise; const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
// Function to create wrapper element for each page for (let i = 1; i <= pdf.numPages; i++) {
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 0.5 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise;
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = 'page-thumbnail relative group';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.pageIndex = pageNumber - 1; wrapper.dataset.pageIndex = i - 1;
const imgContainer = document.createElement('div'); const imgContainer = document.createElement('div');
imgContainer.className = imgContainer.className =
@@ -146,7 +148,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
const pageNumSpan = document.createElement('span'); const pageNumSpan = document.createElement('span');
pageNumSpan.className = pageNumSpan.className =
'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1'; 'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
pageNumSpan.textContent = pageNumber.toString(); pageNumSpan.textContent = i.toString();
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
deleteBtn.className = deleteBtn.className =
@@ -154,36 +156,14 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
deleteBtn.innerHTML = '&times;'; deleteBtn.innerHTML = '&times;';
deleteBtn.addEventListener('click', (e) => { deleteBtn.addEventListener('click', (e) => {
(e.currentTarget as HTMLElement).parentElement.remove(); (e.currentTarget as HTMLElement).parentElement.remove();
// Renumber remaining pages
const pages = container.querySelectorAll('.page-thumbnail');
pages.forEach((page, index) => {
const numSpan = page.querySelector('span');
if (numSpan) {
numSpan.textContent = (index + 1).toString();
}
});
initializeOrganizeSortable(containerId); initializeOrganizeSortable(containerId);
}); });
wrapper.append(pageNumSpan, deleteBtn); wrapper.append(pageNumSpan, deleteBtn);
} else if (toolId === 'rotate') { } else if (toolId === 'rotate') {
wrapper.className = 'page-rotator-item flex flex-col items-center gap-2'; wrapper.className = 'page-rotator-item flex flex-col items-center gap-2';
wrapper.dataset.rotation = '0';
// Read rotation from state (handles "Rotate All" on lazy-loaded pages)
const rotationStateArray = getRotationState();
const pageIndex = pageNumber - 1;
const initialRotation = rotationStateArray[pageIndex] || 0;
wrapper.dataset.rotation = initialRotation.toString();
img.classList.add('transition-transform', 'duration-300'); img.classList.add('transition-transform', 'duration-300');
// Apply initial rotation if any
if (initialRotation !== 0) {
img.style.transform = `rotate(${initialRotation}deg)`;
}
wrapper.appendChild(imgContainer); wrapper.appendChild(imgContainer);
const controlsDiv = document.createElement('div'); const controlsDiv = document.createElement('div');
@@ -191,7 +171,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
const pageNumSpan = document.createElement('span'); const pageNumSpan = document.createElement('span');
pageNumSpan.className = 'font-medium text-sm text-white'; pageNumSpan.className = 'font-medium text-sm text-white';
pageNumSpan.textContent = pageNumber.toString(); pageNumSpan.textContent = i.toString();
const rotateBtn = document.createElement('button'); const rotateBtn = document.createElement('button');
rotateBtn.className = rotateBtn.className =
@@ -214,40 +194,15 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
wrapper.appendChild(controlsDiv); wrapper.appendChild(controlsDiv);
} }
return wrapper; container.appendChild(wrapper);
};
try {
// Render pages progressively with lazy loading
await renderPagesProgressively(
pdf,
container,
createWrapper,
{
batchSize: 6,
useLazyLoading: true,
lazyLoadMargin: '300px',
onProgress: (current, total) => {
showLoader(`Rendering page previews: ${current}/${total}`);
},
onBatchComplete: () => {
createIcons({ icons }); createIcons({ icons });
} }
}
);
if (toolId === 'organize') { if (toolId === 'organize') {
initializeOrganizeSortable(containerId); initializeOrganizeSortable(containerId);
} }
// Reinitialize lucide icons for dynamically added elements
createIcons({ icons });
} catch (error) {
console.error('Error rendering page thumbnails:', error);
showAlert('Error', 'Failed to render page thumbnails');
} finally {
hideLoader(); hideLoader();
}
}; };
/** /**
@@ -296,7 +251,8 @@ const createFileInputHTML = (options = {}) => {
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" ${multiple} accept="${acceptedFiles}"> <input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" ${multiple} accept="${acceptedFiles}">
</div> </div>
${showControls ${
showControls
? ` ? `
<!-- NEW: Add control buttons for multi-file uploads --> <!-- NEW: Add control buttons for multi-file uploads -->
<div id="file-controls" class="hidden mt-4 flex gap-3"> <div id="file-controls" class="hidden mt-4 flex gap-3">
@@ -1032,14 +988,8 @@ export const toolTemplates = {
'image-to-pdf': () => ` 'image-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">Image to PDF Converter</h2> <h2 class="text-2xl font-bold text-white mb-4">Image to PDF Converter</h2>
<p class="mb-4 text-gray-400">Combine multiple images into a single PDF. Drag and drop to reorder.</p> <p class="mb-6 text-gray-400">Combine multiple images into a single PDF. Drag and drop to reorder.</p>
${createFileInputHTML({ multiple: true, accept: 'image/jpeg,image/png,image/webp', showControls: true })}
<div class="mb-6 p-3 bg-gray-900/50 border border-gray-700 rounded-lg">
<p class="text-sm text-gray-300 mb-2"><strong class="text-white">Supported Formats:</strong></p>
<p class="text-xs text-gray-400">JPG, PNG, WebP, BMP, TIFF, SVG, HEIC/HEIF</p>
</div>
${createFileInputHTML({ multiple: true, accept: 'image/jpeg,image/png,image/webp,image/bmp,image/tiff,image/svg+xml', showControls: true })}
<ul id="image-list" class="mt-4 grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-4"> <ul id="image-list" class="mt-4 grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-4">
</ul> </ul>
<div id="image-to-pdf-options" class="hidden mt-6"> <div id="image-to-pdf-options" class="hidden mt-6">
@@ -2034,34 +1984,6 @@ export const toolTemplates = {
<div id="attachment-file-list" class="mt-4 space-y-2"></div> <div id="attachment-file-list" class="mt-4 space-y-2"></div>
<div id="attachment-level-options" class="hidden mt-6 space-y-4">
<div>
<h3 class="text-lg font-semibold text-white mb-2">Attachment Level</h3>
<div class="flex gap-4 p-2 rounded-lg bg-gray-900">
<label class="flex-1 flex items-center gap-2 p-3 rounded-md hover:bg-gray-700 cursor-pointer">
<input type="radio" name="attachment-level" value="document" checked class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
<div>
<span class="font-semibold text-white">Document Level</span>
<p class="text-xs text-gray-400">Attach to the entire document</p>
</div>
</label>
<label class="flex-1 flex items-center gap-2 p-3 rounded-md hover:bg-gray-700 cursor-pointer">
<input type="radio" name="attachment-level" value="page" class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
<div>
<span class="font-semibold text-white">Page Level</span>
<p class="text-xs text-gray-400">Attach to specific pages</p>
</div>
</label>
</div>
</div>
<div id="page-range-wrapper" class="hidden">
<label for="attachment-page-range" class="block mb-2 text-sm font-medium text-gray-300">Page Range</label>
<input type="text" id="attachment-page-range" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="e.g., 1-3, 5, 8">
<p class="text-xs text-gray-400 mt-1">Attachments will be added to each page in this range. Total Pages: <span id="attachment-total-pages"></span></p>
</div>
</div>
<button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Embed Files & Download</button> <button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Embed Files & Download</button>
</div> </div>
`, `,
-379
View File
@@ -1,379 +0,0 @@
import * as pdfjsLib from 'pdfjs-dist';
/**
* Configuration for progressive rendering
*/
export interface RenderConfig {
batchSize?: number;
useLazyLoading?: boolean;
lazyLoadMargin?: string;
eagerLoadBatches?: number; // Number of batches to load ahead eagerly (default: 1)
onProgress?: (current: number, total: number) => void;
onPageRendered?: (pageIndex: number, element: HTMLElement) => void;
onBatchComplete?: () => void;
shouldCancel?: () => boolean;
}
/**
* Page rendering task
*/
interface PageTask {
pageNumber: number;
pdfjsDoc: any;
fileName?: string;
container: HTMLElement;
scale?: number;
createWrapper: (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => HTMLElement;
}
/**
* Lazy loading state
*/
interface LazyLoadState {
observer: IntersectionObserver | null;
pendingTasks: Map<HTMLElement, PageTask>;
isRendering: boolean;
eagerLoadQueue: PageTask[];
nextEagerIndex: number;
}
const lazyLoadState: LazyLoadState = {
observer: null,
pendingTasks: new Map(),
isRendering: false,
eagerLoadQueue: [],
nextEagerIndex: 0,
};
/**
* Creates a placeholder element for a page that will be lazy-loaded
*/
export function createPlaceholder(pageNumber: number, fileName?: string): HTMLElement {
const placeholder = document.createElement('div');
placeholder.className =
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 rounded-lg bg-gray-800 transition-colors';
placeholder.dataset.pageNumber = pageNumber.toString();
if (fileName) {
placeholder.dataset.fileName = fileName;
}
placeholder.dataset.lazyLoad = 'true';
// Create skeleton loader
const skeletonContainer = document.createElement('div');
skeletonContainer.className = 'relative w-full h-36 bg-gray-700 rounded-md animate-pulse flex items-center justify-center';
const loadingText = document.createElement('span');
loadingText.className = 'text-gray-500 text-xs';
loadingText.textContent = 'Loading...';
skeletonContainer.appendChild(loadingText);
placeholder.appendChild(skeletonContainer);
return placeholder;
}
/**
* Renders a single page to canvas
*/
export async function renderPageToCanvas(
pdfjsDoc: any,
pageNumber: number,
scale: number = 0.5
): Promise<HTMLCanvasElement> {
const page = await pdfjsDoc.getPage(pageNumber);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d')!;
await page.render({
canvasContext: context,
canvas: canvas,
viewport,
}).promise;
return canvas;
}
/**
* Renders a batch of pages in parallel
*/
async function renderPageBatch(
tasks: PageTask[],
onProgress?: (current: number, total: number) => void
): Promise<void> {
const renderPromises = tasks.map(async (task) => {
try {
const canvas = await renderPageToCanvas(
task.pdfjsDoc,
task.pageNumber,
task.scale || 0.5
);
const wrapper = task.createWrapper(canvas, task.pageNumber, task.fileName);
// Find and replace the placeholder for this specific page number
const placeholder = task.container.querySelector(
`[data-page-number="${task.pageNumber}"][data-lazy-load="true"]`
);
if (placeholder) {
// Replace placeholder with rendered page
task.container.replaceChild(wrapper, placeholder);
} else {
// Fallback: shouldn't happen with new approach, but just in case
console.warn(`No placeholder found for page ${task.pageNumber}, appending instead`);
task.container.appendChild(wrapper);
}
return wrapper;
} catch (error) {
console.error(`Error rendering page ${task.pageNumber}:`, error);
return null;
}
});
await Promise.all(renderPromises);
}
/**
* Sets up Intersection Observer for lazy loading
*/
function setupLazyRendering(
container: HTMLElement,
config: RenderConfig
): IntersectionObserver {
const options = {
root: container.closest('.overflow-auto') || null,
rootMargin: config.lazyLoadMargin || '200px',
threshold: 0.01,
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const placeholder = entry.target as HTMLElement;
const task = lazyLoadState.pendingTasks.get(placeholder);
if (task) {
// Immediately unobserve to prevent multiple triggers
observer.unobserve(placeholder);
lazyLoadState.pendingTasks.delete(placeholder);
// Render this page immediately (not waiting for isRendering flag)
renderPageBatch([task], config.onProgress)
.then(() => {
// Trigger callback after lazy load batch
if (config.onBatchComplete) {
config.onBatchComplete();
}
// Check if all pages are rendered
if (lazyLoadState.pendingTasks.size === 0 && lazyLoadState.observer) {
lazyLoadState.observer.disconnect();
lazyLoadState.observer = null;
}
})
.catch((error) => {
console.error(`Error lazy loading page ${task.pageNumber}:`, error);
});
}
}
});
}, options);
lazyLoadState.observer = observer;
return observer;
}
/**
* Request idle callback with fallback
*/
function requestIdleCallbackPolyfill(callback: () => void): void {
if ('requestIdleCallback' in window) {
requestIdleCallback(callback);
} else {
setTimeout(callback, 16); // ~60fps
}
}
/**
* Main function to render pages progressively with optional lazy loading
*/
export async function renderPagesProgressively(
pdfjsDoc: any,
container: HTMLElement,
createWrapper: (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => HTMLElement,
config: RenderConfig = {}
): Promise<void> {
const {
batchSize = 8, // Increased from 5 to 8 for faster initial render
useLazyLoading = true,
eagerLoadBatches = 1, // Eagerly load 1 batch ahead by default
onProgress,
onBatchComplete,
} = config;
const totalPages = pdfjsDoc.numPages;
// Render more pages initially to reduce lazy loading issues
const initialRenderCount = useLazyLoading
? Math.min(20, totalPages) // Increased from 12 to 20 pages
: totalPages;
// CRITICAL FIX: Create placeholders for ALL pages first to maintain order
const placeholders: HTMLElement[] = [];
for (let i = 1; i <= totalPages; i++) {
const placeholder = createPlaceholder(i);
container.appendChild(placeholder);
placeholders.push(placeholder);
}
const tasks: PageTask[] = [];
// Create tasks for all pages
for (let i = 1; i <= totalPages; i++) {
tasks.push({
pageNumber: i,
pdfjsDoc,
container,
scale: config.useLazyLoading ? 0.3 : 0.5,
createWrapper,
});
}
// If lazy loading is enabled, set up observer for pages beyond initial render
if (useLazyLoading && totalPages > initialRenderCount) {
const observer = setupLazyRendering(container, config);
for (let i = initialRenderCount + 1; i <= totalPages; i++) {
const placeholder = placeholders[i - 1];
// Store the task for lazy rendering
lazyLoadState.pendingTasks.set(placeholder, tasks[i - 1]);
observer.observe(placeholder);
}
// Prepare eager load queue
const eagerStartIndex = initialRenderCount;
const eagerEndIndex = Math.min(
eagerStartIndex + (eagerLoadBatches * batchSize),
totalPages
);
lazyLoadState.eagerLoadQueue = tasks.slice(eagerStartIndex, eagerEndIndex);
lazyLoadState.nextEagerIndex = 0;
}
// Render initial pages in batches
const initialTasks = tasks.slice(0, initialRenderCount);
for (let i = 0; i < initialTasks.length; i += batchSize) {
if (config.shouldCancel?.()) return;
const batch = initialTasks.slice(i, i + batchSize);
await new Promise<void>((resolve) => {
requestIdleCallbackPolyfill(async () => {
await renderPageBatch(batch, onProgress);
if (onProgress) {
onProgress(Math.min(i + batchSize, initialRenderCount), totalPages);
}
if (onBatchComplete) {
onBatchComplete();
}
resolve();
});
});
}
// Start eager loading AFTER initial batch is complete
if (useLazyLoading && eagerLoadBatches > 0 && totalPages > initialRenderCount) {
renderEagerBatch(config);
}
}
/**
* Manually observe a placeholder element (useful for dynamically created placeholders)
*/
export function observePlaceholder(
placeholder: HTMLElement,
task: PageTask
): void {
if (!lazyLoadState.observer) {
console.warn('No active observer to register placeholder');
return;
}
lazyLoadState.pendingTasks.set(placeholder, task);
lazyLoadState.observer.observe(placeholder);
}
/**
* Eagerly renders the next batch in the background
*/
function renderEagerBatch(config: RenderConfig): void {
const { eagerLoadBatches = 1, batchSize = 8 } = config;
if (eagerLoadBatches <= 0 || lazyLoadState.eagerLoadQueue.length === 0) {
return;
}
if (config.shouldCancel?.()) return;
const { nextEagerIndex, eagerLoadQueue } = lazyLoadState;
if (nextEagerIndex >= eagerLoadQueue.length) {
return; // All eager batches rendered
}
const batchEnd = Math.min(nextEagerIndex + batchSize, eagerLoadQueue.length);
const batch = eagerLoadQueue.slice(nextEagerIndex, batchEnd);
requestIdleCallbackPolyfill(async () => {
if (config.shouldCancel?.()) return;
// Remove these tasks from pending since we're rendering them eagerly
batch.forEach(task => {
const placeholder = Array.from(lazyLoadState.pendingTasks.entries())
.find(([_, t]) => t.pageNumber === task.pageNumber)?.[0];
if (placeholder && lazyLoadState.observer) {
lazyLoadState.observer.unobserve(placeholder);
lazyLoadState.pendingTasks.delete(placeholder);
}
});
await renderPageBatch(batch, config.onProgress);
if (config.onBatchComplete) {
config.onBatchComplete();
}
// Update next eager index
lazyLoadState.nextEagerIndex = batchEnd;
// Queue next eager batch
const remainingBatches = Math.ceil((eagerLoadQueue.length - batchEnd) / batchSize);
if (remainingBatches > 0 && remainingBatches < eagerLoadBatches) {
// Continue eager loading if we have more batches within the eager threshold
renderEagerBatch(config);
}
});
}
/**
* Cleanup function to disconnect observers
*/
export function cleanupLazyRendering(): void {
if (lazyLoadState.observer) {
lazyLoadState.observer.disconnect();
lazyLoadState.observer = null;
}
lazyLoadState.pendingTasks.clear();
lazyLoadState.isRendering = false;
lazyLoadState.eagerLoadQueue = [];
lazyLoadState.nextEagerIndex = 0;
}
+1 -2
View File
@@ -117,7 +117,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -186,7 +186,6 @@
</div> </div>
</footer> </footer>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../version.ts"></script>
<script type="module" src="../js/logic/add-stamps.ts"></script> <script type="module" src="../js/logic/add-stamps.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
+1 -2
View File
@@ -420,7 +420,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -489,7 +489,6 @@
</div> </div>
</footer> </footer>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../version.ts"></script>
<script type="module" src="../js/logic/bookmark-pdf.ts"></script> <script type="module" src="../js/logic/bookmark-pdf.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
+1 -2
View File
@@ -116,7 +116,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -186,7 +186,6 @@
</footer> </footer>
<script type="module" src="../js/logic/json-to-pdf.ts"></script> <script type="module" src="../js/logic/json-to-pdf.ts"></script>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../version.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
+1 -2
View File
@@ -110,7 +110,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -180,7 +180,6 @@
</footer> </footer>
<script type="module" src="../js/logic/pdf-to-json.ts"></script> <script type="module" src="../js/logic/pdf-to-json.ts"></script>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../version.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
-18
View File
@@ -1,18 +0,0 @@
import packageJson from '../package.json';
export const APP_VERSION = packageJson.version;
export function injectVersion() {
const versionElements = document.querySelectorAll('#app-version, #app-version-simple');
versionElements.forEach((element) => {
element.textContent = APP_VERSION;
});
}
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectVersion);
} else {
injectVersion();
}
}
+2 -2
View File
@@ -234,7 +234,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -303,7 +303,7 @@
</div> </div>
</footer> </footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
-1
View File
@@ -54,7 +54,6 @@
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
/* Path aliases (optional but helpful) */ /* Path aliases (optional but helpful) */
"baseUrl": ".", "baseUrl": ".",
-8
View File
@@ -29,13 +29,6 @@ export default defineConfig(({ mode }) => ({
exclude: ['coherentpdf'], exclude: ['coherentpdf'],
}, },
server: { server: {
host: true,
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
preview: {
headers: { headers: {
'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp', 'Cross-Origin-Embedder-Policy': 'require-corp',
@@ -58,7 +51,6 @@ export default defineConfig(({ mode }) => ({
'pdf-to-json': resolve(__dirname, 'src/pages/pdf-to-json.html'), 'pdf-to-json': resolve(__dirname, 'src/pages/pdf-to-json.html'),
'json-to-pdf': resolve(__dirname, 'src/pages/json-to-pdf.html'), 'json-to-pdf': resolve(__dirname, 'src/pages/json-to-pdf.html'),
'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'), 'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'),
'add-stamps': resolve(__dirname, 'src/pages/add-stamps.html'),
}, },
}, },
}, },