133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
/**
|
|
* 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<string, { parent: "A4" | "US Letter"; pagesPerSide: 2 }> = {
|
|
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<PDFDocument["addPage"]>,
|
|
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,
|
|
};
|
|
}
|