Merge pull request #405 from LoganK/crop_restore

(Fix) Restore crop bounds when navigating
This commit is contained in:
Alam
2026-01-27 13:54:01 +05:30
committed by GitHub

View File

@@ -1,12 +1,20 @@
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js'; import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
getPDFDocument,
} from '../utils/helpers.js';
import Cropper from 'cropperjs'; import Cropper from 'cropperjs';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { CropperState } from '@/types'; import { CropperState } from '@/types';
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 cropperState: CropperState = { const cropperState: CropperState = {
pdfDoc: null, pdfDoc: null,
@@ -32,8 +40,13 @@ function initializePage() {
if (fileInput) fileInput.addEventListener('change', handleFileUpload); if (fileInput) fileInput.addEventListener('change', handleFileUpload);
if (dropZone) { if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); }); dropZone.addEventListener('dragover', (e) => {
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('bg-gray-700'); }); e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => { dropZone.addEventListener('drop', (e) => {
e.preventDefault(); e.preventDefault();
dropZone.classList.remove('bg-gray-700'); dropZone.classList.remove('bg-gray-700');
@@ -50,9 +63,15 @@ function initializePage() {
window.location.href = import.meta.env.BASE_URL; window.location.href = import.meta.env.BASE_URL;
}); });
document.getElementById('prev-page')?.addEventListener('click', () => changePage(-1)); document
document.getElementById('next-page')?.addEventListener('click', () => changePage(1)); .getElementById('prev-page')
document.getElementById('crop-button')?.addEventListener('click', performCrop); ?.addEventListener('click', () => changePage(-1));
document
.getElementById('next-page')
?.addEventListener('click', () => changePage(1));
document
.getElementById('crop-button')
?.addEventListener('click', performCrop);
} }
function handleFileUpload(e: Event) { function handleFileUpload(e: Event) {
@@ -61,7 +80,10 @@ function handleFileUpload(e: Event) {
} }
async function handleFile(file: File) { async function handleFile(file: File) {
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { if (
file.type !== 'application/pdf' &&
!file.name.toLowerCase().endsWith('.pdf')
) {
showAlert('Invalid File', 'Please select a PDF file.'); showAlert('Invalid File', 'Please select a PDF file.');
return; return;
} }
@@ -73,7 +95,9 @@ async function handleFile(file: File) {
try { try {
const arrayBuffer = await readFileAsArrayBuffer(file); const arrayBuffer = await readFileAsArrayBuffer(file);
cropperState.originalPdfBytes = arrayBuffer as ArrayBuffer; cropperState.originalPdfBytes = arrayBuffer as ArrayBuffer;
cropperState.pdfDoc = await getPDFDocument({ data: (arrayBuffer as ArrayBuffer).slice(0) }).promise; cropperState.pdfDoc = await getPDFDocument({
data: (arrayBuffer as ArrayBuffer).slice(0),
}).promise;
cropperState.currentPageNum = 1; cropperState.currentPageNum = 1;
updateFileDisplay(); updateFileDisplay();
@@ -92,7 +116,8 @@ function updateFileDisplay() {
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0'; infoContainer.className = 'flex flex-col flex-1 min-w-0';
@@ -200,7 +225,8 @@ async function changePage(offset: number) {
function updatePageInfo() { function updatePageInfo() {
const pageInfo = document.getElementById('page-info'); const pageInfo = document.getElementById('page-info');
if (pageInfo) pageInfo.textContent = `Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`; if (pageInfo)
pageInfo.textContent = `Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
} }
function enableControls() { function enableControls() {
@@ -209,15 +235,21 @@ function enableControls() {
const cropBtn = document.getElementById('crop-button') as HTMLButtonElement; const cropBtn = document.getElementById('crop-button') as HTMLButtonElement;
if (prevBtn) prevBtn.disabled = cropperState.currentPageNum <= 1; if (prevBtn) prevBtn.disabled = cropperState.currentPageNum <= 1;
if (nextBtn) nextBtn.disabled = cropperState.currentPageNum >= cropperState.pdfDoc.numPages; if (nextBtn)
nextBtn.disabled =
cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
if (cropBtn) cropBtn.disabled = false; if (cropBtn) cropBtn.disabled = false;
} }
async function performCrop() { async function performCrop() {
saveCurrentCrop(); saveCurrentCrop();
const isDestructive = (document.getElementById('destructive-crop-toggle') as HTMLInputElement)?.checked; const isDestructive = (
const isApplyToAll = (document.getElementById('apply-to-all-toggle') as HTMLInputElement)?.checked; document.getElementById('destructive-crop-toggle') as HTMLInputElement
)?.checked;
const isApplyToAll = (
document.getElementById('apply-to-all-toggle') as HTMLInputElement
)?.checked;
let finalCropData: Record<number, any> = {}; let finalCropData: Record<number, any> = {};
@@ -235,7 +267,10 @@ async function performCrop() {
} }
if (Object.keys(finalCropData).length === 0) { if (Object.keys(finalCropData).length === 0) {
showAlert('No Crop Area', 'Please select an area on at least one page to crop.'); showAlert(
'No Crop Area',
'Please select an area on at least one page to crop.'
);
return; return;
} }
@@ -250,8 +285,16 @@ async function performCrop() {
} }
const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf'; const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf';
downloadFile(new Blob([finalPdfBytes], { type: 'application/pdf' }), fileName); downloadFile(
showAlert('Success', 'Crop complete! Your download has started.', 'success', () => resetState()); new Blob([finalPdfBytes], { type: 'application/pdf' }),
fileName
);
showAlert(
'Success',
'Crop complete! Your download has started.',
'success',
() => resetState()
);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', 'An error occurred during cropping.'); showAlert('Error', 'An error occurred during cropping.');
@@ -260,8 +303,13 @@ async function performCrop() {
} }
} }
async function performMetadataCrop(cropData: Record<number, any>): Promise<Uint8Array> { async function performMetadataCrop(
const pdfToModify = await PDFLibDocument.load(cropperState.originalPdfBytes!, { ignoreEncryption: true, throwOnInvalidObject: false }); cropData: Record<number, any>
): Promise<Uint8Array> {
const pdfToModify = await PDFLibDocument.load(
cropperState.originalPdfBytes!,
{ ignoreEncryption: true, throwOnInvalidObject: false }
);
for (const pageNum in cropData) { for (const pageNum in cropData) {
const pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum)); const pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum));
@@ -280,9 +328,11 @@ async function performMetadataCrop(cropData: Record<number, any>): Promise<Uint8
{ x: cropX, y: cropY + cropH }, { x: cropX, y: cropY + cropH },
]; ];
const pdfCorners = visualCorners.map(p => viewport.convertToPdfPoint(p.x, p.y)); const pdfCorners = visualCorners.map((p) =>
const pdfXs = pdfCorners.map(p => p[0]); viewport.convertToPdfPoint(p.x, p.y)
const pdfYs = pdfCorners.map(p => p[1]); );
const pdfXs = pdfCorners.map((p) => p[0]);
const pdfYs = pdfCorners.map((p) => p[1]);
const minX = Math.min(...pdfXs); const minX = Math.min(...pdfXs);
const maxX = Math.max(...pdfXs); const maxX = Math.max(...pdfXs);
@@ -296,9 +346,14 @@ async function performMetadataCrop(cropData: Record<number, any>): Promise<Uint8
return pdfToModify.save(); return pdfToModify.save();
} }
async function performFlatteningCrop(cropData: Record<number, any>): Promise<Uint8Array> { async function performFlatteningCrop(
cropData: Record<number, any>
): Promise<Uint8Array> {
const newPdfDoc = await PDFLibDocument.create(); const newPdfDoc = await PDFLibDocument.create();
const sourcePdfDocForCopying = await PDFLibDocument.load(cropperState.originalPdfBytes!, { ignoreEncryption: true, throwOnInvalidObject: false }); const sourcePdfDocForCopying = await PDFLibDocument.load(
cropperState.originalPdfBytes!,
{ ignoreEncryption: true, throwOnInvalidObject: false }
);
const totalPages = cropperState.pdfDoc.numPages; const totalPages = cropperState.pdfDoc.numPages;
for (let i = 0; i < totalPages; i++) { for (let i = 0; i < totalPages; i++) {
@@ -329,17 +384,31 @@ async function performFlatteningCrop(cropData: Record<number, any>): Promise<Uin
tempCanvas.height * crop.y, tempCanvas.height * crop.y,
finalWidth, finalWidth,
finalHeight, finalHeight,
0, 0, finalWidth, finalHeight 0,
0,
finalWidth,
finalHeight
); );
const pngBytes = await new Promise<ArrayBuffer>((res) => const pngBytes = await new Promise<ArrayBuffer>((res) =>
finalCanvas.toBlob((blob) => blob?.arrayBuffer().then(res), 'image/jpeg', 0.9) finalCanvas.toBlob(
(blob) => blob?.arrayBuffer().then(res),
'image/jpeg',
0.9
)
); );
const embeddedImage = await newPdfDoc.embedPng(pngBytes); const embeddedImage = await newPdfDoc.embedPng(pngBytes);
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]); const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
newPage.drawImage(embeddedImage, { x: 0, y: 0, width: finalWidth, height: finalHeight }); newPage.drawImage(embeddedImage, {
x: 0,
y: 0,
width: finalWidth,
height: finalHeight,
});
} else { } else {
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [i]); const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [
i,
]);
newPdfDoc.addPage(copiedPage); newPdfDoc.addPage(copiedPage);
} }
} }