From d32251014ebdcff515b7b7903fa84aed26be92ae Mon Sep 17 00:00:00 2001 From: alam00000 Date: Sat, 7 Mar 2026 14:33:33 +0530 Subject: [PATCH] feat: integrate fix page size functionality into the workflow by extracting core logic to a utility and adding a new workflow node. --- src/js/logic/fix-page-size-page.ts | 356 ++++++++++---------- src/js/logic/pdf-workflow-page.ts | 24 +- src/js/utils/pdf-operations.ts | 86 ++++- src/js/workflow/nodes/fix-page-size-node.ts | 104 ++++++ src/js/workflow/nodes/registry.ts | 8 + 5 files changed, 390 insertions(+), 188 deletions(-) create mode 100644 src/js/workflow/nodes/fix-page-size-node.ts diff --git a/src/js/logic/fix-page-size-page.ts b/src/js/logic/fix-page-size-page.ts index 76c54d4..976c771 100644 --- a/src/js/logic/fix-page-size-page.ts +++ b/src/js/logic/fix-page-size-page.ts @@ -1,230 +1,218 @@ import { showAlert } from '../ui.js'; import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js'; -import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib'; +import { fixPageSize as fixPageSizeCore } from '../utils/pdf-operations'; import { icons, createIcons } from 'lucide'; import { FixPageSizeState } from '@/types'; const pageState: FixPageSizeState = { - file: null, + file: null, }; function resetState() { - pageState.file = null; + pageState.file = null; - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - const toolOptions = document.getElementById('tool-options'); - if (toolOptions) toolOptions.classList.add('hidden'); + 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 fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; } async function updateUI() { - const fileDisplayArea = document.getElementById('file-display-area'); - const toolOptions = document.getElementById('tool-options'); + const fileDisplayArea = document.getElementById('file-display-area'); + const toolOptions = document.getElementById('tool-options'); - if (!fileDisplayArea) return; + if (!fileDisplayArea) return; - fileDisplayArea.innerHTML = ''; + 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'; + 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 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 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); + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = formatBytes(pageState.file.size); - infoContainer.append(nameSpan, metaSpan); + 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 = ''; - removeBtn.onclick = function () { - resetState(); - }; + const removeBtn = document.createElement('button'); + removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = function () { + resetState(); + }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - if (toolOptions) toolOptions.classList.remove('hidden'); - } else { - if (toolOptions) toolOptions.classList.add('hidden'); - } + if (toolOptions) toolOptions.classList.remove('hidden'); + } else { + if (toolOptions) toolOptions.classList.add('hidden'); + } } 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; - updateUI(); - } + if (files && files.length > 0) { + const file = files[0]; + if ( + file.type === 'application/pdf' || + file.name.toLowerCase().endsWith('.pdf') + ) { + pageState.file = file; + updateUI(); } + } } async function fixPageSize() { - if (!pageState.file) { - showAlert('No File', 'Please upload a PDF file first.'); - return; - } + if (!pageState.file) { + showAlert('No File', 'Please upload a PDF file first.'); + return; + } - const targetSizeKey = (document.getElementById('target-size') as HTMLSelectElement).value; - const orientation = (document.getElementById('orientation') as HTMLSelectElement).value; - const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value; - const backgroundColor = hexToRgb((document.getElementById('background-color') as HTMLInputElement).value); + const targetSize = ( + document.getElementById('target-size') as HTMLSelectElement + ).value; + const orientation = ( + document.getElementById('orientation') as HTMLSelectElement + ).value; + const scalingMode = ( + document.querySelector( + 'input[name="scaling-mode"]:checked' + ) as HTMLInputElement + ).value; + const backgroundColor = hexToRgb( + (document.getElementById('background-color') as HTMLInputElement).value + ); - const loaderModal = document.getElementById('loader-modal'); - const loaderText = document.getElementById('loader-text'); - if (loaderModal) loaderModal.classList.remove('hidden'); - if (loaderText) loaderText.textContent = 'Standardizing pages...'; + const loaderModal = document.getElementById('loader-modal'); + const loaderText = document.getElementById('loader-text'); + if (loaderModal) loaderModal.classList.remove('hidden'); + if (loaderText) loaderText.textContent = 'Standardizing pages...'; - try { - let targetWidth, targetHeight; + try { + const customWidth = + parseFloat( + (document.getElementById('custom-width') as HTMLInputElement)?.value + ) || 210; + const customHeight = + parseFloat( + (document.getElementById('custom-height') as HTMLInputElement)?.value + ) || 297; + const customUnits = + (document.getElementById('custom-units') as HTMLSelectElement)?.value || + 'mm'; - if (targetSizeKey === 'Custom') { - const width = parseFloat((document.getElementById('custom-width') as HTMLInputElement).value); - const height = parseFloat((document.getElementById('custom-height') as HTMLInputElement).value); - const units = (document.getElementById('custom-units') as HTMLSelectElement).value; + const arrayBuffer = await pageState.file.arrayBuffer(); + const pdfBytes = new Uint8Array(arrayBuffer); - if (units === 'in') { - targetWidth = width * 72; - targetHeight = height * 72; - } else { - // mm - targetWidth = width * (72 / 25.4); - targetHeight = height * (72 / 25.4); - } - } else { - [targetWidth, targetHeight] = PageSizes[targetSizeKey as keyof typeof PageSizes]; - } + const newPdfBytes = await fixPageSizeCore(pdfBytes, { + targetSize, + orientation, + scalingMode, + backgroundColor, + customWidth, + customHeight, + customUnits, + }); - if (orientation === 'landscape' && targetWidth < targetHeight) { - [targetWidth, targetHeight] = [targetHeight, targetWidth]; - } else if (orientation === 'portrait' && targetWidth > targetHeight) { - [targetWidth, targetHeight] = [targetHeight, targetWidth]; - } - - const arrayBuffer = await pageState.file.arrayBuffer(); - const sourceDoc = await PDFLibDocument.load(arrayBuffer); - const newDoc = await PDFLibDocument.create(); - - for (const sourcePage of sourceDoc.getPages()) { - const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize(); - const embeddedPage = await newDoc.embedPage(sourcePage); - - const newPage = newDoc.addPage([targetWidth, targetHeight]); - newPage.drawRectangle({ - x: 0, - y: 0, - width: targetWidth, - height: targetHeight, - color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b), - }); - - const scaleX = targetWidth / sourceWidth; - const scaleY = targetHeight / sourceHeight; - const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY); - - const scaledWidth = sourceWidth * scale; - const scaledHeight = sourceHeight * scale; - - const x = (targetWidth - scaledWidth) / 2; - const y = (targetHeight - scaledHeight) / 2; - - newPage.drawPage(embeddedPage, { - x, - y, - width: scaledWidth, - height: scaledHeight, - }); - } - - const newPdfBytes = await newDoc.save(); - downloadFile( - new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), - 'standardized.pdf' - ); - showAlert('Success', 'Page sizes standardized successfully!', 'success', () => { resetState(); }); - } catch (e) { - console.error(e); - showAlert('Error', 'An error occurred while standardizing pages.'); - } finally { - if (loaderModal) loaderModal.classList.add('hidden'); - } + downloadFile( + new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), + 'standardized.pdf' + ); + showAlert( + 'Success', + 'Page sizes standardized successfully!', + 'success', + () => { + resetState(); + } + ); + } catch (e) { + console.error(e); + showAlert('Error', 'An error occurred while standardizing pages.'); + } finally { + if (loaderModal) loaderModal.classList.add('hidden'); + } } document.addEventListener('DOMContentLoaded', function () { - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const processBtn = document.getElementById('process-btn'); - const backBtn = document.getElementById('back-to-tools'); - const targetSizeSelect = document.getElementById('target-size'); - const customSizeWrapper = document.getElementById('custom-size-wrapper'); + const fileInput = document.getElementById('file-input') as HTMLInputElement; + const dropZone = document.getElementById('drop-zone'); + const processBtn = document.getElementById('process-btn'); + const backBtn = document.getElementById('back-to-tools'); + const targetSizeSelect = document.getElementById('target-size'); + const customSizeWrapper = document.getElementById('custom-size-wrapper'); - if (backBtn) { - backBtn.addEventListener('click', function () { - window.location.href = import.meta.env.BASE_URL; + if (backBtn) { + backBtn.addEventListener('click', function () { + window.location.href = import.meta.env.BASE_URL; + }); + } + + // Setup custom size toggle + if (targetSizeSelect && customSizeWrapper) { + targetSizeSelect.addEventListener('change', function () { + customSizeWrapper.classList.toggle( + 'hidden', + (targetSizeSelect as HTMLSelectElement).value !== 'Custom' + ); + }); + } + + 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); + } + } + }); - // Setup custom size toggle - if (targetSizeSelect && customSizeWrapper) { - targetSizeSelect.addEventListener('change', function () { - customSizeWrapper.classList.toggle( - 'hidden', - (targetSizeSelect as HTMLSelectElement).value !== 'Custom' - ); - }); - } + fileInput.addEventListener('click', function () { + fileInput.value = ''; + }); + } - 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 = ''; - }); - } - - if (processBtn) { - processBtn.addEventListener('click', fixPageSize); - } + if (processBtn) { + processBtn.addEventListener('click', fixPageSize); + } }); diff --git a/src/js/logic/pdf-workflow-page.ts b/src/js/logic/pdf-workflow-page.ts index a6acaf4..c4ab6bb 100644 --- a/src/js/logic/pdf-workflow-page.ts +++ b/src/js/logic/pdf-workflow-page.ts @@ -1133,9 +1133,7 @@ function showNodeSettings(node: BaseWorkflowNode) { { label: 'Top Right', value: 'top-right' }, ], orientation: [ - { label: 'Vertical', value: 'vertical' }, - { label: 'Horizontal', value: 'horizontal' }, - { label: 'Auto', value: 'auto' }, + { label: 'Auto (Keep Original)', value: 'auto' }, { label: 'Portrait', value: 'portrait' }, { label: 'Landscape', value: 'landscape' }, ], @@ -1160,6 +1158,23 @@ function showNodeSettings(node: BaseWorkflowNode) { { label: 'Letter', value: 'letter' }, { label: 'Legal', value: 'legal' }, ], + targetSize: [ + { label: 'A4', value: 'A4' }, + { label: 'Letter', value: 'Letter' }, + { label: 'Legal', value: 'Legal' }, + { label: 'A3', value: 'A3' }, + { label: 'A5', value: 'A5' }, + { label: 'Tabloid', value: 'Tabloid' }, + { label: 'Custom', value: 'Custom' }, + ], + scalingMode: [ + { label: 'Fit (keep full page visible)', value: 'fit' }, + { label: 'Fill (cover full target page)', value: 'fill' }, + ], + customUnits: [ + { label: 'Millimeters (mm)', value: 'mm' }, + { label: 'Inches (in)', value: 'in' }, + ], numberFormat: [ { label: 'Simple (1, 2, 3)', value: 'simple' }, { label: 'Page X of Y', value: 'page_x_of_y' }, @@ -1294,6 +1309,9 @@ function showNodeSettings(node: BaseWorkflowNode) { text: ['text'], area: ['x0', 'y0', 'x1', 'y1'], }, + targetSize: { + Custom: ['customWidth', 'customHeight', 'customUnits'], + }, }; const controlWrappers: Record = {}; diff --git a/src/js/utils/pdf-operations.ts b/src/js/utils/pdf-operations.ts index 67b5f06..5b9a72b 100644 --- a/src/js/utils/pdf-operations.ts +++ b/src/js/utils/pdf-operations.ts @@ -1,4 +1,4 @@ -import { PDFDocument, degrees, rgb, StandardFonts } from 'pdf-lib'; +import { PDFDocument, degrees, rgb, StandardFonts, PageSizes } from 'pdf-lib'; export async function mergePdfs( pdfBytesList: Uint8Array[] @@ -438,3 +438,87 @@ export async function addPageNumbers( return new Uint8Array(await pdfDoc.save()); } + +export interface FixPageSizeOptions { + targetSize: string; + orientation: string; + scalingMode: string; + backgroundColor: { r: number; g: number; b: number }; + customWidth?: number; + customHeight?: number; + customUnits?: string; +} + +export async function fixPageSize( + pdfBytes: Uint8Array, + options: FixPageSizeOptions +): Promise { + let targetWidth: number; + let targetHeight: number; + + if (options.targetSize.toLowerCase() === 'custom') { + const w = options.customWidth ?? 210; + const h = options.customHeight ?? 297; + const units = (options.customUnits ?? 'mm').toLowerCase(); + if (units === 'in') { + targetWidth = w * 72; + targetHeight = h * 72; + } else { + targetWidth = w * (72 / 25.4); + targetHeight = h * (72 / 25.4); + } + } else { + const selected = + PageSizes[options.targetSize as keyof typeof PageSizes] || PageSizes.A4; + targetWidth = selected[0]; + targetHeight = selected[1]; + } + + const orientation = options.orientation.toLowerCase(); + if (orientation === 'landscape' && targetWidth < targetHeight) { + [targetWidth, targetHeight] = [targetHeight, targetWidth]; + } else if (orientation === 'portrait' && targetWidth > targetHeight) { + [targetWidth, targetHeight] = [targetHeight, targetWidth]; + } + + const sourceDoc = await PDFDocument.load(pdfBytes); + const outputDoc = await PDFDocument.create(); + + for (const sourcePage of sourceDoc.getPages()) { + const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize(); + const embeddedPage = await outputDoc.embedPage(sourcePage); + + const outputPage = outputDoc.addPage([targetWidth, targetHeight]); + outputPage.drawRectangle({ + x: 0, + y: 0, + width: targetWidth, + height: targetHeight, + color: rgb( + options.backgroundColor.r, + options.backgroundColor.g, + options.backgroundColor.b + ), + }); + + const scaleX = targetWidth / sourceWidth; + const scaleY = targetHeight / sourceHeight; + const useFill = options.scalingMode.toLowerCase() === 'fill'; + const scale = useFill ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY); + + const scaledWidth = sourceWidth * scale; + const scaledHeight = sourceHeight * scale; + + const x = (targetWidth - scaledWidth) / 2; + const y = (targetHeight - scaledHeight) / 2; + + outputPage.drawPage(embeddedPage, { + x, + y, + width: scaledWidth, + height: scaledHeight, + }); + } + + return new Uint8Array(await outputDoc.save()); +} diff --git a/src/js/workflow/nodes/fix-page-size-node.ts b/src/js/workflow/nodes/fix-page-size-node.ts new file mode 100644 index 0000000..7ae8581 --- /dev/null +++ b/src/js/workflow/nodes/fix-page-size-node.ts @@ -0,0 +1,104 @@ +import { ClassicPreset } from 'rete'; +import { BaseWorkflowNode } from './base-node'; +import { pdfSocket } from '../sockets'; +import type { SocketData } from '../types'; +import { requirePdfInput, processBatch } from '../types'; +import { fixPageSize } from '../../utils/pdf-operations'; +import { PDFDocument } from 'pdf-lib'; +import { hexToRgb } from '../../utils/helpers.js'; + +export class FixPageSizeNode extends BaseWorkflowNode { + readonly category = 'Organize & Manage' as const; + readonly icon = 'ph-frame-corners'; + readonly description = 'Standardize all pages to a target size'; + + constructor() { + super('Fix Page Size'); + this.addInput('pdf', new ClassicPreset.Input(pdfSocket, 'PDF')); + this.addOutput( + 'pdf', + new ClassicPreset.Output(pdfSocket, 'Standardized PDF') + ); + this.addControl( + 'targetSize', + new ClassicPreset.InputControl('text', { initial: 'A4' }) + ); + this.addControl( + 'orientation', + new ClassicPreset.InputControl('text', { initial: 'auto' }) + ); + this.addControl( + 'scalingMode', + new ClassicPreset.InputControl('text', { initial: 'fit' }) + ); + this.addControl( + 'backgroundColor', + new ClassicPreset.InputControl('text', { initial: '#ffffff' }) + ); + this.addControl( + 'customWidth', + new ClassicPreset.InputControl('number', { initial: 210 }) + ); + this.addControl( + 'customHeight', + new ClassicPreset.InputControl('number', { initial: 297 }) + ); + this.addControl( + 'customUnits', + new ClassicPreset.InputControl('text', { initial: 'mm' }) + ); + } + + async data( + inputs: Record + ): Promise> { + const pdfInputs = requirePdfInput(inputs, 'Fix Page Size'); + + const getText = (key: string, fallback: string) => { + const ctrl = this.controls[key] as + | ClassicPreset.InputControl<'text'> + | undefined; + return (ctrl?.value || fallback).trim(); + }; + + const getNum = (key: string, fallback: number) => { + const ctrl = this.controls[key] as + | ClassicPreset.InputControl<'number'> + | undefined; + const value = ctrl?.value; + return Number.isFinite(value) ? (value as number) : fallback; + }; + + const targetSize = getText('targetSize', 'A4'); + const orientation = getText('orientation', 'auto'); + const scalingMode = getText('scalingMode', 'fit'); + const backgroundHex = getText('backgroundColor', '#ffffff'); + const customWidth = Math.max(1, getNum('customWidth', 210)); + const customHeight = Math.max(1, getNum('customHeight', 297)); + const customUnits = getText('customUnits', 'mm'); + const backgroundColor = hexToRgb(backgroundHex); + + return { + pdf: await processBatch(pdfInputs, async (input) => { + const resultBytes = await fixPageSize(input.bytes, { + targetSize, + orientation, + scalingMode, + backgroundColor, + customWidth, + customHeight, + customUnits, + }); + + const resultDoc = await PDFDocument.load(resultBytes); + + return { + type: 'pdf', + document: resultDoc, + bytes: resultBytes, + filename: input.filename.replace(/\.pdf$/i, '_standardized.pdf'), + }; + }), + }; + } +} diff --git a/src/js/workflow/nodes/registry.ts b/src/js/workflow/nodes/registry.ts index 5f944ec..2520b16 100644 --- a/src/js/workflow/nodes/registry.ts +++ b/src/js/workflow/nodes/registry.ts @@ -13,6 +13,7 @@ import { ReversePagesNode } from './reverse-pages-node'; import { AddBlankPageNode } from './add-blank-page-node'; import { DividePagesNode } from './divide-pages-node'; import { NUpNode } from './n-up-node'; +import { FixPageSizeNode } from './fix-page-size-node'; import { CombineSinglePageNode } from './combine-single-page-node'; import { CropNode } from './crop-node'; import { GreyscaleNode } from './greyscale-node'; @@ -298,6 +299,13 @@ export const nodeRegistry: Record = { description: 'Arrange multiple pages per sheet', factory: () => new NUpNode(), }, + FixPageSizeNode: { + label: 'Fix Page Size', + category: 'Organize & Manage', + icon: 'ph-frame-corners', + description: 'Standardize all pages to a target size', + factory: () => new FixPageSizeNode(), + }, CombineSinglePageNode: { label: 'Combine to Single Page', category: 'Organize & Manage',