squash: feat: Create fillable PDF forms
This commit is contained in:
@@ -1,9 +1,29 @@
|
||||
import { showLoader, hideLoader, showAlert } from './ui.js';
|
||||
import { getPDFDocument } from './utils/helpers.js';
|
||||
import { state } from './state.js';
|
||||
import { toolLogic } from './logic/index.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
const editorState = {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
|
||||
const editorState: {
|
||||
pdf: any;
|
||||
canvas: any;
|
||||
context: any;
|
||||
container: any;
|
||||
currentPageNum: number;
|
||||
pageRendering: boolean;
|
||||
pageNumPending: number | null;
|
||||
scale: number | 'fit';
|
||||
pageSnapshot: any;
|
||||
isDrawing: boolean;
|
||||
startX: number;
|
||||
startY: number;
|
||||
cropBoxes: Record<number, any>;
|
||||
lastInteractionRect: { x: number; y: number; width: number; height: number } | null;
|
||||
} = {
|
||||
pdf: null,
|
||||
canvas: null,
|
||||
context: null,
|
||||
@@ -41,7 +61,6 @@ async function renderPage(num: any) {
|
||||
try {
|
||||
const page = await editorState.pdf.getPage(num);
|
||||
|
||||
// @ts-expect-error TS(2367) FIXME: This condition will always return 'false' since th... Remove this comment to see the full error message
|
||||
if (editorState.scale === 'fit') {
|
||||
editorState.scale = calculateFitScale(page);
|
||||
}
|
||||
@@ -185,12 +204,10 @@ export async function setupCanvasEditor(toolId: any) {
|
||||
|
||||
const pageNav = document.getElementById('page-nav');
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
editorState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
editorState.pdf = await getPDFDocument({ data: pdfData }).promise;
|
||||
|
||||
editorState.cropBoxes = {};
|
||||
editorState.currentPageNum = 1;
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'string' is not assignable to type 'number'.
|
||||
editorState.scale = 'fit';
|
||||
|
||||
pageNav.textContent = '';
|
||||
@@ -256,11 +273,13 @@ export async function setupCanvasEditor(toolId: any) {
|
||||
|
||||
if (toolId === 'crop') {
|
||||
document.getElementById('zoom-in-btn').onclick = () => {
|
||||
editorState.scale += 0.25;
|
||||
if (typeof editorState.scale === 'number') {
|
||||
editorState.scale += 0.25;
|
||||
}
|
||||
renderPage(editorState.currentPageNum);
|
||||
};
|
||||
document.getElementById('zoom-out-btn').onclick = () => {
|
||||
if (editorState.scale > 0.25) {
|
||||
if (typeof editorState.scale === 'number' && editorState.scale > 0.25) {
|
||||
editorState.scale -= 0.25;
|
||||
renderPage(editorState.currentPageNum);
|
||||
}
|
||||
|
||||
@@ -161,6 +161,12 @@ export const categories = [
|
||||
icon: 'square-pen',
|
||||
subtitle: 'Fill in forms directly in the browser. Also supports XFA forms.',
|
||||
},
|
||||
{
|
||||
href: '/src/pages/form-creator.html',
|
||||
name: 'Create PDF Form',
|
||||
icon: 'file-input',
|
||||
subtitle: 'Create fillable PDF forms with drag-and-drop text fields.',
|
||||
},
|
||||
{
|
||||
id: 'remove-blank-pages',
|
||||
name: 'Remove Blank Pages',
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
renderFileDisplay,
|
||||
switchView,
|
||||
} from '../ui.js';
|
||||
import { formatIsoDate, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { formatIsoDate, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||
import { setupCanvasEditor } from '../canvasEditor.js';
|
||||
import { toolLogic } from '../logic/index.js';
|
||||
import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js';
|
||||
@@ -21,16 +21,26 @@ import {
|
||||
} from '../config/pdf-tools.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
// Global state for rotation tracking (used by Rotate tool)
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
const rotationState: number[] = [];
|
||||
let imageSortableInstance: Sortable | null = null;
|
||||
const activeImageUrls = new Map<File, string>();
|
||||
|
||||
// Export getter for rotation state (used by ui.ts)
|
||||
export function getRotationState(): readonly number[] {
|
||||
return rotationState;
|
||||
}
|
||||
|
||||
export function updateRotationState(pageIndex: number, rotation: number) {
|
||||
if (pageIndex >= 0 && pageIndex < rotationState.length) {
|
||||
rotationState[pageIndex] = rotation;
|
||||
}
|
||||
}
|
||||
|
||||
export function resetRotationState() {
|
||||
rotationState.length = 0;
|
||||
}
|
||||
|
||||
async function handleSinglePdfUpload(toolId, file) {
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
@@ -164,7 +174,7 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
|
||||
try {
|
||||
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({
|
||||
const pdfjsDoc = await getPDFDocument({
|
||||
data: pdfBytes as ArrayBuffer,
|
||||
}).promise;
|
||||
const [metadataResult, fieldObjects] = await Promise.all([
|
||||
@@ -469,6 +479,43 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
toolLogic['page-dimensions']();
|
||||
}
|
||||
|
||||
// Setup quality sliders for image conversion tools
|
||||
if (toolId === 'pdf-to-jpg') {
|
||||
const qualitySlider = document.getElementById('jpg-quality') as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('jpg-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
|
||||
};
|
||||
qualitySlider.addEventListener('input', updateValue);
|
||||
updateValue();
|
||||
}
|
||||
}
|
||||
|
||||
if (toolId === 'pdf-to-png') {
|
||||
const qualitySlider = document.getElementById('png-quality') as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('png-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
qualityValue.textContent = `${qualitySlider.value}x`;
|
||||
};
|
||||
qualitySlider.addEventListener('input', updateValue);
|
||||
updateValue();
|
||||
}
|
||||
}
|
||||
|
||||
if (toolId === 'pdf-to-webp') {
|
||||
const qualitySlider = document.getElementById('webp-quality') as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('webp-quality-value');
|
||||
if (qualitySlider && qualityValue) {
|
||||
const updateValue = () => {
|
||||
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
|
||||
};
|
||||
qualitySlider.addEventListener('input', updateValue);
|
||||
updateValue();
|
||||
}
|
||||
}
|
||||
|
||||
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
|
||||
toolLogic[toolId].setup();
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)}.`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -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
2040
src/js/logic/form-creator.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
108
src/js/main.ts
108
src/js/main.ts
@@ -10,10 +10,7 @@ import { formatShortcutDisplay, formatStars } from './utils/helpers.js';
|
||||
import { APP_VERSION, injectVersion } from '../version.js';
|
||||
|
||||
const init = () => {
|
||||
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();
|
||||
|
||||
// Handle simple mode - hide branding sections but keep logo and copyright
|
||||
// Handle simple mode - hide branding sections but keep logo and copyright
|
||||
@@ -308,9 +305,87 @@ const init = () => {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initialize Shortcuts System
|
||||
ShortcutsManager.init();
|
||||
|
||||
// Tab switching for settings modal
|
||||
const shortcutsTabBtn = document.getElementById('shortcuts-tab-btn');
|
||||
const preferencesTabBtn = document.getElementById('preferences-tab-btn');
|
||||
const shortcutsTabContent = document.getElementById('shortcuts-tab-content');
|
||||
const preferencesTabContent = document.getElementById('preferences-tab-content');
|
||||
const shortcutsTabFooter = document.getElementById('shortcuts-tab-footer');
|
||||
const preferencesTabFooter = document.getElementById('preferences-tab-footer');
|
||||
const resetShortcutsBtn = document.getElementById('reset-shortcuts-btn');
|
||||
|
||||
if (shortcutsTabBtn && preferencesTabBtn) {
|
||||
shortcutsTabBtn.addEventListener('click', () => {
|
||||
shortcutsTabBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
shortcutsTabBtn.classList.remove('text-gray-300');
|
||||
preferencesTabBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
preferencesTabBtn.classList.add('text-gray-300');
|
||||
shortcutsTabContent?.classList.remove('hidden');
|
||||
preferencesTabContent?.classList.add('hidden');
|
||||
shortcutsTabFooter?.classList.remove('hidden');
|
||||
preferencesTabFooter?.classList.add('hidden');
|
||||
resetShortcutsBtn?.classList.remove('hidden');
|
||||
});
|
||||
|
||||
preferencesTabBtn.addEventListener('click', () => {
|
||||
preferencesTabBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
preferencesTabBtn.classList.remove('text-gray-300');
|
||||
shortcutsTabBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
shortcutsTabBtn.classList.add('text-gray-300');
|
||||
preferencesTabContent?.classList.remove('hidden');
|
||||
shortcutsTabContent?.classList.add('hidden');
|
||||
preferencesTabFooter?.classList.remove('hidden');
|
||||
shortcutsTabFooter?.classList.add('hidden');
|
||||
resetShortcutsBtn?.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// Full-width toggle functionality
|
||||
const fullWidthToggle = document.getElementById('full-width-toggle') as HTMLInputElement;
|
||||
const toolInterface = document.getElementById('tool-interface');
|
||||
|
||||
// Load saved preference
|
||||
const savedFullWidth = localStorage.getItem('fullWidthMode') === 'true';
|
||||
if (fullWidthToggle) {
|
||||
fullWidthToggle.checked = savedFullWidth;
|
||||
applyFullWidthMode(savedFullWidth);
|
||||
}
|
||||
|
||||
function applyFullWidthMode(enabled: boolean) {
|
||||
if (toolInterface) {
|
||||
if (enabled) {
|
||||
toolInterface.classList.remove('max-w-4xl');
|
||||
} else {
|
||||
toolInterface.classList.add('max-w-4xl');
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to all page uploaders
|
||||
const pageUploaders = document.querySelectorAll('#tool-uploader');
|
||||
pageUploaders.forEach((uploader) => {
|
||||
if (enabled) {
|
||||
uploader.classList.remove('max-w-2xl', 'max-w-5xl');
|
||||
} else {
|
||||
// Restore original max-width (most are max-w-2xl, add-stamps is max-w-5xl)
|
||||
if (!uploader.classList.contains('max-w-2xl') && !uploader.classList.contains('max-w-5xl')) {
|
||||
uploader.classList.add('max-w-2xl');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (fullWidthToggle) {
|
||||
fullWidthToggle.addEventListener('change', (e) => {
|
||||
const enabled = (e.target as HTMLInputElement).checked;
|
||||
localStorage.setItem('fullWidthMode', enabled.toString());
|
||||
applyFullWidthMode(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
// Shortcuts UI Handlers
|
||||
if (dom.openShortcutsBtn) {
|
||||
dom.openShortcutsBtn.addEventListener('click', () => {
|
||||
@@ -705,6 +780,31 @@ const init = () => {
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
const scrollToTopBtn = document.getElementById('scroll-to-top-btn');
|
||||
|
||||
if (scrollToTopBtn) {
|
||||
let lastScrollY = window.scrollY;
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
const currentScrollY = window.scrollY;
|
||||
|
||||
if (currentScrollY < lastScrollY && currentScrollY > 300) {
|
||||
scrollToTopBtn.classList.add('visible');
|
||||
} else {
|
||||
scrollToTopBtn.classList.remove('visible');
|
||||
}
|
||||
|
||||
lastScrollY = currentScrollY;
|
||||
});
|
||||
|
||||
scrollToTopBtn.addEventListener('click', () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'instant'
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
81
src/js/ui.ts
81
src/js/ui.ts
@@ -1,10 +1,14 @@
|
||||
import { resetState } from './state.js';
|
||||
import { formatBytes } from './utils/helpers.js';
|
||||
import { formatBytes, getPDFDocument } from './utils/helpers.js';
|
||||
import { tesseractLanguages } from './config/tesseract-languages.js';
|
||||
import { renderPagesProgressively, cleanupLazyRendering } from './utils/render-utils.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import Sortable from 'sortablejs';
|
||||
import { getRotationState } from './handlers/fileHandler.js';
|
||||
import { getRotationState, updateRotationState } from './handlers/fileHandler.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
|
||||
// Centralizing DOM element selection
|
||||
export const dom = {
|
||||
@@ -133,8 +137,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
||||
showLoader('Rendering page previews...');
|
||||
|
||||
const pdfData = await pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
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) => {
|
||||
@@ -217,10 +220,13 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
||||
'.page-rotator-item'
|
||||
) as HTMLElement;
|
||||
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);
|
||||
});
|
||||
|
||||
controlsDiv.append(pageNumSpan, rotateBtn);
|
||||
@@ -630,8 +636,8 @@ export const toolTemplates = {
|
||||
<div class="mb-4">
|
||||
<label for="jpg-quality" class="block mb-2 text-sm font-medium text-gray-300">Image Quality</label>
|
||||
<div class="flex items-center gap-4">
|
||||
<input type="range" id="jpg-quality" min="0.1" max="1.0" step="0.1" value="0.9" class="flex-1">
|
||||
<span id="jpg-quality-value" class="text-white font-medium w-16 text-right">90%</span>
|
||||
<input type="range" id="jpg-quality" min="0.1" max="1.0" step="0.01" value="1.0" class="flex-1">
|
||||
<span id="jpg-quality-value" class="text-white font-medium w-16 text-right">100%</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-400">Higher quality = larger file size</p>
|
||||
</div>
|
||||
@@ -1314,15 +1320,27 @@ export const toolTemplates = {
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="dimensions-results" class="hidden mt-6">
|
||||
<div class="flex justify-end mb-4">
|
||||
<label for="units-select" class="text-sm font-medium text-gray-300 self-center mr-3">Display Units:</label>
|
||||
<select id="units-select" class="bg-gray-700 border border-gray-600 text-white rounded-lg p-2">
|
||||
<option value="pt" selected>Points (pt)</option>
|
||||
<option value="in">Inches (in)</option>
|
||||
<option value="mm">Millimeters (mm)</option>
|
||||
<option value="px">Pixels (at 96 DPI)</option>
|
||||
</select>
|
||||
<!-- Summary Statistics Panel -->
|
||||
<div id="dimensions-summary" class="mb-6"></div>
|
||||
|
||||
<!-- Controls Row -->
|
||||
<div class="flex flex-wrap justify-between items-center gap-4 mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<label for="units-select" class="text-sm font-medium text-gray-300">Display Units:</label>
|
||||
<select id="units-select" class="bg-gray-700 border border-gray-600 text-white rounded-lg p-2">
|
||||
<option value="pt" selected>Points (pt)</option>
|
||||
<option value="in">Inches (in)</option>
|
||||
<option value="mm">Millimeters (mm)</option>
|
||||
<option value="px">Pixels (at 96 DPI)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="export-csv-btn" class="btn bg-indigo-600 hover:bg-indigo-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i data-lucide="download" class="w-4 h-4"></i>
|
||||
Export to CSV
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Dimensions Table -->
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-700">
|
||||
<table class="min-w-full divide-y divide-gray-700 text-sm text-left">
|
||||
<thead class="bg-gray-900">
|
||||
@@ -1331,6 +1349,9 @@ export const toolTemplates = {
|
||||
<th class="px-4 py-3 font-medium text-white">Dimensions (W x H)</th>
|
||||
<th class="px-4 py-3 font-medium text-white">Standard Size</th>
|
||||
<th class="px-4 py-3 font-medium text-white">Orientation</th>
|
||||
<th class="px-4 py-3 font-medium text-white">Aspect Ratio</th>
|
||||
<th class="px-4 py-3 font-medium text-white">Area</th>
|
||||
<th class="px-4 py-3 font-medium text-white">Rotation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dimensions-table-body" class="divide-y divide-gray-700">
|
||||
@@ -1340,6 +1361,7 @@ export const toolTemplates = {
|
||||
</div>
|
||||
`,
|
||||
|
||||
|
||||
'n-up': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">N-Up Page Arrangement</h2>
|
||||
<p class="mb-6 text-gray-400">Combine multiple pages from your PDF onto a single sheet. This is great for creating booklets or proof sheets.</p>
|
||||
@@ -1418,11 +1440,19 @@ export const toolTemplates = {
|
||||
|
||||
'combine-single-page': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Combine to a Single Page</h2>
|
||||
<p class="mb-6 text-gray-400">Stitch all pages of your PDF together vertically to create one continuous, scrollable page.</p>
|
||||
<p class="mb-6 text-gray-400">Stitch all pages of your PDF together vertically or horizontally to create one continuous page.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="combine-options" class="hidden mt-6 space-y-4">
|
||||
<div>
|
||||
<label for="combine-orientation" class="block mb-2 text-sm font-medium text-gray-300">Orientation</label>
|
||||
<select id="combine-orientation" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
|
||||
<option value="vertical" selected>Vertical (Stack pages top to bottom)</option>
|
||||
<option value="horizontal">Horizontal (Stack pages left to right)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="page-spacing" class="block mb-2 text-sm font-medium text-gray-300">Spacing Between Pages (in points)</label>
|
||||
@@ -1433,12 +1463,25 @@ export const toolTemplates = {
|
||||
<input type="color" id="background-color" value="#FFFFFF" class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="flex items-center gap-2 text-sm font-medium text-gray-300">
|
||||
<input type="checkbox" id="add-separator" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
|
||||
Draw a separator line between pages
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="separator-options" class="hidden grid grid-cols-1 sm:grid-cols-2 gap-4 p-4 rounded-lg bg-gray-900 border border-gray-700">
|
||||
<div>
|
||||
<label for="separator-thickness" class="block mb-2 text-sm font-medium text-gray-300">Separator Line Thickness (in points)</label>
|
||||
<input type="number" id="separator-thickness" value="0.5" min="0.1" max="10" step="0.1" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
|
||||
</div>
|
||||
<div>
|
||||
<label for="separator-color" class="block mb-2 text-sm font-medium text-gray-300">Separator Line Color</label>
|
||||
<input type="color" id="separator-color" value="#CCCCCC" class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Combine Pages</button>
|
||||
</div>
|
||||
`,
|
||||
@@ -1747,6 +1790,14 @@ export const toolTemplates = {
|
||||
<div id="canvas-container-sign" class="relative w-full overflow-auto bg-gray-900 rounded-lg border border-gray-600" style="height: 85vh;">
|
||||
<!-- PDF.js viewer iframe will be loaded here -->
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<label class="flex items-center gap-2 text-sm font-medium text-gray-300 cursor-pointer">
|
||||
<input type="checkbox" id="flatten-signature-toggle" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
|
||||
Flatten PDF (use the Save button below)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button id="process-btn" class="btn-gradient w-full mt-4" style="display:none;">Save & Download Signed PDF</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
34
src/js/utils/full-width.ts
Normal file
34
src/js/utils/full-width.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// Full-width mode utility
|
||||
// This script applies the full-width preference from localStorage to page uploaders
|
||||
|
||||
export function initFullWidthMode() {
|
||||
const savedFullWidth = localStorage.getItem('fullWidthMode') === 'true';
|
||||
|
||||
if (savedFullWidth) {
|
||||
applyFullWidthMode(true);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyFullWidthMode(enabled: boolean) {
|
||||
// Apply to all page uploaders
|
||||
const pageUploaders = document.querySelectorAll('#tool-uploader');
|
||||
pageUploaders.forEach((uploader) => {
|
||||
if (enabled) {
|
||||
uploader.classList.remove('max-w-2xl', 'max-w-5xl');
|
||||
} else {
|
||||
// Restore original max-width if not already present
|
||||
if (!uploader.classList.contains('max-w-2xl') && !uploader.classList.contains('max-w-5xl')) {
|
||||
uploader.classList.add('max-w-2xl');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-initialize on DOM load
|
||||
if (typeof document !== 'undefined') {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initFullWidthMode);
|
||||
} else {
|
||||
initFullWidthMode();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import createModule from '@neslinesli93/qpdf-wasm';
|
||||
import { showLoader, hideLoader, showAlert } from '../ui';
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { createIcons } from 'lucide';
|
||||
import { state, resetState } from '../state.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist'
|
||||
|
||||
|
||||
const STANDARD_SIZES = {
|
||||
A4: { width: 595.28, height: 841.89 },
|
||||
@@ -45,16 +48,17 @@ export function convertPoints(points: any, unit: any) {
|
||||
return result.toFixed(2);
|
||||
}
|
||||
|
||||
export const hexToRgb = (hex: any) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
// Convert hex color to RGB
|
||||
export function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16) / 255,
|
||||
g: parseInt(result[2], 16) / 255,
|
||||
b: parseInt(result[3], 16) / 255,
|
||||
}
|
||||
: { r: 0, g: 0, b: 0 }; // Default to black
|
||||
};
|
||||
: { r: 0, g: 0, b: 0 }
|
||||
}
|
||||
|
||||
export const formatBytes = (bytes: any, decimals = 1) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
@@ -195,14 +199,86 @@ export function formatStars(num: number) {
|
||||
return num.toLocaleString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncates a filename to a maximum length, adding ellipsis if needed.
|
||||
* Preserves the file extension.
|
||||
* @param filename - The filename to truncate
|
||||
* @param maxLength - Maximum length (default: 30)
|
||||
* @returns Truncated filename with ellipsis if needed
|
||||
*/
|
||||
export function truncateFilename(filename: string, maxLength: number = 25): string {
|
||||
if (filename.length <= maxLength) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
const lastDotIndex = filename.lastIndexOf('.');
|
||||
const extension = lastDotIndex !== -1 ? filename.substring(lastDotIndex) : '';
|
||||
const nameWithoutExt = lastDotIndex !== -1 ? filename.substring(0, lastDotIndex) : filename;
|
||||
|
||||
const availableLength = maxLength - extension.length - 3; // 3 for '...'
|
||||
|
||||
if (availableLength <= 0) {
|
||||
return filename.substring(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
return nameWithoutExt.substring(0, availableLength) + '...' + extension;
|
||||
}
|
||||
|
||||
export function formatShortcutDisplay(shortcut: string, isMac: boolean): string {
|
||||
if (!shortcut) return '';
|
||||
return shortcut
|
||||
.replace('mod', isMac ? '⌘' : 'Ctrl')
|
||||
.replace('ctrl', isMac ? '^' : 'Ctrl') // Control key on Mac shows as ^
|
||||
.replace('alt', isMac ? '⌥' : 'Alt')
|
||||
.replace('shift', 'Shift')
|
||||
.split('+')
|
||||
.map(k => k.charAt(0).toUpperCase() + k.slice(1))
|
||||
.join(isMac ? '' : '+');
|
||||
}
|
||||
if (!shortcut) return '';
|
||||
return shortcut
|
||||
.replace('mod', isMac ? '⌘' : 'Ctrl')
|
||||
.replace('ctrl', isMac ? '^' : 'Ctrl') // Control key on Mac shows as ^
|
||||
.replace('alt', isMac ? '⌥' : 'Alt')
|
||||
.replace('shift', 'Shift')
|
||||
.split('+')
|
||||
.map(k => k.charAt(0).toUpperCase() + k.slice(1))
|
||||
.join(isMac ? '' : '+');
|
||||
}
|
||||
|
||||
export function resetAndReloadTool(preResetCallback?: () => void) {
|
||||
const toolid = state.activeTool;
|
||||
|
||||
if (preResetCallback) {
|
||||
preResetCallback();
|
||||
}
|
||||
|
||||
resetState();
|
||||
|
||||
if (toolid) {
|
||||
const element = document.querySelector(
|
||||
`[data-tool-id="${toolid}"]`
|
||||
) as HTMLElement;
|
||||
if (element) element.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for pdfjsLib.getDocument that adds the required wasmUrl configuration.
|
||||
* Use this instead of calling pdfjsLib.getDocument directly.
|
||||
* @param src The source to load (url string, typed array, or parameters object)
|
||||
* @returns The PDF loading task
|
||||
*/
|
||||
export function getPDFDocument(src: any) {
|
||||
let params = src;
|
||||
|
||||
// Handle different input types similar to how getDocument handles them,
|
||||
// but we ensure we have an object to attach wasmUrl to.
|
||||
if (typeof src === 'string') {
|
||||
params = { url: src };
|
||||
} else if (src instanceof Uint8Array || src instanceof ArrayBuffer) {
|
||||
params = { data: src };
|
||||
}
|
||||
|
||||
// Ensure params is an object
|
||||
if (typeof params !== 'object' || params === null) {
|
||||
params = {};
|
||||
}
|
||||
|
||||
// Add wasmUrl pointing to our public/wasm directory
|
||||
// This is required for PDF.js v5+ to load OpenJPEG for certain images
|
||||
return pdfjsLib.getDocument({
|
||||
...params,
|
||||
wasmUrl: '/pdfjs-viewer/wasm/',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
/**
|
||||
* Configuration for progressive rendering
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user