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,373 +1,442 @@
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,
currentPageNum: 1, currentPageNum: 1,
cropper: null, cropper: null,
originalPdfBytes: null, originalPdfBytes: null,
pageCrops: {}, pageCrops: {},
file: null, file: null,
}; };
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage); document.addEventListener('DOMContentLoaded', initializePage);
} else { } else {
initializePage(); initializePage();
} }
function initializePage() { function initializePage() {
createIcons({ icons }); createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement; const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone'); const dropZone = document.getElementById('drop-zone');
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.addEventListener('drop', (e) => { dropZone.classList.add('bg-gray-700');
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) handleFile(droppedFiles[0]);
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
}); });
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) handleFile(droppedFiles[0]);
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
document.getElementById('prev-page')?.addEventListener('click', () => changePage(-1)); document.getElementById('back-to-tools')?.addEventListener('click', () => {
document.getElementById('next-page')?.addEventListener('click', () => changePage(1)); window.location.href = import.meta.env.BASE_URL;
document.getElementById('crop-button')?.addEventListener('click', performCrop); });
document
.getElementById('prev-page')
?.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) {
const input = e.target as HTMLInputElement; const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) handleFile(input.files[0]); if (input.files && input.files.length > 0) handleFile(input.files[0]);
} }
async function handleFile(file: File) { async function handleFile(file: File) {
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { if (
showAlert('Invalid File', 'Please select a PDF file.'); file.type !== 'application/pdf' &&
return; !file.name.toLowerCase().endsWith('.pdf')
} ) {
showAlert('Invalid File', 'Please select a PDF file.');
return;
}
showLoader('Loading PDF...'); showLoader('Loading PDF...');
cropperState.file = file; cropperState.file = file;
cropperState.pageCrops = {}; cropperState.pageCrops = {};
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({
cropperState.currentPageNum = 1; data: (arrayBuffer as ArrayBuffer).slice(0),
}).promise;
cropperState.currentPageNum = 1;
updateFileDisplay(); updateFileDisplay();
await displayPageAsImage(cropperState.currentPageNum); await displayPageAsImage(cropperState.currentPageNum);
hideLoader(); hideLoader();
} catch (error) { } catch (error) {
console.error('Error loading PDF:', error); console.error('Error loading PDF:', error);
hideLoader(); hideLoader();
showAlert('Error', 'Failed to load PDF file.'); showAlert('Error', 'Failed to load PDF file.');
} }
} }
function updateFileDisplay() { function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !cropperState.file) return; if (!fileDisplayArea || !cropperState.file) return;
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';
const nameSpan = document.createElement('div'); const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = cropperState.file.name; nameSpan.textContent = cropperState.file.name;
const metaSpan = document.createElement('div'); const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400'; metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(cropperState.file.size)}${cropperState.pdfDoc?.numPages || 0} pages`; metaSpan.textContent = `${formatBytes(cropperState.file.size)}${cropperState.pdfDoc?.numPages || 0} pages`;
infoContainer.append(nameSpan, metaSpan); infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button'); const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>'; removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => resetState(); removeBtn.onclick = () => resetState();
fileDiv.append(infoContainer, removeBtn); fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv); fileDisplayArea.appendChild(fileDiv);
createIcons({ icons }); createIcons({ icons });
} }
function saveCurrentCrop() { function saveCurrentCrop() {
if (cropperState.cropper) { if (cropperState.cropper) {
const currentCrop = cropperState.cropper.getData(true); const currentCrop = cropperState.cropper.getData(true);
const imageData = cropperState.cropper.getImageData(); const imageData = cropperState.cropper.getImageData();
const cropPercentages = { const cropPercentages = {
x: currentCrop.x / imageData.naturalWidth, x: currentCrop.x / imageData.naturalWidth,
y: currentCrop.y / imageData.naturalHeight, y: currentCrop.y / imageData.naturalHeight,
width: currentCrop.width / imageData.naturalWidth, width: currentCrop.width / imageData.naturalWidth,
height: currentCrop.height / imageData.naturalHeight, height: currentCrop.height / imageData.naturalHeight,
}; };
cropperState.pageCrops[cropperState.currentPageNum] = cropPercentages; cropperState.pageCrops[cropperState.currentPageNum] = cropPercentages;
} }
} }
async function displayPageAsImage(num: number) { async function displayPageAsImage(num: number) {
showLoader(`Rendering Page ${num}...`); showLoader(`Rendering Page ${num}...`);
try { try {
const page = await cropperState.pdfDoc.getPage(num); const page = await cropperState.pdfDoc.getPage(num);
const viewport = page.getViewport({ scale: 2.5 }); const viewport = page.getViewport({ scale: 2.5 });
const tempCanvas = document.createElement('canvas'); const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d'); const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width; tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height; tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise; await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
if (cropperState.cropper) cropperState.cropper.destroy(); if (cropperState.cropper) cropperState.cropper.destroy();
const cropperEditor = document.getElementById('cropper-editor'); const cropperEditor = document.getElementById('cropper-editor');
if (cropperEditor) cropperEditor.classList.remove('hidden'); if (cropperEditor) cropperEditor.classList.remove('hidden');
const container = document.getElementById('cropper-container'); const container = document.getElementById('cropper-container');
if (!container) return; if (!container) return;
container.innerHTML = ''; container.innerHTML = '';
const image = document.createElement('img'); const image = document.createElement('img');
image.src = tempCanvas.toDataURL('image/png'); image.src = tempCanvas.toDataURL('image/png');
container.appendChild(image); container.appendChild(image);
image.onload = () => { image.onload = () => {
cropperState.cropper = new Cropper(image, { cropperState.cropper = new Cropper(image, {
viewMode: 1, viewMode: 1,
background: false, background: false,
autoCropArea: 0.8, autoCropArea: 0.8,
responsive: true, responsive: true,
rotatable: false, rotatable: false,
zoomable: false, zoomable: false,
}); });
const savedCrop = cropperState.pageCrops[num]; const savedCrop = cropperState.pageCrops[num];
if (savedCrop) { if (savedCrop) {
const imageData = cropperState.cropper.getImageData(); const imageData = cropperState.cropper.getImageData();
cropperState.cropper.setData({ cropperState.cropper.setData({
x: savedCrop.x * imageData.naturalWidth, x: savedCrop.x * imageData.naturalWidth,
y: savedCrop.y * imageData.naturalHeight, y: savedCrop.y * imageData.naturalHeight,
width: savedCrop.width * imageData.naturalWidth, width: savedCrop.width * imageData.naturalWidth,
height: savedCrop.height * imageData.naturalHeight, height: savedCrop.height * imageData.naturalHeight,
}); });
} }
updatePageInfo(); updatePageInfo();
enableControls(); enableControls();
hideLoader(); hideLoader();
}; };
} catch (error) { } catch (error) {
console.error('Error rendering page:', error); console.error('Error rendering page:', error);
showAlert('Error', 'Failed to render page.'); showAlert('Error', 'Failed to render page.');
hideLoader(); hideLoader();
} }
} }
async function changePage(offset: number) { async function changePage(offset: number) {
saveCurrentCrop(); saveCurrentCrop();
const newPageNum = cropperState.currentPageNum + offset; const newPageNum = cropperState.currentPageNum + offset;
if (newPageNum > 0 && newPageNum <= cropperState.pdfDoc.numPages) { if (newPageNum > 0 && newPageNum <= cropperState.pdfDoc.numPages) {
cropperState.currentPageNum = newPageNum; cropperState.currentPageNum = newPageNum;
await displayPageAsImage(cropperState.currentPageNum); await displayPageAsImage(cropperState.currentPageNum);
} }
} }
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() {
const prevBtn = document.getElementById('prev-page') as HTMLButtonElement; const prevBtn = document.getElementById('prev-page') as HTMLButtonElement;
const nextBtn = document.getElementById('next-page') as HTMLButtonElement; const nextBtn = document.getElementById('next-page') as HTMLButtonElement;
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)
if (cropBtn) cropBtn.disabled = false; nextBtn.disabled =
cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
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> = {};
if (isApplyToAll) { if (isApplyToAll) {
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum]; const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
if (!currentCrop) { if (!currentCrop) {
showAlert('No Crop Area', 'Please select an area to crop first.'); showAlert('No Crop Area', 'Please select an area to crop first.');
return; return;
} }
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) { for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
finalCropData[i] = currentCrop; finalCropData[i] = currentCrop;
} }
} else {
finalCropData = { ...cropperState.pageCrops };
}
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) {
finalPdfBytes = await performFlatteningCrop(finalCropData);
} else { } else {
finalCropData = { ...cropperState.pageCrops }; finalPdfBytes = await performMetadataCrop(finalCropData);
} }
if (Object.keys(finalCropData).length === 0) { const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf';
showAlert('No Crop Area', 'Please select an area on at least one page to crop.'); downloadFile(
return; new Blob([finalPdfBytes], { type: 'application/pdf' }),
} fileName
);
showLoader('Applying crop...'); showAlert(
'Success',
try { 'Crop complete! Your download has started.',
let finalPdfBytes; 'success',
if (isDestructive) { () => resetState()
finalPdfBytes = await performFlatteningCrop(finalCropData); );
} else { } catch (e) {
finalPdfBytes = await performMetadataCrop(finalCropData); console.error(e);
} showAlert('Error', 'An error occurred during cropping.');
} finally {
const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf'; hideLoader();
downloadFile(new Blob([finalPdfBytes], { type: 'application/pdf' }), fileName); }
showAlert('Success', 'Crop complete! Your download has started.', 'success', () => resetState());
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred during cropping.');
} finally {
hideLoader();
}
} }
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));
const viewport = pdfJsPage.getViewport({ scale: 1 }); const viewport = pdfJsPage.getViewport({ scale: 1 });
const crop = cropData[pageNum]; const crop = cropData[pageNum];
const cropX = viewport.width * crop.x; const cropX = viewport.width * crop.x;
const cropY = viewport.height * crop.y; const cropY = viewport.height * crop.y;
const cropW = viewport.width * crop.width; const cropW = viewport.width * crop.width;
const cropH = viewport.height * crop.height; const cropH = viewport.height * crop.height;
const visualCorners = [ const visualCorners = [
{ x: cropX, y: cropY }, { x: cropX, y: cropY },
{ x: cropX + cropW, y: cropY }, { x: cropX + cropW, y: cropY },
{ x: cropX + cropW, y: cropY + cropH }, { x: cropX + cropW, y: cropY + cropH },
{ 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);
const minY = Math.min(...pdfYs); const minY = Math.min(...pdfYs);
const maxY = Math.max(...pdfYs); const maxY = Math.max(...pdfYs);
const page = pdfToModify.getPages()[Number(pageNum) - 1]; const page = pdfToModify.getPages()[Number(pageNum) - 1];
page.setCropBox(minX, minY, maxX - minX, maxY - minY); page.setCropBox(minX, minY, maxX - minX, maxY - minY);
} }
return pdfToModify.save(); return pdfToModify.save();
} }
async function performFlatteningCrop(cropData: Record<number, any>): Promise<Uint8Array> { async function performFlatteningCrop(
const newPdfDoc = await PDFLibDocument.create(); cropData: Record<number, any>
const sourcePdfDocForCopying = await PDFLibDocument.load(cropperState.originalPdfBytes!, { ignoreEncryption: true, throwOnInvalidObject: false }); ): Promise<Uint8Array> {
const totalPages = cropperState.pdfDoc.numPages; const newPdfDoc = await PDFLibDocument.create();
const sourcePdfDocForCopying = await PDFLibDocument.load(
cropperState.originalPdfBytes!,
{ ignoreEncryption: true, throwOnInvalidObject: false }
);
const totalPages = cropperState.pdfDoc.numPages;
for (let i = 0; i < totalPages; i++) { for (let i = 0; i < totalPages; i++) {
const pageNum = i + 1; const pageNum = i + 1;
showLoader(`Processing page ${pageNum} of ${totalPages}...`); showLoader(`Processing page ${pageNum} of ${totalPages}...`);
if (cropData[pageNum]) { if (cropData[pageNum]) {
const page = await cropperState.pdfDoc.getPage(pageNum); const page = await cropperState.pdfDoc.getPage(pageNum);
const viewport = page.getViewport({ scale: 2.5 }); const viewport = page.getViewport({ scale: 2.5 });
const tempCanvas = document.createElement('canvas'); const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d'); const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width; tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height; tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise; await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
const finalCanvas = document.createElement('canvas'); const finalCanvas = document.createElement('canvas');
const finalCtx = finalCanvas.getContext('2d'); const finalCtx = finalCanvas.getContext('2d');
const crop = cropData[pageNum]; const crop = cropData[pageNum];
const finalWidth = tempCanvas.width * crop.width; const finalWidth = tempCanvas.width * crop.width;
const finalHeight = tempCanvas.height * crop.height; const finalHeight = tempCanvas.height * crop.height;
finalCanvas.width = finalWidth; finalCanvas.width = finalWidth;
finalCanvas.height = finalHeight; finalCanvas.height = finalHeight;
finalCtx?.drawImage( finalCtx?.drawImage(
tempCanvas, tempCanvas,
tempCanvas.width * crop.x, tempCanvas.width * crop.x,
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),
const embeddedImage = await newPdfDoc.embedPng(pngBytes); 'image/jpeg',
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]); 0.9
newPage.drawImage(embeddedImage, { x: 0, y: 0, width: finalWidth, height: finalHeight }); )
} else { );
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [i]); const embeddedImage = await newPdfDoc.embedPng(pngBytes);
newPdfDoc.addPage(copiedPage); const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
} newPage.drawImage(embeddedImage, {
x: 0,
y: 0,
width: finalWidth,
height: finalHeight,
});
} else {
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [
i,
]);
newPdfDoc.addPage(copiedPage);
} }
}
return newPdfDoc.save(); return newPdfDoc.save();
} }
function resetState() { function resetState() {
if (cropperState.cropper) { if (cropperState.cropper) {
cropperState.cropper.destroy(); cropperState.cropper.destroy();
cropperState.cropper = null; cropperState.cropper = null;
} }
cropperState.pdfDoc = null; cropperState.pdfDoc = null;
cropperState.originalPdfBytes = null; cropperState.originalPdfBytes = null;
cropperState.pageCrops = {}; cropperState.pageCrops = {};
cropperState.currentPageNum = 1; cropperState.currentPageNum = 1;
cropperState.file = null; cropperState.file = null;
const cropperEditor = document.getElementById('cropper-editor'); const cropperEditor = document.getElementById('cropper-editor');
if (cropperEditor) cropperEditor.classList.add('hidden'); if (cropperEditor) cropperEditor.classList.add('hidden');
const container = document.getElementById('cropper-container'); const container = document.getElementById('cropper-container');
if (container) container.innerHTML = ''; if (container) container.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = ''; if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const cropBtn = document.getElementById('crop-button') as HTMLButtonElement; const cropBtn = document.getElementById('crop-button') as HTMLButtonElement;
if (cropBtn) cropBtn.disabled = true; if (cropBtn) cropBtn.disabled = true;
} }