feat(posterize): enhance posterize tool with page navigation and auto orientation
- Add page navigation controls to preview different pages - Implement automatic orientation detection based on page dimensions - Improve grid overlay rendering to only show for selected pages - Cache page snapshots for better performance
This commit is contained in:
@@ -1,44 +1,58 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, parsePageRanges } from '../utils/helpers.js';
|
import { downloadFile, parsePageRanges } from '../utils/helpers.js';
|
||||||
import { state } from '../state.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 * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
import { createIcons, icons } from 'lucide';
|
||||||
|
|
||||||
let pdfJsDoc = null;
|
const posterizeState = {
|
||||||
let pageSnapshot = null;
|
pdfJsDoc: null,
|
||||||
|
pageSnapshots: {},
|
||||||
|
currentPage: 1,
|
||||||
|
};
|
||||||
|
|
||||||
async function renderPosterizePreview() {
|
async function renderPosterizePreview(pageNum: number) {
|
||||||
if (!pdfJsDoc) return;
|
if (!posterizeState.pdfJsDoc) return;
|
||||||
showLoader('Rendering preview...');
|
|
||||||
|
posterizeState.currentPage = pageNum;
|
||||||
|
showLoader(`Rendering preview for page ${pageNum}...`);
|
||||||
|
|
||||||
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
const page = await pdfJsDoc.getPage(1); // Always preview the first page
|
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 });
|
const viewport = page.getViewport({ scale: 1.5 });
|
||||||
|
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
|
|
||||||
await page.render({ canvasContext: context, viewport }).promise;
|
await page.render({ canvasContext: context, viewport }).promise;
|
||||||
|
posterizeState.pageSnapshots[pageNum] = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
pageSnapshot = context.getImageData(0, 0, canvas.width, canvas.height);
|
updatePreviewNav();
|
||||||
|
|
||||||
drawGridOverlay();
|
drawGridOverlay();
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawGridOverlay() {
|
function drawGridOverlay() {
|
||||||
if (!pageSnapshot) return;
|
if (!posterizeState.pageSnapshots[posterizeState.currentPage]) return;
|
||||||
|
|
||||||
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
|
context.putImageData(posterizeState.pageSnapshots[posterizeState.currentPage], 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 rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
||||||
const cols = parseInt((document.getElementById('posterize-cols') 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)';
|
||||||
|
|
||||||
context.strokeStyle = 'rgba(239, 68, 68, 0.9)'; // Grid line color
|
|
||||||
context.lineWidth = 2;
|
context.lineWidth = 2;
|
||||||
context.setLineDash([10, 5]);
|
context.setLineDash([10, 5]);
|
||||||
|
|
||||||
@@ -58,22 +72,38 @@ function drawGridOverlay() {
|
|||||||
context.lineTo(canvas.width, i * cellHeight);
|
context.lineTo(canvas.width, i * cellHeight);
|
||||||
context.stroke();
|
context.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
context.setLineDash([]);
|
context.setLineDash([]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
currentPageSpan.textContent = posterizeState.currentPage.toString();
|
||||||
|
prevBtn.disabled = posterizeState.currentPage <= 1;
|
||||||
|
nextBtn.disabled = posterizeState.currentPage >= posterizeState.pdfJsDoc.numPages;
|
||||||
|
}
|
||||||
|
|
||||||
export async function setupPosterizeTool() {
|
export async function setupPosterizeTool() {
|
||||||
if (state.pdfDoc) {
|
if (state.pdfDoc) {
|
||||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString();
|
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString();
|
||||||
|
|
||||||
const pdfBytes = await state.pdfDoc.save();
|
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;
|
||||||
|
|
||||||
await renderPosterizePreview();
|
document.getElementById('total-preview-pages').textContent = posterizeState.pdfJsDoc.numPages.toString();
|
||||||
|
await renderPosterizePreview(1);
|
||||||
|
|
||||||
// Add event listeners to update the grid on change
|
document.getElementById('prev-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage - 1);
|
||||||
document.getElementById('posterize-rows').addEventListener('input', drawGridOverlay);
|
document.getElementById('next-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage + 1);
|
||||||
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 rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
||||||
const cols = parseInt((document.getElementById('posterize-cols') 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 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 scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value;
|
||||||
const overlap = parseFloat((document.getElementById('overlap') as HTMLInputElement).value) || 0;
|
const overlap = parseFloat((document.getElementById('overlap') as HTMLInputElement).value) || 0;
|
||||||
const overlapUnits = (document.getElementById('overlap-units') as HTMLSelectElement).value;
|
const overlapUnits = (document.getElementById('overlap-units') as HTMLSelectElement).value;
|
||||||
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
||||||
|
|
||||||
let overlapInPoints = overlap;
|
let overlapInPoints = overlap;
|
||||||
if (overlapUnits === 'in') {
|
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
|
||||||
overlapInPoints = overlap * 72;
|
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
|
||||||
} else if (overlapUnits === 'mm') {
|
|
||||||
overlapInPoints = overlap * (72 / 25.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceDoc = state.pdfDoc;
|
|
||||||
const newDoc = await PDFDocument.create();
|
const newDoc = await PDFDocument.create();
|
||||||
const totalPages = sourceDoc.getPageCount();
|
const totalPages = posterizeState.pdfJsDoc.numPages;
|
||||||
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||||
|
|
||||||
if (pageIndicesToProcess.length === 0) {
|
if (pageIndicesToProcess.length === 0) {
|
||||||
throw new Error("Invalid page range specified. Please check your input (e.g., '1-3, 5').");
|
throw new Error("Invalid page range specified.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let [targetWidth, targetHeight] = PageSizes[pageSizeKey];
|
const tempCanvas = document.createElement('canvas');
|
||||||
if (orientation === 'landscape' && targetWidth < targetHeight) {
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
|
||||||
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
|
|
||||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const pageIndex of pageIndicesToProcess) {
|
for (const pageIndex of pageIndicesToProcess) {
|
||||||
const sourcePage = sourceDoc.getPages()[pageIndex as number];
|
const page = await posterizeState.pdfJsDoc.getPage(Number(pageIndex) + 1);
|
||||||
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
|
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 r = 0; r < rows; r++) {
|
||||||
for (let c = 0; c < cols; c++) {
|
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 newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||||
|
|
||||||
const tileWidth = sourceWidth / cols;
|
const scaleX = newPage.getWidth() / sWidth;
|
||||||
const tileHeight = sourceHeight / rows;
|
const scaleY = newPage.getHeight() / sHeight;
|
||||||
|
|
||||||
const scaleX = targetWidth / tileWidth;
|
|
||||||
const scaleY = targetHeight / tileHeight;
|
|
||||||
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
const scaledTileWidth = tileWidth * scale;
|
const scaledWidth = sWidth * scale;
|
||||||
const scaledTileHeight = tileHeight * scale;
|
const scaledHeight = sHeight * scale;
|
||||||
|
|
||||||
const offsetX = (targetWidth - scaledTileWidth) / 2;
|
newPage.drawImage(tileImage, {
|
||||||
const offsetY = (targetHeight - scaledTileHeight) / 2;
|
x: (newPage.getWidth() - scaledWidth) / 2,
|
||||||
|
y: (newPage.getHeight() - scaledHeight) / 2,
|
||||||
const tileRowIndexFromBottom = rows - 1 - r;
|
width: scaledWidth,
|
||||||
const overlapOffset = tileRowIndexFromBottom * overlapInPoints;
|
height: scaledHeight,
|
||||||
|
|
||||||
newPage.drawPage(embeddedPage, {
|
|
||||||
x: -c * scaledTileWidth + offsetX - (c * overlapInPoints),
|
|
||||||
y: -tileRowIndexFromBottom * scaledTileHeight + offsetY + overlapOffset,
|
|
||||||
width: sourceWidth * scale,
|
|
||||||
height: sourceHeight * scale,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/js/ui.ts
14
src/js/ui.ts
@@ -1696,14 +1696,19 @@ cropper: () => `
|
|||||||
|
|
||||||
posterize: () => `
|
posterize: () => `
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">Posterize PDF</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">Posterize PDF</h2>
|
||||||
<p class="mb-6 text-gray-400">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.</p>
|
<p class="mb-6 text-gray-400">Split pages into multiple smaller sheets to print as a poster. Navigate the preview and see the grid update based on your settings.</p>
|
||||||
${createFileInputHTML()}
|
${createFileInputHTML()}
|
||||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
|
|
||||||
<div id="posterize-options" class="hidden mt-6 space-y-6">
|
<div id="posterize-options" class="hidden mt-6 space-y-6">
|
||||||
|
|
||||||
<div id="posterize-preview-container" class="relative w-full max-w-xl mx-auto bg-gray-900 rounded-lg border-2 border-gray-600">
|
<div class="space-y-2">
|
||||||
<canvas id="posterize-preview-canvas" class="w-full h-auto"></canvas>
|
<label class="block text-sm font-medium text-gray-300">Page Preview (<span id="current-preview-page">1</span> / <span id="total-preview-pages">1</span>)</label>
|
||||||
|
<div id="posterize-preview-container" class="relative w-full max-w-xl mx-auto bg-gray-900 rounded-lg border-2 border-gray-600 flex items-center justify-center">
|
||||||
|
<button id="prev-preview-page" class="absolute left-2 top-1/2 transform -translate-y-1/2 text-white bg-gray-800 bg-opacity-50 rounded-full p-2 hover:bg-gray-700 disabled:opacity-50 z-10"><i data-lucide="chevron-left"></i></button>
|
||||||
|
<canvas id="posterize-preview-canvas" class="w-full h-auto rounded-md"></canvas>
|
||||||
|
<button id="next-preview-page" class="absolute right-2 top-1/2 transform -translate-y-1/2 text-white bg-gray-800 bg-opacity-50 rounded-full p-2 hover:bg-gray-700 disabled:opacity-50 z-10"><i data-lucide="chevron-right"></i></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 bg-gray-900 border border-gray-700 rounded-lg">
|
<div class="p-4 bg-gray-900 border border-gray-700 rounded-lg">
|
||||||
@@ -1736,7 +1741,8 @@ posterize: () => `
|
|||||||
<div>
|
<div>
|
||||||
<label for="output-orientation" class="block mb-2 text-sm font-medium text-gray-300">Orientation</label>
|
<label for="output-orientation" class="block mb-2 text-sm font-medium text-gray-300">Orientation</label>
|
||||||
<select id="output-orientation" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
|
<select id="output-orientation" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
|
||||||
<option value="portrait" selected>Portrait</option>
|
<option value="auto" selected>Automatic (Recommended)</option>
|
||||||
|
<option value="portrait">Portrait</option>
|
||||||
<option value="landscape">Landscape</option>
|
<option value="landscape">Landscape</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user