diff --git a/src/js/config/pdf-tools.ts b/src/js/config/pdf-tools.ts index 27e3e78..3791c43 100644 --- a/src/js/config/pdf-tools.ts +++ b/src/js/config/pdf-tools.ts @@ -5,7 +5,7 @@ export const singlePdfLoadTools = [ 'extract-pages', 'add-watermark', 'add-header-footer', 'invert-colors', 'view-metadata', 'reverse-pages', 'crop', 'redact', 'pdf-to-bmp', 'pdf-to-tiff', 'split-in-half', 'page-dimensions', 'n-up', 'duplicate-organize', 'combine-single-page', 'fix-dimensions', 'change-background-color', - 'change-text-color', 'ocr-pdf', 'sign-pdf', 'remove-annotations', 'cropper', 'form-filler', + 'change-text-color', 'ocr-pdf', 'sign-pdf', 'remove-annotations', 'cropper', 'form-filler', 'posterize' ]; export const simpleTools = [ diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index 260591e..376db36 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -81,6 +81,7 @@ export const categories = [ { id: 'edit-metadata', name: 'Edit Metadata', icon: 'file-cog', subtitle: 'Change the author, title, and other properties.' }, { id: 'pdf-to-zip', name: 'PDFs to ZIP', icon: 'stretch-horizontal', subtitle: 'Package multiple PDF files into a ZIP archive.' }, { id: 'compare-pdfs', name: 'Compare PDFs', icon: 'git-compare', subtitle: 'Compare two PDFs side by side.' }, + { id: 'posterize', name: 'Posterize PDF', icon: 'layout-grid', subtitle: 'Split a large page into multiple smaller pages.' }, ] }, { diff --git a/src/js/logic/index.ts b/src/js/logic/index.ts index ab6b0e7..dfa184c 100644 --- a/src/js/logic/index.ts +++ b/src/js/logic/index.ts @@ -52,6 +52,7 @@ import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js'; import { removeAnnotations, setupRemoveAnnotationsTool } from './remove-annotations.js'; import { setupCropperTool } from './cropper.js'; import { processAndDownloadForm, setupFormFiller } from './form-filler.js'; +import { posterize, setupPosterizeTool } from './posterize.js'; export const toolLogic = { merge: { process: merge, setup: setupMergeTool }, @@ -107,4 +108,5 @@ export const toolLogic = { 'remove-annotations': { process: removeAnnotations, setup: setupRemoveAnnotationsTool }, 'cropper': { setup: setupCropperTool }, 'form-filler': { process: processAndDownloadForm, setup: setupFormFiller}, + 'posterize': { process: posterize, setup: setupPosterizeTool }, }; \ No newline at end of file diff --git a/src/js/logic/posterize.ts b/src/js/logic/posterize.ts new file mode 100644 index 0000000..955d7ac --- /dev/null +++ b/src/js/logic/posterize.ts @@ -0,0 +1,158 @@ +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 * as pdfjsLib from 'pdfjs-dist'; + +let pdfJsDoc = null; +let pageSnapshot = null; + +async function renderPosterizePreview() { + if (!pdfJsDoc) return; + showLoader('Rendering preview...'); + + 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); + + drawGridOverlay(); + hideLoader(); +} + +function drawGridOverlay() { + if (!pageSnapshot) 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(pageSnapshot, 0, 0); + + context.strokeStyle = 'rgba(239, 68, 68, 0.9)'; // Grid line color + 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([]); +} + +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; + + await renderPosterizePreview(); + + // Add event listeners to update the grid on change + document.getElementById('posterize-rows').addEventListener('input', drawGridOverlay); + document.getElementById('posterize-cols').addEventListener('input', drawGridOverlay); + } +} + +export async function posterize() { + 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; + const 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 sourceDoc = state.pdfDoc; + const newDoc = await PDFDocument.create(); + const totalPages = sourceDoc.getPageCount(); + 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]; + } + + for (const pageIndex of pageIndicesToProcess) { + const sourcePage = sourceDoc.getPages()[pageIndex as number]; + const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize(); + + const embeddedPage = await newDoc.embedPage(sourcePage); + + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const newPage = newDoc.addPage([targetWidth, targetHeight]); + + const tileWidth = sourceWidth / cols; + const tileHeight = sourceHeight / rows; + + const scaleX = targetWidth / tileWidth; + const scaleY = targetHeight / tileHeight; + const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY); + + const scaledTileWidth = tileWidth * scale; + const scaledTileHeight = tileHeight * scale; + + const offsetX = (targetWidth - scaledTileWidth) / 2; + const offsetY = (targetHeight - scaledTileHeight) / 2; + + newPage.drawPage(embeddedPage, { + x: -c * scaledTileWidth + offsetX - (c * overlapInPoints), + y: r * scaledTileHeight + offsetY - ((rows - 1 - r) * overlapInPoints), + width: sourceWidth * scale, + height: sourceHeight * scale, + }); + } + } + } + + 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.message || 'Could not posterize the PDF.'); + } finally { + hideLoader(); + } +} \ No newline at end of file diff --git a/src/js/ui.ts b/src/js/ui.ts index 976a8c1..e63d325 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -1694,4 +1694,98 @@ cropper: () => ` `, +posterize: () => ` +

Posterize PDF

+

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.

+ ${createFileInputHTML()} +
+ + +`, + }; \ No newline at end of file