401 lines
15 KiB
TypeScript
401 lines
15 KiB
TypeScript
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|
import { downloadFile, parsePageRanges, getPDFDocument, formatBytes } from '../utils/helpers.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();
|
|
|
|
interface PosterizeState {
|
|
file: File | null;
|
|
pdfJsDoc: pdfjsLib.PDFDocumentProxy | null;
|
|
pdfBytes: Uint8Array | null;
|
|
pageSnapshots: Record<number, ImageData>;
|
|
currentPage: number;
|
|
}
|
|
|
|
const pageState: PosterizeState = {
|
|
file: null,
|
|
pdfJsDoc: null,
|
|
pdfBytes: null,
|
|
pageSnapshots: {},
|
|
currentPage: 1,
|
|
};
|
|
|
|
function resetState() {
|
|
pageState.file = null;
|
|
pageState.pdfJsDoc = null;
|
|
pageState.pdfBytes = null;
|
|
pageState.pageSnapshots = {};
|
|
pageState.currentPage = 1;
|
|
|
|
const fileDisplayArea = document.getElementById('file-display-area');
|
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
|
|
|
const toolOptions = document.getElementById('tool-options');
|
|
if (toolOptions) toolOptions.classList.add('hidden');
|
|
|
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
|
if (fileInput) fileInput.value = '';
|
|
|
|
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
|
if (processBtn) processBtn.disabled = true;
|
|
|
|
const totalPages = document.getElementById('total-pages');
|
|
if (totalPages) totalPages.textContent = '0';
|
|
}
|
|
|
|
async function renderPosterizePreview(pageNum: number) {
|
|
if (!pageState.pdfJsDoc) return;
|
|
|
|
pageState.currentPage = pageNum;
|
|
showLoader(`Rendering preview for page ${pageNum}...`);
|
|
|
|
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
|
const context = canvas.getContext('2d');
|
|
|
|
if (!context) {
|
|
hideLoader();
|
|
return;
|
|
}
|
|
|
|
if (pageState.pageSnapshots[pageNum]) {
|
|
canvas.width = pageState.pageSnapshots[pageNum].width;
|
|
canvas.height = pageState.pageSnapshots[pageNum].height;
|
|
context.putImageData(pageState.pageSnapshots[pageNum], 0, 0);
|
|
} else {
|
|
const page = await pageState.pdfJsDoc.getPage(pageNum);
|
|
const viewport = page.getViewport({ scale: 1.5 });
|
|
canvas.width = viewport.width;
|
|
canvas.height = viewport.height;
|
|
await page.render({ canvasContext: context, viewport, canvas }).promise;
|
|
pageState.pageSnapshots[pageNum] = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
updatePreviewNav();
|
|
drawGridOverlay();
|
|
hideLoader();
|
|
}
|
|
|
|
function drawGridOverlay() {
|
|
if (!pageState.pageSnapshots[pageState.currentPage] || !pageState.pdfJsDoc) return;
|
|
|
|
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
|
const context = canvas.getContext('2d');
|
|
|
|
if (!context) return;
|
|
|
|
context.putImageData(pageState.pageSnapshots[pageState.currentPage], 0, 0);
|
|
|
|
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
|
const pagesToProcess = parsePageRanges(pageRangeInput, pageState.pdfJsDoc.numPages);
|
|
|
|
if (pagesToProcess.includes(pageState.currentPage - 1)) {
|
|
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
|
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
|
|
|
|
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
|
|
context.lineWidth = 2;
|
|
context.setLineDash([10, 5]);
|
|
|
|
const cellWidth = canvas.width / cols;
|
|
const cellHeight = canvas.height / rows;
|
|
|
|
for (let i = 1; i < cols; i++) {
|
|
context.beginPath();
|
|
context.moveTo(i * cellWidth, 0);
|
|
context.lineTo(i * cellWidth, canvas.height);
|
|
context.stroke();
|
|
}
|
|
|
|
for (let i = 1; i < rows; i++) {
|
|
context.beginPath();
|
|
context.moveTo(0, i * cellHeight);
|
|
context.lineTo(canvas.width, i * cellHeight);
|
|
context.stroke();
|
|
}
|
|
|
|
context.setLineDash([]);
|
|
}
|
|
}
|
|
|
|
function updatePreviewNav() {
|
|
if (!pageState.pdfJsDoc) return;
|
|
|
|
const currentPageSpan = document.getElementById('current-preview-page');
|
|
const prevBtn = document.getElementById('prev-preview-page') as HTMLButtonElement;
|
|
const nextBtn = document.getElementById('next-preview-page') as HTMLButtonElement;
|
|
|
|
if (currentPageSpan) currentPageSpan.textContent = pageState.currentPage.toString();
|
|
if (prevBtn) prevBtn.disabled = pageState.currentPage <= 1;
|
|
if (nextBtn) nextBtn.disabled = pageState.currentPage >= pageState.pdfJsDoc.numPages;
|
|
}
|
|
|
|
async function posterize() {
|
|
if (!pageState.pdfJsDoc || !pageState.pdfBytes) {
|
|
showAlert('No File', 'Please upload a PDF file first.');
|
|
return;
|
|
}
|
|
|
|
showLoader('Posterizing PDF...');
|
|
|
|
try {
|
|
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
|
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
|
|
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value as keyof typeof PageSizes;
|
|
let orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value;
|
|
const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value;
|
|
const overlap = parseFloat((document.getElementById('overlap') as HTMLInputElement).value) || 0;
|
|
const overlapUnits = (document.getElementById('overlap-units') as HTMLSelectElement).value;
|
|
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
|
|
|
let overlapInPoints = overlap;
|
|
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
|
|
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
|
|
|
|
const newDoc = await PDFDocument.create();
|
|
const totalPages = pageState.pdfJsDoc.numPages;
|
|
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
|
|
|
if (pageIndicesToProcess.length === 0) {
|
|
throw new Error('Invalid page range specified.');
|
|
}
|
|
|
|
const tempCanvas = document.createElement('canvas');
|
|
const tempCtx = tempCanvas.getContext('2d');
|
|
|
|
if (!tempCtx) {
|
|
throw new Error('Could not create canvas context.');
|
|
}
|
|
|
|
for (const pageIndex of pageIndicesToProcess) {
|
|
const page = await pageState.pdfJsDoc.getPage(Number(pageIndex) + 1);
|
|
const viewport = page.getViewport({ scale: 2.0 });
|
|
tempCanvas.width = viewport.width;
|
|
tempCanvas.height = viewport.height;
|
|
await page.render({ canvasContext: tempCtx, viewport, canvas: tempCanvas }).promise;
|
|
|
|
let [targetWidth, targetHeight] = PageSizes[pageSizeKey] || PageSizes.A4;
|
|
let currentOrientation = orientation;
|
|
|
|
if (currentOrientation === 'auto') {
|
|
currentOrientation = viewport.width > viewport.height ? 'landscape' : 'portrait';
|
|
}
|
|
|
|
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
|
|
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
|
} else if (currentOrientation === 'portrait' && targetWidth > targetHeight) {
|
|
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
|
}
|
|
|
|
const tileWidth = tempCanvas.width / cols;
|
|
const tileHeight = tempCanvas.height / rows;
|
|
|
|
for (let r = 0; r < rows; r++) {
|
|
for (let c = 0; c < cols; c++) {
|
|
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
|
|
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
|
|
const sWidth = tileWidth + (c > 0 ? overlapInPoints : 0) + (c < cols - 1 ? overlapInPoints : 0);
|
|
const sHeight = tileHeight + (r > 0 ? overlapInPoints : 0) + (r < rows - 1 ? overlapInPoints : 0);
|
|
|
|
const tileCanvas = document.createElement('canvas');
|
|
tileCanvas.width = sWidth;
|
|
tileCanvas.height = sHeight;
|
|
const tileCtx = tileCanvas.getContext('2d');
|
|
|
|
if (tileCtx) {
|
|
tileCtx.drawImage(tempCanvas, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
|
|
|
|
const tileImage = await newDoc.embedPng(tileCanvas.toDataURL('image/png'));
|
|
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
|
|
|
const scaleX = newPage.getWidth() / sWidth;
|
|
const scaleY = newPage.getHeight() / sHeight;
|
|
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
|
|
|
const scaledWidth = sWidth * scale;
|
|
const scaledHeight = sHeight * scale;
|
|
|
|
newPage.drawImage(tileImage, {
|
|
x: (newPage.getWidth() - scaledWidth) / 2,
|
|
y: (newPage.getHeight() - scaledHeight) / 2,
|
|
width: scaledWidth,
|
|
height: scaledHeight,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const newPdfBytes = await newDoc.save();
|
|
downloadFile(
|
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
|
'posterized.pdf'
|
|
);
|
|
|
|
showAlert('Success', 'Your PDF has been posterized.');
|
|
} catch (e) {
|
|
console.error(e);
|
|
showAlert('Error', (e as Error).message || 'Could not posterize the PDF.');
|
|
} finally {
|
|
hideLoader();
|
|
}
|
|
}
|
|
|
|
async function updateUI() {
|
|
const fileDisplayArea = document.getElementById('file-display-area');
|
|
const toolOptions = document.getElementById('tool-options');
|
|
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
|
|
|
if (!fileDisplayArea) return;
|
|
|
|
fileDisplayArea.innerHTML = '';
|
|
|
|
if (pageState.file) {
|
|
const fileDiv = document.createElement('div');
|
|
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
|
|
|
const infoContainer = document.createElement('div');
|
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
|
|
|
const nameSpan = document.createElement('div');
|
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
|
nameSpan.textContent = pageState.file.name;
|
|
|
|
const metaSpan = document.createElement('div');
|
|
metaSpan.className = 'text-xs text-gray-400';
|
|
metaSpan.textContent = formatBytes(pageState.file.size);
|
|
|
|
infoContainer.append(nameSpan, metaSpan);
|
|
|
|
const removeBtn = document.createElement('button');
|
|
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.onclick = function () {
|
|
resetState();
|
|
};
|
|
|
|
fileDiv.append(infoContainer, removeBtn);
|
|
fileDisplayArea.appendChild(fileDiv);
|
|
createIcons({ icons });
|
|
|
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
|
if (processBtn) processBtn.disabled = false;
|
|
} else {
|
|
if (toolOptions) toolOptions.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
async function handleFileSelect(files: FileList | null) {
|
|
if (files && files.length > 0) {
|
|
const file = files[0];
|
|
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
|
pageState.file = file;
|
|
pageState.pdfBytes = new Uint8Array(await file.arrayBuffer());
|
|
pageState.pdfJsDoc = await getPDFDocument({ data: pageState.pdfBytes }).promise;
|
|
pageState.pageSnapshots = {};
|
|
pageState.currentPage = 1;
|
|
|
|
const totalPagesSpan = document.getElementById('total-pages');
|
|
const totalPreviewPages = document.getElementById('total-preview-pages');
|
|
|
|
if (totalPagesSpan && pageState.pdfJsDoc) {
|
|
totalPagesSpan.textContent = pageState.pdfJsDoc.numPages.toString();
|
|
}
|
|
if (totalPreviewPages && pageState.pdfJsDoc) {
|
|
totalPreviewPages.textContent = pageState.pdfJsDoc.numPages.toString();
|
|
}
|
|
|
|
await updateUI();
|
|
await renderPosterizePreview(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
|
const dropZone = document.getElementById('drop-zone');
|
|
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
|
const backBtn = document.getElementById('back-to-tools');
|
|
const prevBtn = document.getElementById('prev-preview-page');
|
|
const nextBtn = document.getElementById('next-preview-page');
|
|
const rowsInput = document.getElementById('posterize-rows');
|
|
const colsInput = document.getElementById('posterize-cols');
|
|
const pageRangeInput = document.getElementById('page-range');
|
|
|
|
if (backBtn) {
|
|
backBtn.addEventListener('click', function () {
|
|
window.location.href = import.meta.env.BASE_URL;
|
|
});
|
|
}
|
|
|
|
if (fileInput && dropZone) {
|
|
fileInput.addEventListener('change', function (e) {
|
|
handleFileSelect((e.target as HTMLInputElement).files);
|
|
});
|
|
|
|
dropZone.addEventListener('dragover', function (e) {
|
|
e.preventDefault();
|
|
dropZone.classList.add('bg-gray-700');
|
|
});
|
|
|
|
dropZone.addEventListener('dragleave', function (e) {
|
|
e.preventDefault();
|
|
dropZone.classList.remove('bg-gray-700');
|
|
});
|
|
|
|
dropZone.addEventListener('drop', function (e) {
|
|
e.preventDefault();
|
|
dropZone.classList.remove('bg-gray-700');
|
|
const files = e.dataTransfer?.files;
|
|
if (files && files.length > 0) {
|
|
const pdfFiles = Array.from(files).filter(function (f) {
|
|
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
|
});
|
|
if (pdfFiles.length > 0) {
|
|
const dataTransfer = new DataTransfer();
|
|
dataTransfer.items.add(pdfFiles[0]);
|
|
handleFileSelect(dataTransfer.files);
|
|
}
|
|
}
|
|
});
|
|
|
|
fileInput.addEventListener('click', function () {
|
|
fileInput.value = '';
|
|
});
|
|
}
|
|
|
|
// Preview navigation
|
|
if (prevBtn) {
|
|
prevBtn.addEventListener('click', function () {
|
|
if (pageState.currentPage > 1) {
|
|
renderPosterizePreview(pageState.currentPage - 1);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (nextBtn) {
|
|
nextBtn.addEventListener('click', function () {
|
|
if (pageState.pdfJsDoc && pageState.currentPage < pageState.pdfJsDoc.numPages) {
|
|
renderPosterizePreview(pageState.currentPage + 1);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Grid input changes trigger overlay redraw
|
|
if (rowsInput) {
|
|
rowsInput.addEventListener('input', drawGridOverlay);
|
|
}
|
|
if (colsInput) {
|
|
colsInput.addEventListener('input', drawGridOverlay);
|
|
}
|
|
if (pageRangeInput) {
|
|
pageRangeInput.addEventListener('input', drawGridOverlay);
|
|
}
|
|
|
|
// Process button
|
|
if (processBtn) {
|
|
processBtn.addEventListener('click', posterize);
|
|
}
|
|
});
|