feat(pdf-tools): add posterize tool to split pages into grid

Add new posterize feature that allows splitting PDF pages into a grid of smaller pages. The tool includes:
- Configurable rows and columns
- Page size and orientation options
- Content scaling modes
- Overlap settings for assembly
- Page range selection
This commit is contained in:
abdullahalam123
2025-10-14 11:40:22 +05:30
parent ea8b350215
commit 0c1351adc5
5 changed files with 256 additions and 1 deletions

View File

@@ -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 },
};

158
src/js/logic/posterize.ts Normal file
View File

@@ -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();
}
}