diff --git a/src/js/logic/posterize.ts b/src/js/logic/posterize.ts index c22c74e..d1c322b 100644 --- a/src/js/logic/posterize.ts +++ b/src/js/logic/posterize.ts @@ -1,79 +1,109 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, parsePageRanges } from '../utils/helpers.js'; import { state } from '../state.js'; -import { PDFDocument, rgb, PageSizes } from 'pdf-lib'; +import { PDFDocument, PageSizes } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; +import { createIcons, icons } from 'lucide'; -let pdfJsDoc = null; -let pageSnapshot = null; +const posterizeState = { + pdfJsDoc: null, + pageSnapshots: {}, + currentPage: 1, +}; -async function renderPosterizePreview() { - if (!pdfJsDoc) return; - showLoader('Rendering preview...'); +async function renderPosterizePreview(pageNum: number) { + if (!posterizeState.pdfJsDoc) return; + + posterizeState.currentPage = pageNum; + showLoader(`Rendering preview for page ${pageNum}...`); const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement; const context = canvas.getContext('2d'); - const page = await pdfJsDoc.getPage(1); // Always preview the first page - const viewport = page.getViewport({ scale: 1.5 }); - - canvas.width = viewport.width; - canvas.height = viewport.height; - - await page.render({ canvasContext: context, viewport }).promise; - - pageSnapshot = context.getImageData(0, 0, canvas.width, canvas.height); - + if (posterizeState.pageSnapshots[pageNum]) { + canvas.width = posterizeState.pageSnapshots[pageNum].width; + canvas.height = posterizeState.pageSnapshots[pageNum].height; + context.putImageData(posterizeState.pageSnapshots[pageNum], 0, 0); + } else { + const page = await posterizeState.pdfJsDoc.getPage(pageNum); + const viewport = page.getViewport({ scale: 1.5 }); + canvas.width = viewport.width; + canvas.height = viewport.height; + await page.render({ canvasContext: context, viewport }).promise; + posterizeState.pageSnapshots[pageNum] = context.getImageData(0, 0, canvas.width, canvas.height); + } + + updatePreviewNav(); drawGridOverlay(); hideLoader(); } function drawGridOverlay() { - if (!pageSnapshot) return; + if (!posterizeState.pageSnapshots[posterizeState.currentPage]) return; const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement; const context = canvas.getContext('2d'); - const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1; - const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1; + context.putImageData(posterizeState.pageSnapshots[posterizeState.currentPage], 0, 0); - context.putImageData(pageSnapshot, 0, 0); + const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value; + const pagesToProcess = parsePageRanges(pageRangeInput, posterizeState.pdfJsDoc.numPages); + + if (pagesToProcess.includes(posterizeState.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)'; // Grid line color - context.lineWidth = 2; - context.setLineDash([10, 5]); + 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; + 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 < 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([]); } +} - for (let i = 1; i < rows; i++) { - context.beginPath(); - context.moveTo(0, i * cellHeight); - context.lineTo(canvas.width, i * cellHeight); - context.stroke(); - } +function updatePreviewNav() { + 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; - context.setLineDash([]); + currentPageSpan.textContent = posterizeState.currentPage.toString(); + prevBtn.disabled = posterizeState.currentPage <= 1; + nextBtn.disabled = posterizeState.currentPage >= posterizeState.pdfJsDoc.numPages; } export async function setupPosterizeTool() { if (state.pdfDoc) { document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString(); - const pdfBytes = await state.pdfDoc.save(); - pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise; + posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise; + posterizeState.pageSnapshots = {}; + posterizeState.currentPage = 1; + + document.getElementById('total-preview-pages').textContent = posterizeState.pdfJsDoc.numPages.toString(); + await renderPosterizePreview(1); - await renderPosterizePreview(); + document.getElementById('prev-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage - 1); + document.getElementById('next-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage + 1); - // Add event listeners to update the grid on change - document.getElementById('posterize-rows').addEventListener('input', drawGridOverlay); - document.getElementById('posterize-cols').addEventListener('input', drawGridOverlay); + ['posterize-rows', 'posterize-cols', 'page-range'].forEach(id => { + document.getElementById(id).addEventListener('input', drawGridOverlay); + }); + createIcons({ icons }); } } @@ -83,66 +113,77 @@ export async function posterize() { 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; - const orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value; + 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); - } + if (overlapUnits === 'in') overlapInPoints = overlap * 72; + else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4); - const sourceDoc = state.pdfDoc; const newDoc = await PDFDocument.create(); - const totalPages = sourceDoc.getPageCount(); + const totalPages = posterizeState.pdfJsDoc.numPages; const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages); if (pageIndicesToProcess.length === 0) { - throw new Error("Invalid page range specified. Please check your input (e.g., '1-3, 5')."); - } - - let [targetWidth, targetHeight] = PageSizes[pageSizeKey]; - if (orientation === 'landscape' && targetWidth < targetHeight) { - [targetWidth, targetHeight] = [targetHeight, targetWidth]; - } else if (orientation === 'portrait' && targetWidth > targetHeight) { - [targetWidth, targetHeight] = [targetHeight, targetWidth]; + throw new Error("Invalid page range specified."); } + + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); for (const pageIndex of pageIndicesToProcess) { - const sourcePage = sourceDoc.getPages()[pageIndex as number]; - const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize(); + const page = await posterizeState.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 }).promise; - const embeddedPage = await newDoc.embedPage(sourcePage); + let [targetWidth, targetHeight] = PageSizes[pageSizeKey]; + 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; + tileCanvas.getContext('2d').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 tileWidth = sourceWidth / cols; - const tileHeight = sourceHeight / rows; - - const scaleX = targetWidth / tileWidth; - const scaleY = targetHeight / tileHeight; + + 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; - const scaledTileWidth = tileWidth * scale; - const scaledTileHeight = tileHeight * scale; - - const offsetX = (targetWidth - scaledTileWidth) / 2; - const offsetY = (targetHeight - scaledTileHeight) / 2; - - const tileRowIndexFromBottom = rows - 1 - r; - const overlapOffset = tileRowIndexFromBottom * overlapInPoints; - - newPage.drawPage(embeddedPage, { - x: -c * scaledTileWidth + offsetX - (c * overlapInPoints), - y: -tileRowIndexFromBottom * scaledTileHeight + offsetY + overlapOffset, - width: sourceWidth * scale, - height: sourceHeight * scale, + newPage.drawImage(tileImage, { + x: (newPage.getWidth() - scaledWidth) / 2, + y: (newPage.getHeight() - scaledHeight) / 2, + width: scaledWidth, + height: scaledHeight, }); } } @@ -158,4 +199,4 @@ export async function posterize() { } finally { hideLoader(); } -} \ No newline at end of file +} diff --git a/src/js/ui.ts b/src/js/ui.ts index e63d325..63a02c2 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -1696,14 +1696,19 @@ cropper: () => ` posterize: () => `
Split a single page into multiple smaller pages to print as a poster. Specify a page range or leave it blank to process all pages.
+Split pages into multiple smaller sheets to print as a poster. Navigate the preview and see the grid update based on your settings.
${createFileInputHTML()}