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 { 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user