rspace-online/modules/rpubs/imposition.ts

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