-
${title}
-
${message}
-
-
OK
+
+
${title}
+
${message}
+
+ OK
-
- `;
+
+ `;
overlay.appendChild(modal);
modalContainer.appendChild(overlay);
@@ -832,7 +833,7 @@ deleteAllBtn.addEventListener('click', async () => {
}
const confirmed = await showConfirmModal(
- `Delete all ${bookmarkTree.length} bookmark(s)?`
+ `Delete all ${bookmarkTree.length} bookmark(s) ? `
);
if (confirmed) {
bookmarkTree = [];
@@ -949,7 +950,7 @@ batchDeleteBtn.addEventListener('click', async () => {
if (selectedBookmarks.size === 0) return;
const confirmed = await showConfirmModal(
- `Delete ${selectedBookmarks.size} bookmark(s)?`
+ `Delete ${selectedBookmarks.size} bookmark(s) ? `
);
if (!confirmed) return;
@@ -1173,7 +1174,7 @@ async function renderPage(num, zoom = null, destX = null, destY = null) {
ctx.stroke();
// Draw coordinate text background
- const text = `X: ${Math.round(destX)}, Y: ${Math.round(destY)}`;
+ const text = `X: ${Math.round(destX)}, Y: ${Math.round(destY)} `;
ctx.font = 'bold 12px monospace';
const textMetrics = ctx.measureText(text);
const textWidth = textMetrics.width;
@@ -1532,7 +1533,7 @@ function createNodeElement(node, level = 0) {
if (result && result.title) {
node.children.push({
id: Date.now() + Math.random(),
- title: result.title,
+ title: cleanTitle(result.title),
page: currentPage,
children: [],
color: null,
@@ -1608,7 +1609,7 @@ function createNodeElement(node, level = 0) {
);
if (result) {
- node.title = result.title;
+ node.title = cleanTitle(result.title);
node.color = result.color || null;
node.style = result.style || null;
@@ -1756,7 +1757,7 @@ function parseCSV(text) {
const [, title, page, level] = match;
const bookmark = {
id: Date.now() + Math.random(),
- title: title.replace(/""/g, '"'),
+ title: cleanTitle(title.replace(/""/g, '"')),
page: parseInt(page),
children: [],
color: null,
@@ -1787,6 +1788,15 @@ jsonImportHidden.addEventListener('change', async (e) => {
const text = await file.text();
try {
const imported = JSON.parse(text);
+ // Recursively clean titles in imported JSON
+ function cleanImportedTree(nodes) {
+ if (!nodes) return;
+ for (const node of nodes) {
+ if (node.title) node.title = cleanTitle(node.title);
+ if (node.children) cleanImportedTree(node.children);
+ }
+ }
+ cleanImportedTree(imported);
bookmarkTree = imported;
saveState();
renderBookmarkTree();
@@ -1834,254 +1844,84 @@ extractExistingBtn.addEventListener('click', async () => {
}
});
+// function cleanTitle(title) {
+// // @TODO@ALAM: visit this for encoding issues later
+// if (typeof title === 'string') {
+// if (title.includes('€') && !title.includes(' ')) {
+// return title.replace(/€/g, ' ');
+// }
+// return title.replace(/[\x00-\x1F\x7F-\x9F]/g, '').trim();
+// }
+// return title;
+// }
+
+function cleanTitle(title) {
+ // @TODO@ALAM: check for other encoding issues
+ if (typeof title === 'string') {
+ return title.replace(/[\x00-\x1F\x7F-\x9F]/g, '').trim();
+ }
+ return title;
+}
+
async function extractExistingBookmarks(doc) {
try {
- const outlines = doc.catalog.lookup(PDFName.of('Outlines'));
- if (!outlines) return [];
-
- const pages = doc.getPages();
-
- // Helper to resolve references
- function resolveRef(obj) {
- if (!obj) return null;
- if (obj.lookup) return obj;
- if (obj.objectNumber !== undefined && doc.context) {
- return doc.context.lookup(obj);
- }
- return obj;
- }
-
- // Build named destinations map (support full name-tree with Kids and Catalog-level Dests)
- const namedDests = new Map();
- try {
- function addNamePair(nameObj, destObj) {
- try {
- const key = nameObj.decodeText
- ? nameObj.decodeText()
- : String(nameObj);
- namedDests.set(key, resolveRef(destObj));
- } catch (_) {
- // ignore malformed entry
- }
- }
-
- function traverseNamesNode(node) {
- if (!node) return;
- node = resolveRef(node);
- if (!node) return;
-
- const namesArray = node.lookup
- ? node.lookup(PDFName.of('Names'))
- : null;
- if (namesArray && namesArray.array) {
- for (let i = 0; i < namesArray.array.length; i += 2) {
- const n = namesArray.array[i];
- const d = namesArray.array[i + 1];
- addNamePair(n, d);
- }
- }
-
- const kidsArray = node.lookup ? node.lookup(PDFName.of('Kids')) : null;
- if (kidsArray && kidsArray.array) {
- for (const kid of kidsArray.array) traverseNamesNode(kid);
- }
- }
-
- // Names tree under Catalog/Names/Dests
- const names = doc.catalog.lookup(PDFName.of('Names'));
- if (names) {
- const destsTree = names.lookup(PDFName.of('Dests'));
- if (destsTree) traverseNamesNode(destsTree);
- }
-
- // Some PDFs store Dests directly under the Catalog's Dests as a dictionary
- const catalogDests = doc.catalog.lookup(PDFName.of('Dests'));
- if (catalogDests && catalogDests.dict) {
- const entries = catalogDests.dict.entries();
- for (const [key, value] of entries) {
- // keys are PDFName; convert to string
- const keyStr = key.decodeText ? key.decodeText() : key.toString();
- namedDests.set(keyStr, resolveRef(value));
- }
- }
- } catch (e) {
- console.error('Error building named destinations:', e);
- }
-
- function findPageIndex(pageRef, resolvedPageRef = null) {
- if (!pageRef) return 0;
-
- try {
- // Method 1: Try the resolved page ref first (if provided)
- if (resolvedPageRef) {
- // Check if resolved is a page dictionary - compare to each page's dictionary
- for (let i = 0; i < pages.length; i++) {
- const pageDict = doc.context.lookup(pages[i].ref);
- if (pageDict === resolvedPageRef) {
- return i;
- }
- }
- }
-
- // Method 2: Direct PDFRef comparison
- const directMatch = pages.findIndex((p) => p.ref === pageRef);
- if (directMatch !== -1) {
- return directMatch;
- }
-
- // Method 3: Try resolving if not already resolved
- if (!resolvedPageRef) {
- const resolved = resolveRef(pageRef);
- if (resolved) {
- // Check if the resolved object matches any page's dictionary
- for (let i = 0; i < pages.length; i++) {
- const pageDict = doc.context.lookup(pages[i].ref);
- if (pageDict === resolved) {
- return i;
- }
- }
- }
- }
-
- // Method 4: Compare by string representation
- if (pageRef.toString) {
- const target = pageRef.toString();
- const stringMatch = pages.findIndex(
- (p) => p.ref && p.ref.toString() === target
- );
- if (stringMatch !== -1) {
- return stringMatch;
- }
- }
-
- // Method 5: Try numeric value (for edge cases)
- if (pageRef.numberValue !== undefined) {
- const numericIndex = pageRef.numberValue | 0;
- if (numericIndex >= 0 && numericIndex < pages.length) {
- return numericIndex;
- }
- }
- } catch (e) {
- console.error('Error finding page index:', e);
- }
-
- // If we couldn't find a match, return 0
- return 0;
- }
-
- function getDestination(item) {
- if (!item) return null;
-
- // Try Dest entry first
- let dest = item.lookup(PDFName.of('Dest'));
-
- // If no Dest, try Action/D
- if (!dest) {
- const action = resolveRef(item.lookup(PDFName.of('A')));
- if (action) {
- dest = action.lookup(PDFName.of('D'));
- }
- }
-
- // Handle named destinations
- if (dest && !dest.array) {
- try {
- const name = dest.decodeText ? dest.decodeText() : dest.toString();
- if (namedDests.has(name)) {
- const namedDest = namedDests.get(name);
-
- // Named destinations can be:
- // 1. A direct array [pageRef, /XYZ, ...]
- // 2. A dictionary with a 'D' entry containing the array
- if (namedDest.array) {
- dest = namedDest;
- } else if (namedDest.lookup) {
- // It's a dictionary - extract the 'D' entry
- const destFromDict = namedDest.lookup(PDFName.of('D'));
- if (destFromDict) {
- dest = destFromDict;
- } else {
- dest = namedDest;
- }
- } else {
- dest = namedDest;
- }
- } else if (dest.lookup) {
- // Some named destinations resolve to a dictionary with 'D' entry
- const maybeDict = resolveRef(dest);
- const dictD =
- maybeDict && maybeDict.lookup
- ? maybeDict.lookup(PDFName.of('D'))
- : null;
- if (dictD) dest = resolveRef(dictD);
- }
- } catch (_) {
- // leave dest as-is if it can't be decoded
- }
- }
-
- // dest is typically an array like [pageRef, /XYZ, x, y, zoom]
- // Resolving it would corrupt the array structure
- return dest;
- }
-
- function traverse(item) {
- if (!item) return null;
- item = resolveRef(item);
- if (!item) return null;
-
- const title = item.lookup(PDFName.of('Title'));
- const dest = getDestination(item);
- const colorObj = item.lookup(PDFName.of('C'));
- const flagsObj = item.lookup(PDFName.of('F'));
+ const outline = await pdfJsDoc.getOutline();
+ console.log(outline);
+ if (!outline) return [];
+ async function processOutlineItem(item) {
let pageIndex = 0;
let destX = null;
let destY = null;
let zoom = null;
- if (dest && dest.array) {
- const pageRef = dest.array[0];
- const resolvedPageRef = resolveRef(pageRef);
- pageIndex = findPageIndex(pageRef, resolvedPageRef);
+ try {
+ let dest = item.dest;
+ if (typeof dest === 'string') {
+ dest = await pdfJsDoc.getDestination(dest);
+ }
- if (dest.array.length > 2) {
- const xObj = resolveRef(dest.array[2]);
- const yObj = resolveRef(dest.array[3]);
- const zoomObj = resolveRef(dest.array[4]);
+ if (Array.isArray(dest)) {
+ const destRef = dest[0];
+ pageIndex = await pdfJsDoc.getPageIndex(destRef);
- if (xObj && xObj.numberValue !== undefined) destX = xObj.numberValue;
- if (yObj && yObj.numberValue !== undefined) destY = yObj.numberValue;
- if (zoomObj && zoomObj.numberValue !== undefined) {
- zoom = String(Math.round(zoomObj.numberValue * 100));
+ if (dest.length > 2) {
+ const x = dest[2];
+ const y = dest[3];
+ const z = dest[4];
+
+ if (typeof x === 'number') destX = x;
+ if (typeof y === 'number') destY = y;
+ if (typeof z === 'number') zoom = String(Math.round(z * 100));
}
}
+ } catch (e) {
+ console.warn('Error resolving destination:', e);
}
- // Rest of the color and style processing remains the same
let color = null;
- if (colorObj && colorObj.array) {
- const [r, g, b] = colorObj.array;
- if (r > 0.8 && g < 0.3 && b < 0.3) color = 'red';
- else if (r < 0.3 && g < 0.3 && b > 0.8) color = 'blue';
- else if (r < 0.3 && g > 0.8 && b < 0.3) color = 'green';
- else if (r > 0.8 && g > 0.8 && b < 0.3) color = 'yellow';
- else if (r > 0.5 && g < 0.5 && b > 0.5) color = 'purple';
+ if (item.color) {
+ const [r, g, b] = item.color;
+ const rN = r / 255;
+ const gN = g / 255;
+ const bN = b / 255;
+
+ if (rN > 0.8 && gN < 0.3 && bN < 0.3) color = 'red';
+ else if (rN < 0.3 && gN < 0.3 && bN > 0.8) color = 'blue';
+ else if (rN < 0.3 && gN > 0.8 && bN < 0.3) color = 'green';
+ else if (rN > 0.8 && gN > 0.8 && bN < 0.3) color = 'yellow';
+ else if (rN > 0.5 && gN < 0.5 && bN > 0.5) color = 'purple';
}
+ // Map style
let style = null;
- if (flagsObj) {
- const flags = flagsObj.numberValue || 0;
- const isBold = (flags & 2) !== 0;
- const isItalic = (flags & 1) !== 0;
- if (isBold && isItalic) style = 'bold-italic';
- else if (isBold) style = 'bold';
- else if (isItalic) style = 'italic';
- }
+ if (item.bold && item.italic) style = 'bold-italic';
+ else if (item.bold) style = 'bold';
+ else if (item.italic) style = 'italic';
const bookmark = {
id: Date.now() + Math.random(),
- title: title ? title.decodeText() : 'Untitled',
+ title: cleanTitle(item.title),
page: pageIndex + 1,
children: [],
color,
@@ -2091,21 +1931,10 @@ async function extractExistingBookmarks(doc) {
zoom,
};
- // Process children (make sure to resolve refs)
- let child = resolveRef(item.lookup(PDFName.of('First')));
- while (child) {
- const childBookmark = traverse(child);
- if (childBookmark) bookmark.children.push(childBookmark);
- child = resolveRef(child.lookup(PDFName.of('Next')));
- }
-
- if (pageIndex === 0 && bookmark.children.length > 0) {
- const firstChild = bookmark.children[0];
- if (firstChild) {
- bookmark.page = firstChild.page;
- bookmark.destX = firstChild.destX;
- bookmark.destY = firstChild.destY;
- bookmark.zoom = firstChild.zoom;
+ if (item.items && item.items.length > 0) {
+ for (const childItem of item.items) {
+ const childBookmark = await processOutlineItem(childItem);
+ bookmark.children.push(childBookmark);
}
}
@@ -2113,11 +1942,9 @@ async function extractExistingBookmarks(doc) {
}
const result = [];
- let first = resolveRef(outlines.lookup(PDFName.of('First')));
- while (first) {
- const bookmark = traverse(first);
- if (bookmark) result.push(bookmark);
- first = resolveRef(first.lookup(PDFName.of('Next')));
+ for (const item of outline) {
+ const bookmark = await processOutlineItem(item);
+ result.push(bookmark);
}
return result;
@@ -2153,7 +1980,7 @@ downloadBtn.addEventListener('click', async () => {
const itemDict = pdfLibDoc.context.obj({});
const itemRef = pdfLibDoc.context.register(itemDict);
- itemDict.set(PDFName.of('Title'), PDFString.of(node.title));
+ itemDict.set(PDFName.of('Title'), PDFHexString.fromText(node.title));
itemDict.set(PDFName.of('Parent'), parentRef);
// Always map bookmark page to zero-based index consistently
diff --git a/src/js/logic/cropper.ts b/src/js/logic/cropper.ts
index f8d5661..e10f2ca 100644
--- a/src/js/logic/cropper.ts
+++ b/src/js/logic/cropper.ts
@@ -180,7 +180,8 @@ async function performFlatteningCrop(cropData: any) {
// Load the original PDF with pdf-lib to copy un-cropped pages from
const sourcePdfDocForCopying = await PDFLibDocument.load(
- cropperState.originalPdfBytes
+ cropperState.originalPdfBytes,
+ {ignoreEncryption: true, throwOnInvalidObject: false}
);
const totalPages = cropperState.pdfDoc.numPages;
@@ -321,7 +322,8 @@ export async function setupCropperTool() {
finalPdfBytes = await newPdfDoc.save();
} else {
const pdfToModify = await PDFLibDocument.load(
- cropperState.originalPdfBytes
+ cropperState.originalPdfBytes,
+ {ignoreEncryption: true, throwOnInvalidObject: false}
);
await performMetadataCrop(pdfToModify, finalCropData);
finalPdfBytes = await pdfToModify.save();
diff --git a/src/js/logic/delete-pages.ts b/src/js/logic/delete-pages.ts
index 6c4b280..c8aa86a 100644
--- a/src/js/logic/delete-pages.ts
+++ b/src/js/logic/delete-pages.ts
@@ -68,3 +68,44 @@ export async function deletePages() {
hideLoader();
}
}
+
+export function setupDeletePagesTool() {
+ const input = document.getElementById('pages-to-delete') as HTMLInputElement;
+ if (!input) return;
+
+ const updateHighlights = () => {
+ const val = input.value;
+ const pagesToDelete = new Set
();
+
+ const parts = val.split(',');
+ for (const part of parts) {
+ const trimmed = part.trim();
+ if (trimmed.includes('-')) {
+ const [start, end] = trimmed.split('-').map(Number);
+ if (!isNaN(start) && !isNaN(end) && start <= end) {
+ for (let i = start; i <= end; i++) pagesToDelete.add(i);
+ }
+ } else {
+ const num = Number(trimmed);
+ if (!isNaN(num)) pagesToDelete.add(num);
+ }
+ }
+
+ const thumbnails = document.querySelectorAll('#delete-pages-preview .page-thumbnail');
+ thumbnails.forEach((thumb) => {
+ const pageNum = parseInt((thumb as HTMLElement).dataset.pageNumber || '0');
+ const innerContainer = thumb.querySelector('div.relative');
+
+ if (pagesToDelete.has(pageNum)) {
+ innerContainer?.classList.add('border-red-500');
+ innerContainer?.classList.remove('border-gray-600');
+ } else {
+ innerContainer?.classList.remove('border-red-500');
+ innerContainer?.classList.add('border-gray-600');
+ }
+ });
+ };
+
+ input.addEventListener('input', updateHighlights);
+ updateHighlights();
+}
diff --git a/src/js/logic/index.ts b/src/js/logic/index.ts
index d4b708b..ecf1eff 100644
--- a/src/js/logic/index.ts
+++ b/src/js/logic/index.ts
@@ -18,7 +18,7 @@ import { pdfToPng } from './pdf-to-png.js';
import { pngToPdf } from './png-to-pdf.js';
import { pdfToWebp } from './pdf-to-webp.js';
import { webpToPdf } from './webp-to-pdf.js';
-import { deletePages } from './delete-pages.js';
+import { deletePages, setupDeletePagesTool } from './delete-pages.js';
import { addBlankPage } from './add-blank-page.js';
import { extractPages } from './extract-pages.js';
import { addWatermark, setupWatermarkUI } from './add-watermark.js';
@@ -67,6 +67,7 @@ import { extractAttachments } from './extract-attachments.js';
import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js';
import { sanitizePdf } from './sanitize-pdf.js';
import { removeRestrictions } from './remove-restrictions.js';
+import { repairPdf } from './repair-pdf.js';
export const toolLogic = {
merge: { process: merge, setup: setupMergeTool },
@@ -74,6 +75,7 @@ export const toolLogic = {
encrypt,
decrypt,
'remove-restrictions': removeRestrictions,
+ 'repair-pdf': repairPdf,
organize,
rotate,
'add-page-numbers': addPageNumbers,
@@ -90,7 +92,7 @@ export const toolLogic = {
'png-to-pdf': pngToPdf,
'pdf-to-webp': pdfToWebp,
'webp-to-pdf': webpToPdf,
- 'delete-pages': deletePages,
+ 'delete-pages': { process: deletePages, setup: setupDeletePagesTool },
'add-blank-page': addBlankPage,
'extract-pages': extractPages,
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
diff --git a/src/js/logic/merge.ts b/src/js/logic/merge.ts
index 4ce21e5..3024876 100644
--- a/src/js/logic/merge.ts
+++ b/src/js/logic/merge.ts
@@ -1,7 +1,7 @@
import { showLoader, hideLoader, showAlert } from '../ui.ts';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.ts';
import { state } from '../state.ts';
-import { renderPagesProgressively, cleanupLazyRendering, createPlaceholder } from '../utils/render-utils.ts';
+import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.ts';
import { createIcons, icons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
@@ -9,9 +9,22 @@ import Sortable from 'sortablejs';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
-const mergeState = {
- pdfDocs: {} as Record,
- pdfBytes: {} as Record,
+interface MergeState {
+ pdfDocs: Record;
+ pdfBytes: Record;
+ activeMode: 'file' | 'page';
+ sortableInstances: {
+ fileList?: Sortable;
+ pageThumbnails?: Sortable;
+ };
+ isRendering: boolean;
+ cachedThumbnails: boolean | null;
+ lastFileHash: string | null;
+}
+
+const mergeState: MergeState = {
+ pdfDocs: {},
+ pdfBytes: {},
activeMode: 'file',
sortableInstances: {},
isRendering: false,
@@ -21,47 +34,14 @@ const mergeState = {
const mergeWorker = new Worker('/workers/merge.worker.js');
-function parsePageRanges(rangeString: any, totalPages: any) {
- const indices = new Set();
- if (!rangeString.trim()) return [];
-
- const ranges = rangeString.split(',');
- for (const range of ranges) {
- const trimmedRange = range.trim();
- if (trimmedRange.includes('-')) {
- const [start, end] = trimmedRange.split('-').map(Number);
- if (
- isNaN(start) ||
- isNaN(end) ||
- start < 1 ||
- end > totalPages ||
- start > end
- )
- continue;
- for (let i = start; i <= end; i++) {
- indices.add(i - 1);
- }
- } else {
- const pageNum = Number(trimmedRange);
- if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
- indices.add(pageNum - 1);
- }
- }
- // @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
- return Array.from(indices).sort((a, b) => a - b);
-}
-
function initializeFileListSortable() {
const fileList = document.getElementById('file-list');
if (!fileList) return;
- // @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
if (mergeState.sortableInstances.fileList) {
- // @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
mergeState.sortableInstances.fileList.destroy();
}
- // @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
handle: '.drag-handle',
animation: 150,
@@ -81,13 +61,10 @@ function initializePageThumbnailsSortable() {
const container = document.getElementById('page-merge-preview');
if (!container) return;
- // @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
if (mergeState.sortableInstances.pageThumbnails) {
- // @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
mergeState.sortableInstances.pageThumbnails.destroy();
}
- // @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
mergeState.sortableInstances.pageThumbnails = Sortable.create(container, {
animation: 150,
ghostClass: 'sortable-ghost',
@@ -192,7 +169,7 @@ async function renderPageMergeThumbnails() {
container,
createWrapperWithFileName,
{
- batchSize: 6,
+ batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '300px',
onProgress: (current, total) => {
@@ -224,8 +201,8 @@ async function renderPageMergeThumbnails() {
export async function merge() {
showLoader('Merging PDFs...');
try {
- const jobs: any[] = [];
- const filesToMerge: any[] = [];
+ const jobs: MergeJob[] = [];
+ const filesToMerge: MergeFile[] = [];
const uniqueFileNames = new Set();
if (mergeState.activeMode === 'file') {
@@ -234,8 +211,7 @@ export async function merge() {
const sortedFiles = Array.from(fileList.children)
.map((li) => {
- // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element...
- return state.files.find((f) => f.name === li.dataset.fileName);
+ return state.files.find((f) => f.name === (li as HTMLElement).dataset.fileName);
})
.filter(Boolean);
@@ -267,10 +243,9 @@ export async function merge() {
const rawPages: { fileName: string; pageIndex: number }[] = [];
for (const el of pageElements) {
- // @ts-expect-error TS(2339)
- const fileName = el.dataset.fileName;
- // @ts-expect-error TS(2339)
- const pageIndex = parseInt(el.dataset.pageIndex, 10); // 0-based index from dataset
+ const element = el as HTMLElement;
+ const fileName = element.dataset.fileName;
+ const pageIndex = parseInt(element.dataset.pageIndex || '', 10); // 0-based index from dataset
if (fileName && !isNaN(pageIndex)) {
uniqueFileNames.add(fileName);
@@ -304,8 +279,8 @@ export async function merge() {
jobs.push({
fileName: current.fileName,
rangeType: 'range',
- startPage: current.pageIndex + 1,
- endPage: endPage + 1
+ startPage: current.pageIndex + 1,
+ endPage: endPage + 1
});
}
}
@@ -324,13 +299,15 @@ export async function merge() {
}
}
- mergeWorker.postMessage({
+ const message: MergeMessage = {
command: 'merge',
files: filesToMerge,
jobs: jobs
- }, filesToMerge.map(f => f.data));
+ };
- mergeWorker.onmessage = (e) => {
+ mergeWorker.postMessage(message, filesToMerge.map(f => f.data));
+
+ mergeWorker.onmessage = (e: MessageEvent) => {
hideLoader();
if (e.data.status === 'success') {
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
diff --git a/src/js/logic/pdf-multi-tool.ts b/src/js/logic/pdf-multi-tool.ts
index 3bd5906..5576eef 100644
--- a/src/js/logic/pdf-multi-tool.ts
+++ b/src/js/logic/pdf-multi-tool.ts
@@ -6,6 +6,7 @@ import Sortable from 'sortablejs';
import { downloadFile, getPDFDocument } from '../utils/helpers';
import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, createPlaceholder } from '../utils/render-utils';
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
+import { repairPdfFile } from './repair-pdf.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -351,8 +352,27 @@ async function loadPdfs(files: File[]) {
if (renderCancelled) break;
try {
- const arrayBuffer = await file.arrayBuffer();
- const pdfDoc = await PDFLibDocument.load(arrayBuffer);
+ let arrayBuffer: ArrayBuffer;
+
+ try {
+ console.log(`Repairing ${file.name}...`);
+ const loadingText = document.getElementById('loading-text');
+ if (loadingText) loadingText.textContent = `Repairing ${file.name}...`;
+
+ const repairedData = await repairPdfFile(file);
+ if (repairedData) {
+ arrayBuffer = repairedData.buffer as ArrayBuffer;
+ console.log(`Successfully repaired ${file.name} before loading.`);
+ } else {
+ console.warn(`Repair returned null for ${file.name}, using original file.`);
+ arrayBuffer = await file.arrayBuffer();
+ }
+ } catch (repairError) {
+ console.warn(`Failed to repair ${file.name}, attempting to load original:`, repairError);
+ arrayBuffer = await file.arrayBuffer();
+ }
+
+ const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
currentPdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1;
@@ -735,7 +755,7 @@ async function handleInsertPdf(e: Event) {
try {
const arrayBuffer = await file.arrayBuffer();
- const pdfDoc = await PDFLibDocument.load(arrayBuffer);
+ const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
currentPdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1;
diff --git a/src/js/logic/repair-pdf-page.ts b/src/js/logic/repair-pdf-page.ts
new file mode 100644
index 0000000..5f9ed62
--- /dev/null
+++ b/src/js/logic/repair-pdf-page.ts
@@ -0,0 +1,92 @@
+import { repairPdf } from './repair-pdf.js';
+import { state } from '../state.js';
+import { renderFileDisplay } from '../ui.js';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const fileInput = document.getElementById('file-input') as HTMLInputElement;
+ const dropZone = document.getElementById('drop-zone');
+ const processBtn = document.getElementById('process-btn');
+ const fileDisplayArea = document.getElementById('file-display-area');
+
+ const fileControls = document.getElementById('file-controls');
+ const addMoreBtn = document.getElementById('add-more-btn');
+ const clearFilesBtn = document.getElementById('clear-files-btn');
+ const backBtn = document.getElementById('back-to-tools');
+
+ if (backBtn) {
+ backBtn.addEventListener('click', () => {
+ window.location.href = '/';
+ });
+ }
+
+ const updateUI = () => {
+ if (state.files.length > 0) {
+ renderFileDisplay(fileDisplayArea, state.files);
+ if (processBtn) processBtn.classList.remove('hidden');
+ if (fileControls) fileControls.classList.remove('hidden');
+ } else {
+ if (fileDisplayArea) fileDisplayArea.innerHTML = '';
+ if (processBtn) processBtn.classList.add('hidden');
+ if (fileControls) fileControls.classList.add('hidden');
+ }
+ };
+
+ if (fileInput && dropZone) {
+ fileInput.addEventListener('change', async (e) => {
+ const files = (e.target as HTMLInputElement).files;
+ if (files && files.length > 0) {
+ state.files = [...state.files, ...Array.from(files)];
+ updateUI();
+ }
+ fileInput.value = '';
+ });
+
+ dropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ dropZone.classList.add('bg-gray-700');
+ });
+
+ dropZone.addEventListener('dragleave', (e) => {
+ e.preventDefault();
+ dropZone.classList.remove('bg-gray-700');
+ });
+
+ dropZone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ dropZone.classList.remove('bg-gray-700');
+ const files = e.dataTransfer?.files;
+ if (files && files.length > 0) {
+ const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
+ if (pdfFiles.length > 0) {
+ state.files = [...state.files, ...pdfFiles];
+ updateUI();
+ }
+ }
+ });
+
+ dropZone.addEventListener('click', () => {
+ fileInput.click();
+ });
+ }
+
+ if (addMoreBtn) {
+ addMoreBtn.addEventListener('click', () => {
+ fileInput.click();
+ });
+ }
+
+ if (clearFilesBtn) {
+ clearFilesBtn.addEventListener('click', () => {
+ state.files = [];
+ updateUI();
+ });
+ }
+
+ if (processBtn) {
+ processBtn.addEventListener('click', async () => {
+ await repairPdf();
+ });
+ }
+
+ updateUI();
+});
diff --git a/src/js/logic/repair-pdf.ts b/src/js/logic/repair-pdf.ts
new file mode 100644
index 0000000..0fd684f
--- /dev/null
+++ b/src/js/logic/repair-pdf.ts
@@ -0,0 +1,128 @@
+import { showLoader, hideLoader, showAlert } from '../ui.js';
+import {
+ downloadFile,
+ initializeQpdf,
+ readFileAsArrayBuffer,
+} from '../utils/helpers.js';
+import { state } from '../state.js';
+import JSZip from 'jszip';
+
+export async function repairPdfFile(file: File): Promise {
+ const inputPath = '/input.pdf';
+ const outputPath = '/repaired_form.pdf';
+ let qpdf: any;
+
+ try {
+ qpdf = await initializeQpdf();
+ const fileBuffer = await readFileAsArrayBuffer(file);
+ const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
+
+ qpdf.FS.writeFile(inputPath, uint8Array);
+
+ const args = [inputPath, '--decrypt', outputPath];
+
+ try {
+ qpdf.callMain(args);
+ } catch (e) {
+ console.warn(`QPDF execution warning for ${file.name}:`, e);
+ }
+
+ let repairedData: Uint8Array | null = null;
+ try {
+ repairedData = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
+ } catch (e) {
+ console.warn(`Failed to read output for ${file.name}:`, e);
+ }
+
+ try {
+ try {
+ qpdf.FS.unlink(inputPath);
+ } catch (e) {
+ console.warn(e);
+ }
+ try {
+ qpdf.FS.unlink(outputPath);
+ } catch (e) {
+ console.warn(e);
+ }
+ } catch (cleanupError) {
+ console.warn('Cleanup error:', cleanupError);
+ }
+
+ return repairedData;
+
+ } catch (error) {
+ console.error(`Error repairing ${file.name}:`, error);
+ return null;
+ }
+}
+
+export async function repairPdf() {
+ if (state.files.length === 0) {
+ showAlert('No Files', 'Please select one or more PDF files.');
+ return;
+ }
+
+ const successfulRepairs: { name: string; data: Uint8Array }[] = [];
+ const failedRepairs: string[] = [];
+
+ try {
+ showLoader('Initializing repair engine...');
+
+ for (let i = 0; i < state.files.length; i++) {
+ const file = state.files[i];
+ showLoader(`Repairing ${file.name} (${i + 1}/${state.files.length})...`);
+
+ const repairedData = await repairPdfFile(file);
+
+ if (repairedData && repairedData.length > 0) {
+ successfulRepairs.push({
+ name: `repaired-${file.name}`,
+ data: repairedData,
+ });
+ } else {
+ failedRepairs.push(file.name);
+ }
+ }
+
+ hideLoader();
+
+ if (successfulRepairs.length === 0) {
+ showAlert('Repair Failed', 'Unable to repair any of the uploaded PDF files.');
+ return;
+ }
+
+ if (failedRepairs.length > 0) {
+ const failedList = failedRepairs.join(', ');
+ showAlert(
+ 'Partial Success',
+ `Repaired ${successfulRepairs.length} file(s). Failed to repair: ${failedList}`
+ );
+ }
+
+ if (successfulRepairs.length === 1) {
+ const file = successfulRepairs[0];
+ const blob = new Blob([file.data as any], { type: 'application/pdf' });
+ downloadFile(blob, file.name);
+ } else {
+ showLoader('Creating ZIP archive...');
+ const zip = new JSZip();
+ successfulRepairs.forEach((file) => {
+ zip.file(file.name, file.data);
+ });
+
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
+ downloadFile(zipBlob, 'repaired_pdfs.zip');
+ hideLoader();
+ }
+
+ if (failedRepairs.length === 0) {
+ showAlert('Success', 'All files repaired successfully!');
+ }
+
+ } catch (error: any) {
+ console.error('Critical error during repair:', error);
+ hideLoader();
+ showAlert('Error', 'An unexpected error occurred during the repair process.');
+ }
+}
diff --git a/src/js/logic/reverse-pages.ts b/src/js/logic/reverse-pages.ts
index ee32834..32c429a 100644
--- a/src/js/logic/reverse-pages.ts
+++ b/src/js/logic/reverse-pages.ts
@@ -19,7 +19,7 @@ export async function reversePages() {
for (let j = 0; j < pdfDocs.length; j++) {
const file = pdfDocs[j];
const arrayBuffer = await file.arrayBuffer();
- const pdfDoc = await PDFLibDocument.load(arrayBuffer);
+ const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
const newPdf = await PDFLibDocument.create();
const pageCount = pdfDoc.getPageCount();
const reversedIndices = Array.from(
diff --git a/src/js/logic/rotate.ts b/src/js/logic/rotate.ts
index 623bf10..19caa13 100644
--- a/src/js/logic/rotate.ts
+++ b/src/js/logic/rotate.ts
@@ -3,25 +3,56 @@ import { downloadFile, resetAndReloadTool } from '../utils/helpers.js';
import { state } from '../state.js';
import { getRotationState, resetRotationState } from '../handlers/fileHandler.js';
-import { degrees } from 'pdf-lib';
+import { PDFDocument, degrees } from 'pdf-lib';
export async function rotate() {
showLoader('Applying rotations...');
try {
- const pages = state.pdfDoc.getPages();
+ const originalPdf = state.pdfDoc;
+ const pageCount = originalPdf.getPageCount();
const rotationStateArray = getRotationState();
- // Apply rotations from state (not DOM) to ensure all pages including lazy-loaded ones are rotated
- rotationStateArray.forEach((rotation, pageIndex) => {
- if (rotation !== 0 && pages[pageIndex]) {
- const currentRotation = pages[pageIndex].getRotation().angle;
- pages[pageIndex].setRotation(degrees(currentRotation + rotation));
- }
- });
+ const newPdfDoc = await PDFDocument.create();
- const rotatedPdfBytes = await state.pdfDoc.save();
+ for (let i = 0; i < pageCount; i++) {
+ const rotation = rotationStateArray[i] || 0;
+ const originalPage = originalPdf.getPage(i);
+ const currentRotation = originalPage.getRotation().angle;
+ const totalRotation = currentRotation + rotation;
+
+ if (totalRotation % 90 === 0) {
+ const [copiedPage] = await newPdfDoc.copyPages(originalPdf, [i]);
+ copiedPage.setRotation(degrees(totalRotation));
+ newPdfDoc.addPage(copiedPage);
+ } else {
+ const embeddedPage = await newPdfDoc.embedPage(originalPage);
+ const { width, height } = embeddedPage.scale(1);
+
+ const angleRad = (totalRotation * Math.PI) / 180;
+ const absCos = Math.abs(Math.cos(angleRad));
+ const absSin = Math.abs(Math.sin(angleRad));
+
+ const newWidth = width * absCos + height * absSin;
+ const newHeight = width * absSin + height * absCos;
+
+ const newPage = newPdfDoc.addPage([newWidth, newHeight]);
+
+ const x = newWidth / 2 - (width / 2 * Math.cos(angleRad) - height / 2 * Math.sin(angleRad));
+ const y = newHeight / 2 - (width / 2 * Math.sin(angleRad) + height / 2 * Math.cos(angleRad));
+
+ newPage.drawPage(embeddedPage, {
+ x,
+ y,
+ width,
+ height,
+ rotate: degrees(totalRotation),
+ });
+ }
+ }
+
+ const rotatedPdfBytes = await newPdfDoc.save();
downloadFile(
- new Blob([rotatedPdfBytes], { type: 'application/pdf' }),
+ new Blob([rotatedPdfBytes as any], { type: 'application/pdf' }),
'rotated.pdf'
);
diff --git a/src/js/ui.ts b/src/js/ui.ts
index d057ced..e85d265 100644
--- a/src/js/ui.ts
+++ b/src/js/ui.ts
@@ -44,19 +44,23 @@ export const dom = {
};
export const showLoader = (text = 'Processing...') => {
- dom.loaderText.textContent = text;
- dom.loaderModal.classList.remove('hidden');
+ if (dom.loaderText) dom.loaderText.textContent = text;
+ if (dom.loaderModal) dom.loaderModal.classList.remove('hidden');
};
-export const hideLoader = () => dom.loaderModal.classList.add('hidden');
+export const hideLoader = () => {
+ if (dom.loaderModal) dom.loaderModal.classList.add('hidden');
+};
export const showAlert = (title: any, message: any) => {
- dom.alertTitle.textContent = title;
- dom.alertMessage.textContent = message;
- dom.alertModal.classList.remove('hidden');
+ if (dom.alertTitle) dom.alertTitle.textContent = title;
+ if (dom.alertMessage) dom.alertMessage.textContent = message;
+ if (dom.alertModal) dom.alertModal.classList.remove('hidden');
};
-export const hideAlert = () => dom.alertModal.classList.add('hidden');
+export const hideAlert = () => {
+ if (dom.alertModal) dom.alertModal.classList.add('hidden');
+};
export const switchView = (view: any) => {
if (view === 'grid') {
@@ -125,7 +129,7 @@ function initializeOrganizeSortable(containerId: any) {
* @param {object} pdfDoc The loaded pdf-lib document instance.
*/
export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
- const containerId = toolId === 'organize' ? 'page-organizer' : 'page-rotator';
+ const containerId = toolId === 'organize' ? 'page-organizer' : toolId === 'delete-pages' ? 'delete-pages-preview' : 'page-rotator';
const container = document.getElementById(containerId);
if (!container) return;
@@ -134,6 +138,9 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
// Cleanup any previous lazy loading observers
cleanupLazyRendering();
+ const currentRenderId = Date.now();
+ container.dataset.renderId = currentRenderId.toString();
+
showLoader('Rendering page previews...');
const pdfData = await pdfDoc.save();
@@ -185,7 +192,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
wrapper.append(pageNumSpan, deleteBtn);
} 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 relative group';
// Read rotation from state (handles "Rotate All" on lazy-loaded pages)
const rotationStateArray = getRotationState();
@@ -202,35 +209,122 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
wrapper.appendChild(imgContainer);
- const controlsDiv = document.createElement('div');
- controlsDiv.className = 'flex items-center justify-center gap-3 w-full';
-
+ // Page Number Overlay (Top Left)
const pageNumSpan = document.createElement('span');
- pageNumSpan.className = 'font-medium text-sm text-white';
+ pageNumSpan.className =
+ 'absolute top-2 left-2 bg-gray-900 bg-opacity-75 text-white text-xs font-medium rounded-md px-2 py-1 shadow-sm z-10 pointer-events-none';
pageNumSpan.textContent = pageNumber.toString();
+ wrapper.appendChild(pageNumSpan);
- const rotateBtn = document.createElement('button');
- rotateBtn.className =
- 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full';
- rotateBtn.title = 'Rotate 90°';
- rotateBtn.innerHTML = ' ';
- rotateBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- const card = (e.currentTarget as HTMLElement).closest(
- '.page-rotator-item'
- ) as HTMLElement;
+ const controlsDiv = document.createElement('div');
+ controlsDiv.className = 'flex flex-col lg:flex-row items-center justify-center w-full gap-2 px-1';
+
+ // Custom Stepper Component
+ const stepperContainer = document.createElement('div');
+ stepperContainer.className = 'flex items-center border border-gray-600 rounded-md bg-gray-800 overflow-hidden w-24 h-8';
+
+ const decrementBtn = document.createElement('button');
+ decrementBtn.className = 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-r border-gray-600 transition-colors flex items-center justify-center';
+ decrementBtn.innerHTML = ' ';
+
+ const angleInput = document.createElement('input');
+ angleInput.type = 'number';
+ angleInput.className = 'no-spinner w-full h-full bg-transparent text-white text-xs text-center focus:outline-none appearance-none m-0 p-0 border-none';
+ angleInput.value = initialRotation.toString();
+ angleInput.placeholder = "0";
+
+ const incrementBtn = document.createElement('button');
+ incrementBtn.className = 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-l border-gray-600 transition-colors flex items-center justify-center';
+ incrementBtn.innerHTML = ' ';
+
+ // Helper to update rotation
+ const updateRotation = (newRotation: number) => {
+ const card = wrapper; // Closure capture
const imgEl = card.querySelector('img');
const pageIndex = pageNumber - 1;
- let currentRotation = parseInt(card.dataset.rotation);
- currentRotation = (currentRotation + 90) % 360;
- card.dataset.rotation = currentRotation.toString();
- imgEl.style.transform = `rotate(${currentRotation}deg)`;
- updateRotationState(pageIndex, currentRotation);
+ // Update UI
+ angleInput.value = newRotation.toString();
+ card.dataset.rotation = newRotation.toString();
+ imgEl.style.transform = `rotate(${newRotation}deg)`;
+
+ // Update State
+ updateRotationState(pageIndex, newRotation);
+ };
+
+ // Event Listeners
+ decrementBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ let current = parseInt(angleInput.value) || 0;
+ updateRotation(current - 1);
});
- controlsDiv.append(pageNumSpan, rotateBtn);
+ incrementBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ let current = parseInt(angleInput.value) || 0;
+ updateRotation(current + 1);
+ });
+
+ angleInput.addEventListener('change', (e) => {
+ e.stopPropagation();
+ let val = parseInt((e.target as HTMLInputElement).value) || 0;
+ updateRotation(val);
+ });
+ angleInput.addEventListener('click', (e) => e.stopPropagation());
+
+ stepperContainer.append(decrementBtn, angleInput, incrementBtn);
+
+ const rotateBtn = document.createElement('button');
+ rotateBtn.className = 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-1.5 rounded-md text-gray-200 transition-colors flex-shrink-0';
+ rotateBtn.title = 'Rotate +90°';
+ rotateBtn.innerHTML = ' ';
+ rotateBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ let current = parseInt(angleInput.value) || 0;
+ updateRotation(current + 90);
+ });
+
+ controlsDiv.append(stepperContainer, rotateBtn);
wrapper.appendChild(controlsDiv);
+ } else if (toolId === 'delete-pages') {
+ wrapper.className = 'page-thumbnail relative group cursor-pointer transition-all duration-200';
+ wrapper.dataset.pageNumber = pageNumber.toString();
+
+ const innerContainer = document.createElement('div');
+ innerContainer.className = 'relative w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600 transition-colors duration-200';
+ innerContainer.appendChild(img);
+ wrapper.appendChild(innerContainer);
+
+ const pageNumSpan = document.createElement('span');
+ pageNumSpan.className =
+ 'absolute top-2 left-2 bg-gray-900 bg-opacity-75 text-white text-xs font-medium rounded-md px-2 py-1 shadow-sm z-10 pointer-events-none';
+ pageNumSpan.textContent = pageNumber.toString();
+ wrapper.appendChild(pageNumSpan);
+
+ wrapper.addEventListener('click', () => {
+ const input = document.getElementById('pages-to-delete') as HTMLInputElement;
+ if (!input) return;
+
+ const currentVal = input.value;
+ let pages = currentVal.split(',').map(s => s.trim()).filter(s => s);
+ const pageStr = pageNumber.toString();
+
+ if (pages.includes(pageStr)) {
+ pages = pages.filter(p => p !== pageStr);
+ } else {
+ pages.push(pageStr);
+ }
+
+ pages.sort((a, b) => {
+ const numA = parseInt(a.split('-')[0]);
+ const numB = parseInt(b.split('-')[0]);
+ return numA - numB;
+ });
+
+ input.value = pages.join(', ');
+
+ input.dispatchEvent(new Event('input'));
+ });
}
return wrapper;
@@ -243,7 +337,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
container,
createWrapper,
{
- batchSize: 6,
+ batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '300px',
onProgress: (current, total) => {
@@ -251,12 +345,17 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
},
onBatchComplete: () => {
createIcons({ icons });
+ },
+ shouldCancel: () => {
+ return container.dataset.renderId !== currentRenderId.toString();
}
}
);
if (toolId === 'organize') {
initializeOrganizeSortable(containerId);
+ } else if (toolId === 'delete-pages') {
+ // No sortable needed for delete pages
}
// Reinitialize lucide icons for dynamically added elements
@@ -576,15 +675,49 @@ export const toolTemplates = {
BATCH ACTIONS
-
-
-
- Rotate All Left
-
-
-
- Rotate All Right
-
+
+
+
+
+
Rotate By 90 degrees
+
+
+
+ Left
+
+
+
+ Right
+
+
+
+
+
+
+
+
+
Rotate By Custom Degrees
+
+
+
@@ -897,6 +1030,7 @@ export const toolTemplates = {
Total Pages:
Enter pages to delete (e.g., 2, 4-6, 9):
+
Delete Pages & Download
`,
diff --git a/src/js/utils/render-utils.ts b/src/js/utils/render-utils.ts
index 526c8c5..5e4511a 100644
--- a/src/js/utils/render-utils.ts
+++ b/src/js/utils/render-utils.ts
@@ -9,7 +9,7 @@ export interface RenderConfig {
batchSize?: number;
useLazyLoading?: boolean;
lazyLoadMargin?: string;
- eagerLoadBatches?: number; // Number of batches to load ahead eagerly (default: 1)
+ eagerLoadBatches?: number; // Number of batches to load ahead eagerly (default: 2)
onProgress?: (current: number, total: number) => void;
onPageRendered?: (pageIndex: number, element: HTMLElement) => void;
onBatchComplete?: () => void;
@@ -214,7 +214,7 @@ export async function renderPagesProgressively(
const {
batchSize = 8, // Increased from 5 to 8 for faster initial render
useLazyLoading = true,
- eagerLoadBatches = 1, // Eagerly load 1 batch ahead by default
+ eagerLoadBatches = 2, // Eagerly load 1 batch ahead by default
onProgress,
onBatchComplete,
} = config;
@@ -318,7 +318,7 @@ export function observePlaceholder(
* Eagerly renders the next batch in the background
*/
function renderEagerBatch(config: RenderConfig): void {
- const { eagerLoadBatches = 1, batchSize = 8 } = config;
+ const { eagerLoadBatches = 2, batchSize = 8 } = config;
if (eagerLoadBatches <= 0 || lazyLoadState.eagerLoadQueue.length === 0) {
return;
diff --git a/src/pages/add-stamps.html b/src/pages/add-stamps.html
index 45440d8..ab1b0c1 100644
--- a/src/pages/add-stamps.html
+++ b/src/pages/add-stamps.html
@@ -5,7 +5,10 @@
Add Stamps - BentoPDF
-
+
+
+
+
diff --git a/src/pages/bookmark.html b/src/pages/bookmark.html
index 451bef0..e4d1112 100644
--- a/src/pages/bookmark.html
+++ b/src/pages/bookmark.html
@@ -5,7 +5,10 @@
Advanced PDF Bookmark Tool - BentoPDF
-
+
+
+
+
@@ -271,7 +274,7 @@
+ class="w-full px-3 py-2 btn-gradient hover:shadow-xl text-white rounded text-sm font-medium !flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200">
Add to Page 1
@@ -391,11 +394,14 @@
+ class="w-full px-4 py-2 btn-gradient hover:shadow-xl text-white rounded font-medium !flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200">
Save PDF with Bookmarks
+ class="w-full px-4 py-2 bg-gray-50 text-gray-800
+ border border-gray-200 rounded-xl font-medium
+ flex items-center justify-center gap-2
+ shadow-sm hover:shadow-md transition duration-200">
Extract Existing Bookmarks
diff --git a/src/pages/form-creator.html b/src/pages/form-creator.html
index a96cb7a..3206a6f 100644
--- a/src/pages/form-creator.html
+++ b/src/pages/form-creator.html
@@ -5,7 +5,10 @@
Create PDF Form - BentoPDF
-
+
+
+
+
diff --git a/src/pages/json-to-pdf.html b/src/pages/json-to-pdf.html
index e63c7c1..a7df403 100644
--- a/src/pages/json-to-pdf.html
+++ b/src/pages/json-to-pdf.html
@@ -5,7 +5,10 @@
JSON to PDF Converter - BentoPDF
-
+
+
+
+
diff --git a/src/pages/pdf-multi-tool.html b/src/pages/pdf-multi-tool.html
index 5490d14..546ab11 100644
--- a/src/pages/pdf-multi-tool.html
+++ b/src/pages/pdf-multi-tool.html
@@ -5,7 +5,10 @@
PDF Multi Tool - BentoPDF
-
+
+
+
+