/** * Saddle-stitch imposition generator using pdf-lib. * Reorders PDF pages for 2-up printing on A4/Letter sheets with fold marks. */ import { PDFDocument, rgb, LineCapStyle } from "pdf-lib"; const PARENT_SHEETS = { A4: { width: 595.28, height: 841.89 }, "US Letter": { width: 612, height: 792 }, }; const FORMAT_LAYOUT: Record = { a7: { parent: "A4", pagesPerSide: 2 }, a6: { parent: "A4", pagesPerSide: 2 }, "quarter-letter": { parent: "US Letter", pagesPerSide: 2 }, digest: { parent: "US Letter", pagesPerSide: 2 }, }; /** * Generate saddle-stitch signature page order. * For N pages (must be multiple of 4): * Sheet 1 front: [N-1, 0], back: [1, N-2] * Sheet 2 front: [N-3, 2], back: [3, N-4] etc. * Returns [leftPage, rightPage] pairs, alternating front/back. 0-indexed. */ function saddleStitchOrder(totalPages: number): [number, number][] { const pairs: [number, number][] = []; const sheets = totalPages / 4; for (let i = 0; i < sheets; i++) { const frontLeft = totalPages - 1 - 2 * i; const frontRight = 2 * i; pairs.push([frontLeft, frontRight]); const backLeft = 2 * i + 1; const backRight = totalPages - 2 - 2 * i; pairs.push([backLeft, backRight]); } return pairs; } function drawFoldMarks( page: ReturnType, parentWidth: number, parentHeight: number, ) { const markLen = 15; const markColor = rgb(0.7, 0.7, 0.7); const markWidth = 0.5; const cx = parentWidth / 2; page.drawLine({ start: { x: cx, y: parentHeight }, end: { x: cx, y: parentHeight - markLen }, thickness: markWidth, color: markColor, lineCap: LineCapStyle.Round, dashArray: [3, 3], }); page.drawLine({ start: { x: cx, y: 0 }, end: { x: cx, y: markLen }, thickness: markWidth, color: markColor, lineCap: LineCapStyle.Round, dashArray: [3, 3], }); } export async function generateImposition( pdfBuffer: Buffer | Uint8Array, formatId: string, ): Promise<{ pdf: Uint8Array; sheetCount: number; pageCount: number }> { const layout = FORMAT_LAYOUT[formatId]; if (!layout) throw new Error(`Imposition not supported for format: ${formatId}`); const srcDoc = await PDFDocument.load(pdfBuffer); const srcPages = srcDoc.getPages(); const srcPageCount = srcPages.length; const padded = Math.ceil(srcPageCount / 4) * 4; const impDoc = await PDFDocument.create(); const parent = PARENT_SHEETS[layout.parent]; const pairs = saddleStitchOrder(padded); for (const [leftIdx, rightIdx] of pairs) { const page = impDoc.addPage([parent.width, parent.height]); const bookPageWidth = parent.width / 2; const bookPageHeight = parent.height; if (leftIdx >= 0 && leftIdx < srcPageCount) { const [embedded] = await impDoc.embedPages([srcDoc.getPage(leftIdx)]); const srcW = srcPages[leftIdx].getWidth(); const srcH = srcPages[leftIdx].getHeight(); const scale = Math.min(bookPageWidth / srcW, bookPageHeight / srcH); const scaledW = srcW * scale; const scaledH = srcH * scale; page.drawPage(embedded, { x: (bookPageWidth - scaledW) / 2, y: (bookPageHeight - scaledH) / 2, width: scaledW, height: scaledH, }); } if (rightIdx >= 0 && rightIdx < srcPageCount) { const [embedded] = await impDoc.embedPages([srcDoc.getPage(rightIdx)]); const srcW = srcPages[rightIdx].getWidth(); const srcH = srcPages[rightIdx].getHeight(); const scale = Math.min(bookPageWidth / srcW, bookPageHeight / srcH); const scaledW = srcW * scale; const scaledH = srcH * scale; page.drawPage(embedded, { x: bookPageWidth + (bookPageWidth - scaledW) / 2, y: (bookPageHeight - scaledH) / 2, width: scaledW, height: scaledH, }); } drawFoldMarks(page, parent.width, parent.height); } const impBytes = await impDoc.save(); return { pdf: impBytes, sheetCount: pairs.length / 2, pageCount: srcPageCount, }; }