printmaker
Version:
Generate PDF documents and from JavaScript objects
129 lines (115 loc) • 4.73 kB
text/typescript
import {
PDFPage,
PDFPageDrawImageOptions,
PDFPageDrawLineOptions,
PDFPageDrawRectangleOptions,
PDFPageDrawSVGOptions,
PDFPageDrawTextOptions,
} from 'pdf-lib';
import { Pos, Size } from './box.js';
import { Document } from './document.js';
import { ImageObject, LineObject, PolylineObject, RectObject } from './graphics.js';
import { renderGuide } from './guides.js';
import { AnchorObject, Frame, LinkObject, TextObject } from './layout.js';
import { createLinkAnnotation, createNamedDest } from './pdf-annotations.js';
export type Page = {
size: Size;
content: Frame;
header?: Frame;
footer?: Frame;
guides?: boolean;
pdfPage?: PDFPage;
};
export function renderPage(page: Page, doc: Document) {
page.pdfPage = doc.pdfDoc.addPage([page.size.width, page.size.height]);
renderFrame(page.content, page);
page.header && renderFrame(page.header, page);
page.footer && renderFrame(page.footer, page);
}
export function renderFrame(frame: Frame, page: Page, base: Pos = null) {
const { width, height } = frame;
const topLeft = { x: frame.x + (base?.x ?? 0), y: frame.y + (base?.y ?? 0) };
const bottomLeft = { x: topLeft.x, y: topLeft.y + height };
renderGuide(page, { ...tr(bottomLeft, page), width, height }, frame.type);
frame.objects?.forEach((object) => {
if (object.type === 'text') {
renderText(object, page, bottomLeft);
}
if (object.type === 'anchor') {
renderAnchor(object, page, topLeft);
}
if (object.type === 'link') {
renderLink(object, page, bottomLeft);
}
if (object.type === 'rect') {
renderRect(object, page, topLeft);
}
if (object.type === 'line') {
renderLine(object, page, topLeft);
}
if (object.type === 'polyline') {
renderPolyline(object, page, topLeft);
}
if (object.type === 'image') {
renderImage(object, page, topLeft);
}
});
frame.children?.forEach((frame) => {
renderFrame(frame, page, topLeft);
});
}
function renderText(el: TextObject, page: Page, base: Pos) {
const { x, y } = tr({ x: el.x + base.x, y: el.y + base.y }, page);
const options: PDFPageDrawTextOptions = { x, y, size: el.fontSize, font: el.font };
if (el.color) options.color = el.color;
page.pdfPage.drawText(el.text, options);
}
function renderAnchor(el: AnchorObject, page: Page, base: Pos) {
const { x, y } = tr({ x: el.x + base.x, y: el.y + base.y }, page);
createNamedDest(page.pdfPage, el.name, { x, y });
}
function renderLink(el: LinkObject, page: Page, base: Pos) {
const { x, y } = tr({ x: el.x + base.x, y: el.y + base.y }, page);
const { width, height, url } = el;
createLinkAnnotation(page.pdfPage, { x, y, width, height }, url);
}
function renderRect(rect: RectObject, page: Page, base: Pos) {
const { x, y } = tr({ x: rect.x + base.x, y: rect.y + base.y + rect.height }, page);
const { width, height } = rect;
const options: PDFPageDrawRectangleOptions = { x, y, width, height };
if ('strokeColor' in rect) options.borderColor = rect.strokeColor;
if ('strokeWidth' in rect) options.borderWidth = rect.strokeWidth;
if ('fillColor' in rect) options.color = rect.fillColor;
page.pdfPage.drawRectangle(options);
}
function renderLine(line: LineObject, page: Page, base: Pos) {
const options: PDFPageDrawLineOptions = {
start: tr({ x: line.x1 + base.x, y: line.y1 + base.y }, page),
end: tr({ x: line.x2 + base.x, y: line.y2 + base.y }, page),
};
if ('strokeWidth' in line) options.thickness = line.strokeWidth;
if ('strokeColor' in line) options.color = line.strokeColor;
page.pdfPage.drawLine(options);
}
function renderPolyline(polyline: PolylineObject, page: Page, base: Pos) {
const path = createSvgPath(polyline.points, polyline.closePath);
const { x, y } = tr(base, page);
const options: PDFPageDrawSVGOptions = { x, y };
if ('strokeColor' in polyline) options.borderColor = polyline.strokeColor;
if ('strokeWidth' in polyline) options.borderWidth = polyline.strokeWidth;
if ('fillColor' in polyline) options.color = polyline.fillColor;
page.pdfPage.drawSvgPath(path, options);
}
function renderImage(object: ImageObject, page: Page, base: Pos) {
const { x, y } = tr({ x: object.x + base.x, y: object.y + base.y + object.height }, page);
const { width, height } = object;
const options: PDFPageDrawImageOptions = { x, y, width, height };
page.pdfPage.drawImage(object.image, options);
}
function createSvgPath(points: { x: number; y: number }[], closePath = false): string {
const z = closePath ? ' Z' : '';
return points.reduce((s, p) => (s ? `${s} L` : `M`) + `${p.x},${p.y}`, '') + z;
}
function tr(pos: Pos, page: Page): Pos {
return { x: pos.x, y: page.size.height - pos.y };
}