squash: feat: Create fillable PDF forms

This commit is contained in:
abdullahalam123
2025-11-24 21:16:23 +05:30
parent 1e8866018d
commit 95927cd899
45 changed files with 3595 additions and 356 deletions

View File

@@ -3,6 +3,7 @@ import {
downloadFile,
readFileAsArrayBuffer,
hexToRgb,
resetAndReloadTool,
} from '../utils/helpers.js';
import { state, resetState } from '../state.js';
@@ -21,7 +22,7 @@ export function setupWatermarkUI() {
const imageOptions = document.getElementById('image-watermark-options');
watermarkTypeRadios.forEach((radio) => {
radio.addEventListener('change', (e) => {
radio.addEventListener('change', (e) => {
if (e.target.value === 'text') {
textOptions.classList.remove('hidden');
imageOptions.classList.add('hidden');
@@ -40,9 +41,9 @@ export function setupWatermarkUI() {
opacitySliderText.addEventListener(
'input',
() =>
(opacityValueText.textContent = (
opacitySliderText as HTMLInputElement
).value)
(opacityValueText.textContent = (
opacitySliderText as HTMLInputElement
).value)
);
angleSliderText.addEventListener(
@@ -59,17 +60,17 @@ export function setupWatermarkUI() {
opacitySliderImage.addEventListener(
'input',
() =>
(opacityValueImage.textContent = (
opacitySliderImage as HTMLInputElement
).value)
(opacityValueImage.textContent = (
opacitySliderImage as HTMLInputElement
).value)
);
angleSliderImage.addEventListener(
'input',
() =>
(angleValueImage.textContent = (
angleSliderImage as HTMLInputElement
).value)
(angleValueImage.textContent = (
angleSliderImage as HTMLInputElement
).value)
);
}
@@ -175,14 +176,7 @@ export async function addWatermark() {
'watermarked.pdf'
);
const toolid = state.activeTool;
resetState();
if (toolid) {
const element = document.querySelector(
`[data-tool-id="${toolid}"]`
) as HTMLElement;
if (element) element.click();
}
resetAndReloadTool();
} catch (e) {
console.error(e);
showAlert(

View File

@@ -7,11 +7,10 @@ import Sortable from 'sortablejs';
import { createIcons, icons } from 'lucide';
import '../../css/bookmark.css';
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
import { truncateFilename, getPDFDocument } from '../utils/helpers.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const modalContainer = document.getElementById('modal-container');
@@ -715,8 +714,10 @@ function handleResize() {
if (window.innerWidth >= 1024) {
viewerSection.classList.remove('hidden');
bookmarksSection.classList.remove('hidden');
showViewerBtn.classList.remove('bg-blue-50', 'text-blue-600');
showBookmarksBtn.classList.remove('bg-blue-50', 'text-blue-600');
showViewerBtn.classList.remove('bg-indigo-600', 'text-white');
showViewerBtn.classList.add('text-gray-300');
showBookmarksBtn.classList.remove('bg-indigo-600', 'text-white');
showBookmarksBtn.classList.add('text-gray-300');
}
}
@@ -725,15 +726,19 @@ window.addEventListener('resize', handleResize);
showViewerBtn.addEventListener('click', () => {
viewerSection.classList.remove('hidden');
bookmarksSection.classList.add('hidden');
showViewerBtn.classList.add('bg-blue-50', 'text-blue-600');
showBookmarksBtn.classList.remove('bg-blue-50', 'text-blue-600');
showViewerBtn.classList.add('bg-indigo-600', 'text-white');
showViewerBtn.classList.remove('text-gray-300');
showBookmarksBtn.classList.remove('bg-indigo-600', 'text-white');
showBookmarksBtn.classList.add('text-gray-300');
});
showBookmarksBtn.addEventListener('click', () => {
viewerSection.classList.add('hidden');
bookmarksSection.classList.remove('hidden');
showBookmarksBtn.classList.add('bg-blue-50', 'text-blue-600');
showViewerBtn.classList.remove('bg-blue-50', 'text-blue-600');
showBookmarksBtn.classList.add('bg-indigo-600', 'text-white');
showBookmarksBtn.classList.remove('text-gray-300');
showViewerBtn.classList.remove('bg-indigo-600', 'text-white');
showViewerBtn.classList.add('text-gray-300');
});
// Dropdown toggles
@@ -863,8 +868,10 @@ function resetToUploader() {
// Reset mobile view
viewerSection.classList.remove('hidden');
bookmarksSection.classList.add('hidden');
showViewerBtn.classList.add('bg-blue-50', 'text-blue-600');
showBookmarksBtn.classList.remove('bg-blue-50', 'text-blue-600');
showViewerBtn.classList.add('bg-indigo-600', 'text-white');
showViewerBtn.classList.remove('text-gray-300');
showBookmarksBtn.classList.remove('bg-indigo-600', 'text-white');
showBookmarksBtn.classList.add('text-gray-300');
}
document.addEventListener('keydown', (e) => {
@@ -1033,7 +1040,7 @@ async function loadPDF(e) {
if (!file) return;
originalFileName = file.name.replace('.pdf', '');
filenameDisplay.textContent = originalFileName;
filenameDisplay.textContent = truncateFilename(file.name);
renderFileDisplay(file);
const arrayBuffer = await file.arrayBuffer();
@@ -1046,7 +1053,7 @@ async function loadPDF(e) {
pdfLibDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
const loadingTask = pdfjsLib.getDocument({
const loadingTask = getPDFDocument({
data: new Uint8Array(arrayBuffer),
});
pdfJsDoc = await loadingTask.promise;

View File

@@ -3,10 +3,14 @@ import {
downloadFile,
hexToRgb,
readFileAsArrayBuffer,
getPDFDocument,
} from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
let isRenderingPreview = false;
let renderTimeout: any;
@@ -16,29 +20,25 @@ async function updateTextColorPreview() {
isRenderingPreview = true;
try {
const textColorCanvas = document.getElementById('text-color-canvas');
const textColorCanvas = document.getElementById('text-color-canvas') as HTMLCanvasElement;
if (!textColorCanvas) return;
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const page = await pdf.getPage(1); // Preview first page
const viewport = page.getViewport({ scale: 0.8 });
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
const context = textColorCanvas.getContext('2d');
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
textColorCanvas.width = viewport.width;
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
textColorCanvas.height = viewport.height;
await page.render({ canvasContext: context, viewport }).promise;
await page.render({ canvasContext: context, viewport, canvas: textColorCanvas }).promise;
const imageData = context.getImageData(
0,
0,
(textColorCanvas as HTMLCanvasElement).width,
(textColorCanvas as HTMLCanvasElement).height
textColorCanvas.width,
textColorCanvas.height
);
const data = imageData.data;
const colorHex = (
@@ -78,21 +78,19 @@ export async function setupTextColorTool() {
renderTimeout = setTimeout(updateTextColorPreview, 250);
});
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 0.8 });
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
originalCanvas.width = viewport.width;
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
originalCanvas.height = viewport.height;
(originalCanvas as HTMLCanvasElement).width = viewport.width;
(originalCanvas as HTMLCanvasElement).height = viewport.height;
await page.render({
canvasContext: (originalCanvas as HTMLCanvasElement).getContext('2d'),
viewport,
canvas: originalCanvas as HTMLCanvasElement,
}).promise;
await updateTextColorPreview();
}
@@ -103,16 +101,14 @@ export async function changeTextColor() {
return;
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color-input').value;
const colorHex = (document.getElementById('text-color-input') as HTMLInputElement).value;
const { r, g, b } = hexToRgb(colorHex);
const darknessThreshold = 120;
showLoader('Changing text color...');
try {
const newPdfDoc = await PDFLibDocument.create();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
@@ -126,7 +122,7 @@ export async function changeTextColor() {
canvas.height = viewport.height;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport }).promise;
await page.render({ canvasContext: context, viewport, canvas }).promise;
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
@@ -144,16 +140,15 @@ export async function changeTextColor() {
}
context.putImageData(imageData, 0, 0);
const pngImageBytes = await new Promise((resolve) =>
const pngImageBytes = await new Promise<Uint8Array>((resolve) =>
canvas.toBlob((blob) => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));
reader.readAsArrayBuffer(blob!);
}, 'image/png')
);
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
const pngImage = await newPdfDoc.embedPng(pngImageBytes);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(pngImage, {
x: 0,

View File

@@ -1,67 +1,208 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
document.addEventListener('change', (e) => {
const target = e.target as HTMLElement;
if (target.id === 'add-separator') {
const separatorOptions = document.getElementById('separator-options');
if (separatorOptions) {
const checkbox = target as HTMLInputElement;
if (checkbox.checked) {
separatorOptions.classList.remove('hidden');
} else {
separatorOptions.classList.add('hidden');
}
}
}
});
export async function combineToSinglePage() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const orientation = document.getElementById('combine-orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const spacing = parseInt(document.getElementById('page-spacing').value) || 0;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const backgroundColorHex = document.getElementById('background-color').value;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const addSeparator = document.getElementById('add-separator').checked;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const separatorThickness = parseFloat(document.getElementById('separator-thickness').value) || 0.5;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const separatorColorHex = document.getElementById('separator-color').value;
const backgroundColor = hexToRgb(backgroundColorHex);
const separatorColor = hexToRgb(separatorColorHex);
showLoader('Combining pages...');
try {
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
const sourcePages = sourceDoc.getPages();
const pdfBytes = await sourceDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
const sourcePages = sourceDoc.getPages();
let maxWidth = 0;
let maxHeight = 0;
let totalWidth = 0;
let totalHeight = 0;
sourcePages.forEach((page: any) => {
const { width, height } = page.getSize();
if (width > maxWidth) maxWidth = width;
if (height > maxHeight) maxHeight = height;
totalWidth += width;
totalHeight += height;
});
totalHeight += Math.max(0, sourcePages.length - 1) * spacing;
const newPage = newDoc.addPage([maxWidth, totalHeight]);
let finalWidth, finalHeight;
if (orientation === 'horizontal') {
finalWidth = totalWidth + Math.max(0, sourcePages.length - 1) * spacing;
finalHeight = maxHeight;
} else {
finalWidth = maxWidth;
finalHeight = totalHeight + Math.max(0, sourcePages.length - 1) * spacing;
}
const newPage = newDoc.addPage([finalWidth, finalHeight]);
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
newPage.drawRectangle({
x: 0,
y: 0,
width: maxWidth,
height: totalHeight,
width: finalWidth,
height: finalHeight,
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
});
}
let currentY = totalHeight;
let currentX = 0;
let currentY = finalHeight;
for (let i = 0; i < sourcePages.length; i++) {
const sourcePage = sourcePages[i];
const { width, height } = sourcePage.getSize();
const embeddedPage = await newDoc.embedPage(sourcePage);
currentY -= height;
const x = (maxWidth - width) / 2;
try {
const page = await pdfjsDoc.getPage(i + 1);
const scale = 2.0;
const viewport = page.getViewport({ scale });
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d')!;
if (addSeparator && i < sourcePages.length - 1) {
const lineY = currentY - spacing / 2;
newPage.drawLine({
start: { x: 0, y: lineY },
end: { x: maxWidth, y: lineY },
thickness: 0.5,
color: rgb(0.8, 0.8, 0.8),
});
await page.render({
canvasContext: context,
viewport,
canvas
}).promise;
const pngDataUrl = canvas.toDataURL('image/png');
const pngImage = await newDoc.embedPng(pngDataUrl);
if (orientation === 'horizontal') {
const y = (finalHeight - height) / 2;
newPage.drawImage(pngImage, { x: currentX, y, width, height });
} else {
// Vertical layout: stack top to bottom
currentY -= height;
const x = (finalWidth - width) / 2; // Center horizontally
newPage.drawImage(pngImage, { x, y: currentY, width, height });
}
} catch (renderError) {
console.warn(`Failed to render page ${i + 1} with PDF.js, trying fallback method:`, renderError);
// Fallback: try to copy and embed the page directly
try {
const [copiedPage] = await newDoc.copyPages(sourceDoc, [i]);
if (orientation === 'horizontal') {
const y = (finalHeight - height) / 2;
const embeddedPage = await newDoc.embedPage(copiedPage);
newPage.drawPage(embeddedPage, { x: currentX, y, width, height });
} else {
currentY -= height;
const x = (finalWidth - width) / 2;
const embeddedPage = await newDoc.embedPage(copiedPage);
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
}
} catch (embedError) {
console.error(`Failed to process page ${i + 1}:`, embedError);
if (orientation === 'horizontal') {
const y = (finalHeight - height) / 2;
newPage.drawRectangle({
x: currentX,
y,
width,
height,
borderColor: rgb(0.8, 0, 0),
borderWidth: 2,
});
newPage.drawText(`Page ${i + 1} could not be rendered`, {
x: currentX + 10,
y: y + height / 2,
size: 12,
color: rgb(0.8, 0, 0),
});
} else {
currentY -= height;
const x = (finalWidth - width) / 2;
newPage.drawRectangle({
x,
y: currentY,
width,
height,
borderColor: rgb(0.8, 0, 0),
borderWidth: 2,
});
newPage.drawText(`Page ${i + 1} could not be rendered`, {
x: x + 10,
y: currentY + height / 2,
size: 12,
color: rgb(0.8, 0, 0),
});
}
}
}
currentY -= spacing;
// Draw separator line
if (addSeparator && i < sourcePages.length - 1) {
if (orientation === 'horizontal') {
const lineX = currentX + width + spacing / 2;
newPage.drawLine({
start: { x: lineX, y: 0 },
end: { x: lineX, y: finalHeight },
thickness: separatorThickness,
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
});
currentX += width + spacing;
} else {
const lineY = currentY - spacing / 2;
newPage.drawLine({
start: { x: 0, y: lineY },
end: { x: finalWidth, y: lineY },
thickness: separatorThickness,
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
});
currentY -= spacing;
}
} else {
if (orientation === 'horizontal') {
currentX += width + spacing;
} else {
currentY -= spacing;
}
}
}
const newPdfBytes = await newDoc.save();

View File

@@ -1,6 +1,10 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const state = {
pdfDoc1: null,
@@ -122,8 +126,7 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) {
try {
showLoader(`Loading ${file.name}...`);
const pdfBytes = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
state[docKey] = await pdfjsLib.getDocument(pdfBytes).promise;
state[docKey] = await getPDFDocument(pdfBytes).promise;
if (state.pdfDoc1 && state.pdfDoc2) {
document.getElementById('compare-viewer').classList.remove('hidden');

View File

@@ -3,12 +3,14 @@ import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
getPDFDocument,
} from '../utils/helpers.js';
import { state } from '../state.js';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
function dataUrlToBytes(dataUrl: any) {
const base64 = dataUrl.split(',')[1];
const binaryString = atob(base64);
@@ -70,8 +72,8 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
const bitsPerComponent =
stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
? (
stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber
).asNumber()
stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber
).asNumber()
: 8;
if (width > 0 && height > 0) {
@@ -183,12 +185,7 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
}
async function performLegacyCompression(arrayBuffer: any, settings: any) {
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise;
const newPdfDoc = await PDFDocument.create();
for (let i = 1; i <= pdfJsDoc.numPages; i++) {
@@ -324,13 +321,13 @@ export async function compress() {
showAlert(
'Compression Complete',
`Method: **${usedMethod}**. ` +
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
);
} else {
showAlert(
'Compression Finished',
`Method: **${usedMethod}**. ` +
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
'warning'
);
@@ -384,13 +381,13 @@ export async function compress() {
showAlert(
'Compression Complete',
`Compressed ${state.files.length} PDF(s). ` +
`Total size reduced from ${formatBytes(totalOriginalSize)} to ${formatBytes(totalCompressedSize)} (Saved ${totalSavingsPercent}%).`
`Total size reduced from ${formatBytes(totalOriginalSize)} to ${formatBytes(totalCompressedSize)} (Saved ${totalSavingsPercent}%).`
);
} else {
showAlert(
'Compression Finished',
`Compressed ${state.files.length} PDF(s). ` +
`Total size: ${formatBytes(totalCompressedSize)}.`
`Total size: ${formatBytes(totalCompressedSize)}.`
);
}

View File

@@ -1,10 +1,12 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import Cropper from 'cropperjs';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
// --- Global State for the Cropper Tool ---
const cropperState = {
pdfDoc: null,
@@ -129,45 +131,44 @@ function enableControls() {
*/
async function performMetadataCrop(pdfToModify: any, cropData: any) {
for (const pageNum in cropData) {
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
const page = pdfToModify.getPages()[pageNum - 1];
const { width: pageWidth, height: pageHeight } = page.getSize();
const rotation = page.getRotation().angle;
const pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum));
const viewport = pdfJsPage.getViewport({ scale: 1 });
const crop = cropData[pageNum];
const visualPdfWidth = pageWidth * crop.width;
const visualPdfHeight = pageHeight * crop.height;
const visualPdfX = pageWidth * crop.x;
const visualPdfY = pageHeight * crop.y;
// Man I hate doing math
// Calculate visual crop rectangle in viewport pixels
const cropX = viewport.width * crop.x;
const cropY = viewport.height * crop.y;
const cropW = viewport.width * crop.width;
const cropH = viewport.height * crop.height;
let finalX, finalY, finalWidth, finalHeight;
switch (rotation) {
case 90:
finalX = visualPdfY;
finalY = pageWidth - visualPdfX - visualPdfWidth;
finalWidth = visualPdfHeight;
finalHeight = visualPdfWidth;
break;
case 180:
finalX = pageWidth - visualPdfX - visualPdfWidth;
finalY = pageHeight - visualPdfY - visualPdfHeight;
finalWidth = visualPdfWidth;
finalHeight = visualPdfHeight;
break;
case 270:
finalX = pageHeight - visualPdfY - visualPdfHeight;
finalY = visualPdfX;
finalWidth = visualPdfHeight;
finalHeight = visualPdfWidth;
break;
default:
finalX = visualPdfX;
finalY = pageHeight - visualPdfY - visualPdfHeight;
finalWidth = visualPdfWidth;
finalHeight = visualPdfHeight;
break;
}
page.setCropBox(finalX, finalY, finalWidth, finalHeight);
// Define the 4 corners of the crop rectangle in visual coordinates (Top-Left origin)
const visualCorners = [
{ x: cropX, y: cropY }, // TL
{ x: cropX + cropW, y: cropY }, // TR
{ x: cropX + cropW, y: cropY + cropH }, // BR
{ x: cropX, y: cropY + cropH }, // BL
];
// This handles rotation, media box offsets, and coordinate system flips automatically
const pdfCorners = visualCorners.map(p => {
return viewport.convertToPdfPoint(p.x, p.y);
});
// Find the bounding box of the converted points in PDF coordinates
// convertToPdfPoint returns [x, y] arrays
const pdfXs = pdfCorners.map(p => p[0]);
const pdfYs = pdfCorners.map(p => p[1]);
const minX = Math.min(...pdfXs);
const maxX = Math.max(...pdfXs);
const minY = Math.min(...pdfYs);
const maxY = Math.max(...pdfYs);
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
const page = pdfToModify.getPages()[pageNum - 1];
page.setCropBox(minX, minY, maxX - minX, maxY - minY);
}
}
@@ -242,107 +243,104 @@ export async function setupCropperTool() {
if (state.files.length === 0) return;
// Clear pageCrops on new file upload
cropperState.pageCrops = {};
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
cropperState.originalPdfBytes = arrayBuffer;
const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
try {
const loadingTask = pdfjsLib.getDocument({ data: arrayBufferForPdfJs });
// Clear pageCrops on new file upload
cropperState.pageCrops = {};
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
cropperState.originalPdfBytes = arrayBuffer;
const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
const loadingTask = getPDFDocument({ data: arrayBufferForPdfJs });
cropperState.pdfDoc = await loadingTask.promise;
cropperState.currentPageNum = 1;
await displayPageAsImage(cropperState.currentPageNum);
document
.getElementById('prev-page')
.addEventListener('click', () => changePage(-1));
document
.getElementById('next-page')
.addEventListener('click', () => changePage(1));
document
.getElementById('crop-button')
.addEventListener('click', async () => {
// Get the last known crop from the active page before processing
saveCurrentCrop();
const isDestructive = (
document.getElementById('destructive-crop-toggle') as HTMLInputElement
).checked;
const isApplyToAll = (
document.getElementById('apply-to-all-toggle') as HTMLInputElement
).checked;
let finalCropData = {};
if (isApplyToAll) {
const currentCrop =
cropperState.pageCrops[cropperState.currentPageNum];
if (!currentCrop) {
showAlert('No Crop Area', 'Please select an area to crop first.');
return;
}
// Apply the active page's crop to all pages
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
finalCropData[i] = currentCrop;
}
} else {
// If not applying to all, only process pages with saved crops
finalCropData = Object.keys(cropperState.pageCrops).reduce(
(obj, key) => {
obj[key] = cropperState.pageCrops[key];
return obj;
},
{}
);
}
if (Object.keys(finalCropData).length === 0) {
showAlert(
'No Crop Area',
'Please select an area on at least one page to crop.'
);
return;
}
showLoader('Applying crop...');
try {
let finalPdfBytes;
if (isDestructive) {
const newPdfDoc = await performFlatteningCrop(finalCropData);
finalPdfBytes = await newPdfDoc.save();
} else {
const pdfToModify = await PDFLibDocument.load(
cropperState.originalPdfBytes
);
await performMetadataCrop(pdfToModify, finalCropData);
finalPdfBytes = await pdfToModify.save();
}
const fileName = isDestructive
? 'flattened_crop.pdf'
: 'standard_crop.pdf';
downloadFile(
new Blob([finalPdfBytes], { type: 'application/pdf' }),
fileName
);
showAlert('Success', 'Crop complete! Your download has started.');
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred during cropping.');
} finally {
hideLoader();
}
});
} catch (error) {
console.error('Error setting up cropper tool:', error);
showAlert('Error', 'Failed to load PDF for cropping.');
}
document
.getElementById('prev-page')
.addEventListener('click', () => changePage(-1));
document
.getElementById('next-page')
.addEventListener('click', () => changePage(1));
document
.getElementById('crop-button')
.addEventListener('click', async () => {
// Get the last known crop from the active page before processing
saveCurrentCrop();
const isDestructive = (
document.getElementById('destructive-crop-toggle') as HTMLInputElement
).checked;
const isApplyToAll = (
document.getElementById('apply-to-all-toggle') as HTMLInputElement
).checked;
let finalCropData = {};
if (isApplyToAll) {
const currentCrop =
cropperState.pageCrops[cropperState.currentPageNum];
if (!currentCrop) {
showAlert('No Crop Area', 'Please select an area to crop first.');
return;
}
// Apply the active page's crop to all pages
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
finalCropData[i] = currentCrop;
}
} else {
// If not applying to all, only process pages with saved crops
finalCropData = Object.keys(cropperState.pageCrops).reduce(
(obj, key) => {
obj[key] = cropperState.pageCrops[key];
return obj;
},
{}
);
}
if (Object.keys(finalCropData).length === 0) {
showAlert(
'No Crop Area',
'Please select an area on at least one page to crop.'
);
return;
}
showLoader('Applying crop...');
try {
let finalPdfBytes;
if (isDestructive) {
const newPdfDoc = await performFlatteningCrop(finalCropData);
finalPdfBytes = await newPdfDoc.save();
} else {
const pdfToModify = await PDFLibDocument.load(
cropperState.originalPdfBytes
);
await performMetadataCrop(pdfToModify, finalCropData);
finalPdfBytes = await pdfToModify.save();
}
const fileName = isDestructive
? 'flattened_crop.pdf'
: 'standard_crop.pdf';
downloadFile(
new Blob([finalPdfBytes], { type: 'application/pdf' }),
fileName
);
showAlert('Success', 'Crop complete! Your download has started.');
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred during cropping.');
} finally {
hideLoader();
}
});
}

View File

@@ -1,10 +1,13 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { downloadFile, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
import Sortable from 'sortablejs';
import { icons, createIcons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const duplicateOrganizeState = {
sortableInstances: {},
@@ -88,8 +91,7 @@ export async function renderDuplicateOrganizeThumbnails() {
showLoader('Rendering page previews...');
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
const pdfjsDoc = await getPDFDocument({ data: pdfData }).promise;
grid.textContent = '';
@@ -191,7 +193,7 @@ export async function processAndSave() {
}
const newPdfDoc = await PDFLibDocument.create();
const totalPages = state.pdfDoc.getPageCount();
const invalidIndices = finalIndices.filter(i => i >= totalPages);
if (invalidIndices.length > 0) {

2040
src/js/logic/form-creator.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,11 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { downloadFile, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function invertColors() {
if (!state.pdfDoc) {
@@ -12,8 +16,7 @@ export async function invertColors() {
try {
const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i);
@@ -22,7 +25,7 @@ export async function invertColors() {
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport }).promise;
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;

View File

@@ -1,5 +1,5 @@
import { showLoader, hideLoader, showAlert } from '../ui.ts';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.ts';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.ts';
import { state } from '../state.ts';
import { renderPagesProgressively, cleanupLazyRendering, createPlaceholder } from '../utils/render-utils.ts';
@@ -8,10 +8,7 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import Sortable from 'sortablejs';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const mergeState = {
pdfDocs: {},
@@ -183,7 +180,7 @@ async function renderPageMergeThumbnails() {
if (!pdfDoc) continue;
const pdfData = await pdfDoc.save();
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
const pdfjsDoc = await getPDFDocument({ data: pdfData }).promise;
// Create a wrapper function that includes the file name
const createWrapperWithFileName = (canvas: HTMLCanvasElement, pageNumber: number) => {

View File

@@ -1,10 +1,14 @@
import { tesseractLanguages } from '../config/tesseract-languages.js';
import { showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import Tesseract from 'tesseract.js';
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
import { icons, createIcons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
let searchablePdfBytes: any = null;
@@ -117,8 +121,7 @@ async function runOCR() {
tessedit_char_whitelist: whitelist,
});
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const newPdfDoc = await PDFLibDocument.create();
@@ -136,7 +139,7 @@ async function runOCR() {
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport }).promise;
await page.render({ canvasContext: context, viewport, canvas }).promise;
if (binarize) {
binarizeCanvas(context);

View File

@@ -1,8 +1,120 @@
import { state } from '../state.js';
import { getStandardPageName, convertPoints } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
let analyzedPagesData: any = []; // Store raw data to avoid re-analyzing
function calculateAspectRatio(width: number, height: number): string {
const ratio = width / height;
return ratio.toFixed(3);
}
function calculateArea(width: number, height: number, unit: string): string {
const areaInPoints = width * height;
let convertedArea = 0;
let unitSuffix = '';
switch (unit) {
case 'in':
convertedArea = areaInPoints / (72 * 72); // 72 points per inch
unitSuffix = 'in²';
break;
case 'mm':
convertedArea = areaInPoints / (72 * 72) * (25.4 * 25.4); // Convert to mm²
unitSuffix = 'mm²';
break;
case 'px':
const pxPerPoint = 96 / 72;
convertedArea = areaInPoints * (pxPerPoint * pxPerPoint);
unitSuffix = 'px²';
break;
default: // 'pt'
convertedArea = areaInPoints;
unitSuffix = 'pt²';
break;
}
return `${convertedArea.toFixed(2)} ${unitSuffix}`;
}
function getSummaryStats() {
const totalPages = analyzedPagesData.length;
// Count unique page sizes
const uniqueSizes = new Map();
analyzedPagesData.forEach((pageData: any) => {
const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`;
const label = `${pageData.standardSize} (${pageData.orientation})`;
uniqueSizes.set(key, {
count: (uniqueSizes.get(key)?.count || 0) + 1,
label: label,
width: pageData.width,
height: pageData.height
});
});
const hasMixedSizes = uniqueSizes.size > 1;
return {
totalPages,
uniqueSizesCount: uniqueSizes.size,
uniqueSizes: Array.from(uniqueSizes.values()),
hasMixedSizes
};
}
function renderSummary() {
const summaryContainer = document.getElementById('dimensions-summary');
if (!summaryContainer) return;
const stats = getSummaryStats();
let summaryHTML = `
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Total Pages</p>
<p class="text-2xl font-bold text-white">${stats.totalPages}</p>
</div>
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Unique Page Sizes</p>
<p class="text-2xl font-bold text-white">${stats.uniqueSizesCount}</p>
</div>
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Document Type</p>
<p class="text-2xl font-bold ${stats.hasMixedSizes ? 'text-yellow-400' : 'text-green-400'}">
${stats.hasMixedSizes ? 'Mixed Sizes' : 'Uniform'}
</p>
</div>
</div>
`;
if (stats.hasMixedSizes) {
summaryHTML += `
<div class="bg-yellow-900/20 border border-yellow-500/30 rounded-lg p-4 mb-4">
<div class="flex items-start gap-3">
<i data-lucide="alert-triangle" class="w-5 h-5 text-yellow-400 mt-0.5 flex-shrink-0"></i>
<div>
<h4 class="text-yellow-200 font-semibold mb-2">Mixed Page Sizes Detected</h4>
<p class="text-sm text-gray-300 mb-3">This document contains pages with different dimensions:</p>
<ul class="space-y-1 text-sm text-gray-300">
${stats.uniqueSizes.map((size: any) => `
<li>• ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}</li>
`).join('')}
</ul>
</div>
</div>
</div>
`;
}
summaryContainer.innerHTML = summaryHTML;
if (stats.hasMixedSizes) {
createIcons({ icons });
}
}
/**
* Renders the dimensions table based on the stored data and selected unit.
* @param {string} unit The unit to display dimensions in ('pt', 'in', 'mm', 'px').
@@ -16,31 +128,89 @@ function renderTable(unit: any) {
analyzedPagesData.forEach((pageData) => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
const area = calculateArea(pageData.width, pageData.height, unit);
const row = document.createElement('tr');
// Create and append each cell safely using textContent
// Page number
const pageNumCell = document.createElement('td');
pageNumCell.className = 'px-4 py-3 text-white';
pageNumCell.textContent = pageData.pageNum;
// Dimensions
const dimensionsCell = document.createElement('td');
dimensionsCell.className = 'px-4 py-3 text-gray-300';
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
// Standard size
const sizeCell = document.createElement('td');
sizeCell.className = 'px-4 py-3 text-gray-300';
sizeCell.textContent = pageData.standardSize;
// Orientation
const orientationCell = document.createElement('td');
orientationCell.className = 'px-4 py-3 text-gray-300';
orientationCell.textContent = pageData.orientation;
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell);
// Aspect Ratio
const aspectRatioCell = document.createElement('td');
aspectRatioCell.className = 'px-4 py-3 text-gray-300';
aspectRatioCell.textContent = aspectRatio;
// Area
const areaCell = document.createElement('td');
areaCell.className = 'px-4 py-3 text-gray-300';
areaCell.textContent = area;
// Rotation
const rotationCell = document.createElement('td');
rotationCell.className = 'px-4 py-3 text-gray-300';
rotationCell.textContent = `${pageData.rotation}°`;
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell, aspectRatioCell, areaCell, rotationCell);
tableBody.appendChild(row);
});
}
function exportToCSV() {
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement;
const unit = unitsSelect?.value || 'pt';
const headers = ['Page #', `Width (${unit})`, `Height (${unit})`, 'Standard Size', 'Orientation', 'Aspect Ratio', `Area (${unit}²)`, 'Rotation'];
const csvRows = [headers.join(',')];
analyzedPagesData.forEach((pageData: any) => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
const area = calculateArea(pageData.width, pageData.height, unit);
const row = [
pageData.pageNum,
width,
height,
pageData.standardSize,
pageData.orientation,
aspectRatio,
area,
`${pageData.rotation}°`
];
csvRows.push(row.join(','));
});
const csvContent = csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'page-dimensions.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/**
* Main function to analyze the PDF and display dimensions.
* This is called once after the file is loaded.
@@ -53,28 +223,36 @@ export function analyzeAndDisplayDimensions() {
pages.forEach((page: any, index: any) => {
const { width, height } = page.getSize();
const rotation = page.getRotation().angle || 0;
analyzedPagesData.push({
pageNum: index + 1,
width, // Store raw width in points
height, // Store raw height in points
orientation: width > height ? 'Landscape' : 'Portrait',
standardSize: getStandardPageName(width, height),
rotation: rotation
});
});
const resultsContainer = document.getElementById('dimensions-results');
const unitsSelect = document.getElementById('units-select');
renderSummary();
// Initial render with default unit (points)
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
renderTable(unitsSelect.value);
// Show the results table
resultsContainer.classList.remove('hidden');
// Add event listener to handle unit changes
unitsSelect.addEventListener('change', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
renderTable(e.target.value);
});
const exportButton = document.getElementById('export-csv-btn');
if (exportButton) {
exportButton.addEventListener('click', exportToCSV);
}
}

View File

@@ -3,7 +3,7 @@ import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import JSZip from 'jszip';
import Sortable from 'sortablejs';
import { downloadFile } from '../utils/helpers';
import { downloadFile, getPDFDocument } from '../utils/helpers';
import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, createPlaceholder } from '../utils/render-utils';
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
@@ -357,7 +357,7 @@ async function loadPdfs(files: File[]) {
const pdfIndex = currentPdfDocs.length - 1;
const pdfBytes = await pdfDoc.save();
const pdfjsDoc = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
const pdfjsDoc = await getPDFDocument({ data: new Uint8Array(pdfBytes) }).promise;
const numPages = pdfjsDoc.numPages;
// Pre-fill allPages with placeholders to maintain order/state
@@ -741,7 +741,7 @@ async function handleInsertPdf(e: Event) {
// Load PDF.js document for rendering
const pdfBytes = await pdfDoc.save();
const pdfjsDoc = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
const pdfjsDoc = await getPDFDocument({ data: new Uint8Array(pdfBytes) }).promise;
const numPages = pdfjsDoc.numPages;
const newPages: PageData[] = [];

View File

@@ -1,7 +1,10 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
/**
* Creates a BMP file buffer from raw pixel data (ImageData).
@@ -53,8 +56,7 @@ function encodeBMP(imageData: any) {
export async function pdfToBmp() {
showLoader('Converting PDF to BMP images...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();
@@ -69,7 +71,7 @@ export async function pdfToBmp() {
canvas.width = viewport.width;
// Render the PDF page directly to the canvas
await page.render({ canvasContext: context, viewport: viewport }).promise;
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise;
// Get the raw pixel data from this canvas
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);

View File

@@ -1,9 +1,11 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { downloadFile, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function pdfToGreyscale() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
@@ -13,8 +15,7 @@ export async function pdfToGreyscale() {
try {
const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i);
@@ -24,7 +25,7 @@ export async function pdfToGreyscale() {
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport }).promise;
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;

View File

@@ -1,13 +1,16 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function pdfToJpg() {
showLoader('Converting to JPG...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();
@@ -23,7 +26,7 @@ export async function pdfToJpg() {
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport: viewport }).promise;
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise;
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', quality)

View File

@@ -1,5 +1,5 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
export async function pdfToMarkdown() {
@@ -7,8 +7,7 @@ export async function pdfToMarkdown() {
try {
const file = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const pdf = await getPDFDocument({ data: arrayBuffer }).promise;
let markdown = '';
for (let i = 1; i <= pdf.numPages; i++) {

View File

@@ -1,20 +1,23 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function pdfToPng() {
showLoader('Converting to PNG...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();
const qualityInput = document.getElementById('png-quality') as HTMLInputElement;
const scale = qualityInput ? parseFloat(qualityInput.value) : 2.0;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale });
@@ -22,7 +25,7 @@ export async function pdfToPng() {
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise;
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise;
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/png')
);

View File

@@ -1,14 +1,16 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import UTIF from 'utif';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function pdfToTiff() {
showLoader('Converting PDF to TIFF...');
try {
const pdf = await pdfjsLib.getDocument(
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();

View File

@@ -1,13 +1,16 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function pdfToWebp() {
showLoader('Converting to WebP...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();
@@ -18,10 +21,10 @@ export async function pdfToWebp() {
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise;
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise;
const qualityInput = document.getElementById('webp-quality') as HTMLInputElement;
const quality = qualityInput ? parseFloat(qualityInput.value) : 0.9;
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/webp', quality)
);

View File

@@ -1,10 +1,12 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, parsePageRanges } from '../utils/helpers.js';
import { downloadFile, parsePageRanges, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument, PageSizes } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import { createIcons, icons } from 'lucide';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const posterizeState = {
pdfJsDoc: null,
pageSnapshots: {},
@@ -121,7 +123,7 @@ export async function setupPosterizeTool() {
.getPageCount()
.toString();
const pdfBytes = await state.pdfDoc.save();
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes })
posterizeState.pdfJsDoc = await getPDFDocument({ data: pdfBytes })
.promise;
posterizeState.pageSnapshots = {};
posterizeState.currentPage = 1;

View File

@@ -1,10 +1,12 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { downloadFile, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFPageProxy } from 'pdfjs-dist/types/src/display/api.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
let analysisCache = [];
async function isPageBlank(page: PDFPageProxy, threshold: number) {
@@ -38,7 +40,7 @@ async function analyzePages() {
showLoader('Analyzing for blank pages...');
const pdfBytes = await state.pdfDoc.save();
const pdf = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
const pdf = await getPDFDocument({ data: pdfBytes }).promise;
analysisCache = [];
const promises = [];

View File

@@ -1,7 +1,7 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { downloadFile, resetAndReloadTool } from '../utils/helpers.js';
import { state } from '../state.js';
import { getRotationState } from '../handlers/fileHandler.js';
import { getRotationState, resetRotationState } from '../handlers/fileHandler.js';
import { degrees } from 'pdf-lib';
@@ -24,6 +24,10 @@ export async function rotate() {
new Blob([rotatedPdfBytes], { type: 'application/pdf' }),
'rotated.pdf'
);
resetAndReloadTool(() => {
resetRotationState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'Could not apply rotations.');

View File

@@ -1,3 +1,4 @@
import { PDFDocument } from 'pdf-lib';
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
@@ -50,7 +51,7 @@ export async function setupSignTool() {
enablePermissions: false,
};
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs));
} catch {}
} catch { }
const viewerUrl = new URL('/pdfjs-viewer/viewer.html', window.location.origin);
const query = new URLSearchParams({ file: blobUrl });
@@ -83,7 +84,7 @@ export async function setupSignTool() {
try {
const highlightBtn = doc.getElementById('editorHighlightButton') as HTMLButtonElement | null;
highlightBtn?.click();
} catch {}
} catch { }
});
}
} catch (e) {
@@ -115,11 +116,41 @@ export async function applyAndSaveSignatures() {
return;
}
// Delegate to the built-in download behavior of the base viewer.
const app = viewerWindow.PDFViewerApplication;
app.eventBus?.dispatch('download', { source: app });
const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null;
const shouldFlatten = flattenCheckbox?.checked;
if (shouldFlatten) {
showLoader('Flattening and saving PDF...');
const rawPdfBytes = await app.pdfDocument.saveDocument(app.pdfDocument.annotationStorage);
const pdfBytes = new Uint8Array(rawPdfBytes);
const pdfDoc = await PDFDocument.load(pdfBytes);
pdfDoc.getForm().flatten();
const flattenedPdfBytes = await pdfDoc.save();
const blob = new Blob([flattenedPdfBytes as BlobPart], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `signed_flattened_${state.files[0].name}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
hideLoader();
} else {
// Delegate to the built-in download behavior of the base viewer.
app.eventBus?.dispatch('download', { source: app });
}
} catch (error) {
console.error('Failed to trigger download in base PDF.js viewer:', error);
console.error('Failed to export the signed PDF:', error);
hideLoader();
showAlert('Export failed', 'Could not export the signed PDF. Please try again.');
}
}

View File

@@ -1,7 +1,9 @@
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, getPDFDocument } from '../utils/helpers.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
import { state } from '../state.js';
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
import JSZip from 'jszip';
@@ -27,7 +29,7 @@ async function renderVisualSelector() {
try {
const pdfData = await state.pdfDoc.save();
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
const pdf = await getPDFDocument({ data: pdfData }).promise;
// Function to create wrapper element for each page
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => {