Merge pull request #405 from LoganK/crop_restore
(Fix) Restore crop bounds when navigating
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
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 * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
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 = {
|
||||
pdfDoc: null,
|
||||
@@ -32,8 +40,13 @@ function initializePage() {
|
||||
if (fileInput) fileInput.addEventListener('change', handleFileUpload);
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
|
||||
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('bg-gray-700'); });
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
@@ -50,9 +63,15 @@ function initializePage() {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
|
||||
document.getElementById('prev-page')?.addEventListener('click', () => changePage(-1));
|
||||
document.getElementById('next-page')?.addEventListener('click', () => changePage(1));
|
||||
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) {
|
||||
@@ -61,7 +80,10 @@ function handleFileUpload(e: Event) {
|
||||
}
|
||||
|
||||
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.');
|
||||
return;
|
||||
}
|
||||
@@ -73,7 +95,9 @@ async function handleFile(file: File) {
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
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;
|
||||
|
||||
updateFileDisplay();
|
||||
@@ -92,7 +116,8 @@ function updateFileDisplay() {
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
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');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
@@ -200,7 +225,8 @@ async function changePage(offset: number) {
|
||||
|
||||
function updatePageInfo() {
|
||||
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() {
|
||||
@@ -209,15 +235,21 @@ function enableControls() {
|
||||
const cropBtn = document.getElementById('crop-button') as HTMLButtonElement;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function performCrop() {
|
||||
saveCurrentCrop();
|
||||
|
||||
const isDestructive = (document.getElementById('destructive-crop-toggle') as HTMLInputElement)?.checked;
|
||||
const isApplyToAll = (document.getElementById('apply-to-all-toggle') as HTMLInputElement)?.checked;
|
||||
const isDestructive = (
|
||||
document.getElementById('destructive-crop-toggle') as HTMLInputElement
|
||||
)?.checked;
|
||||
const isApplyToAll = (
|
||||
document.getElementById('apply-to-all-toggle') as HTMLInputElement
|
||||
)?.checked;
|
||||
|
||||
let finalCropData: Record<number, any> = {};
|
||||
|
||||
@@ -235,7 +267,10 @@ async function performCrop() {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -250,8 +285,16 @@ async function performCrop() {
|
||||
}
|
||||
|
||||
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.', 'success', () => resetState());
|
||||
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.');
|
||||
@@ -260,8 +303,13 @@ async function performCrop() {
|
||||
}
|
||||
}
|
||||
|
||||
async function performMetadataCrop(cropData: Record<number, any>): Promise<Uint8Array> {
|
||||
const pdfToModify = await PDFLibDocument.load(cropperState.originalPdfBytes!, { ignoreEncryption: true, throwOnInvalidObject: false });
|
||||
async function performMetadataCrop(
|
||||
cropData: Record<number, any>
|
||||
): Promise<Uint8Array> {
|
||||
const pdfToModify = await PDFLibDocument.load(
|
||||
cropperState.originalPdfBytes!,
|
||||
{ ignoreEncryption: true, throwOnInvalidObject: false }
|
||||
);
|
||||
|
||||
for (const pageNum in cropData) {
|
||||
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 },
|
||||
];
|
||||
|
||||
const pdfCorners = visualCorners.map(p => viewport.convertToPdfPoint(p.x, p.y));
|
||||
const pdfXs = pdfCorners.map(p => p[0]);
|
||||
const pdfYs = pdfCorners.map(p => p[1]);
|
||||
const pdfCorners = visualCorners.map((p) =>
|
||||
viewport.convertToPdfPoint(p.x, p.y)
|
||||
);
|
||||
const pdfXs = pdfCorners.map((p) => p[0]);
|
||||
const pdfYs = pdfCorners.map((p) => p[1]);
|
||||
|
||||
const minX = Math.min(...pdfXs);
|
||||
const maxX = Math.max(...pdfXs);
|
||||
@@ -296,9 +346,14 @@ async function performMetadataCrop(cropData: Record<number, any>): Promise<Uint8
|
||||
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 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;
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
@@ -329,17 +384,31 @@ async function performFlatteningCrop(cropData: Record<number, any>): Promise<Uin
|
||||
tempCanvas.height * crop.y,
|
||||
finalWidth,
|
||||
finalHeight,
|
||||
0, 0, finalWidth, finalHeight
|
||||
0,
|
||||
0,
|
||||
finalWidth,
|
||||
finalHeight
|
||||
);
|
||||
|
||||
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 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 {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [i]);
|
||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [
|
||||
i,
|
||||
]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user