printmaker
Version:
Generate PDF documents and from JavaScript objects
215 lines • 10.2 kB
JavaScript
import { parseEdges, parseLength, subtractEdges, ZERO_EDGES } from './box.js';
import { shiftGraphicsObject } from './graphics.js';
import { layoutColumns } from './layout-columns.js';
import { layoutImage } from './layout-image.js';
import { layoutRows } from './layout-rows.js';
import { parseBlock, parseContent, parseInheritableAttrs, } from './text.js';
import { breakLine, extractTextSegments, flattenTextSegments } from './text.js';
import { asArray, asObject, getFrom, optional, pickDefined, required } from './types.js';
const pageSize = { width: parseLength('210mm'), height: parseLength('297mm') }; // A4, portrait
const defaultPageMargin = parseEdges('2cm');
export function layoutPages(def, doc) {
var _a, _b;
const content = getFrom(def, 'content', required(asArray));
const pageMargin = (_a = getFrom(def, 'margin', optional(parseEdges))) !== null && _a !== void 0 ? _a : defaultPageMargin;
const defaultStyle = getFrom(def, 'defaultStyle', optional(parseInheritableAttrs));
const guides = (_b = getFrom(def, 'dev', optional(asObject))) === null || _b === void 0 ? void 0 : _b.guides;
const contentBox = subtractEdges(Object.assign({ x: 0, y: 0 }, pageSize), pageMargin);
const blocks = parseContent(content, defaultStyle);
const pages = [];
let remainingBlocks = blocks;
while (remainingBlocks === null || remainingBlocks === void 0 ? void 0 : remainingBlocks.length) {
const { frame, remainder } = layoutPageContent(remainingBlocks, contentBox, doc);
remainingBlocks = remainder;
pages.push({ size: pageSize, content: frame, guides });
}
pages.map((page, idx) => {
const pageInfo = { pageCount: pages.length, pageNumber: idx + 1, pageSize };
const parse = (block) => parseBlock(asObject(resolveFn(block, pageInfo)), defaultStyle);
const header = getFrom(def, 'header', optional(parse));
const footer = getFrom(def, 'footer', optional(parse));
page.header = header && layoutHeader(header, doc);
page.footer = header && layoutFooter(footer, doc);
});
return pages.map(pickDefined);
}
function resolveFn(value, ...args) {
if (typeof value !== 'function')
return value;
try {
return value(...args);
}
catch (error) {
throw new Error(`Function threw: ${error}`);
}
}
function layoutHeader(header, doc) {
const box = subtractEdges(Object.assign({ x: 0, y: 0 }, pageSize), header.margin);
return layoutBlock(header, box, doc);
}
function layoutFooter(footer, doc) {
var _a, _b;
const box = subtractEdges(Object.assign({ x: 0, y: 0 }, pageSize), footer.margin);
const frame = layoutBlock(footer, box, doc);
frame.y = (_b = pageSize.height - frame.height - ((_a = footer.margin) === null || _a === void 0 ? void 0 : _a.bottom)) !== null && _b !== void 0 ? _b : 0;
return frame;
}
export function layoutPageContent(blocks, box, doc) {
var _a;
const { x, y, width, height } = box;
const children = [];
const pos = { x: 0, y: 0 };
let lastMargin = 0;
let remainingHeight = height;
let remainder;
for (const [idx, block] of blocks.entries()) {
const margin = (_a = block.margin) !== null && _a !== void 0 ? _a : ZERO_EDGES;
const topMargin = Math.max(lastMargin, margin.top);
lastMargin = margin.bottom;
const nextPos = { x: pos.x + margin.left, y: pos.y + topMargin };
const maxSize = { width: width - margin.left - margin.right, height: remainingHeight };
const frame = layoutBlock(block, Object.assign(Object.assign({}, nextPos), maxSize), doc);
// If the first paragraph does not fit on the page, render it anyway.
// It wouldn't fit on the next page as well, ending in an endless loop.
if (remainingHeight < topMargin + frame.height && idx) {
remainder = blocks.slice(idx);
break;
}
children.push(frame);
pos.y += topMargin + frame.height;
remainingHeight = height - pos.y;
}
return { frame: { type: 'page', x, y, width, height, children }, remainder };
}
export function layoutBlock(block, box, doc) {
if (block.columns) {
return layoutColumns(block, box, doc);
}
if (block.rows) {
return layoutRows(block, box, doc);
}
if (block.image) {
return layoutImage(block, box, doc);
}
return layoutParagraph(block, box, doc);
}
export function layoutParagraph(paragraph, box, doc) {
var _a, _b, _c, _d;
const padding = (_a = paragraph.padding) !== null && _a !== void 0 ? _a : ZERO_EDGES;
const fixedWidth = paragraph.width;
const fixedHeight = paragraph.height;
const maxWidth = (fixedWidth !== null && fixedWidth !== void 0 ? fixedWidth : box.width) - padding.left - padding.right;
const maxHeight = (fixedHeight !== null && fixedHeight !== void 0 ? fixedHeight : box.height) - padding.top - padding.bottom;
const innerBox = { x: padding.left, y: padding.top, width: maxWidth, height: maxHeight };
const text = paragraph.text && layoutText(paragraph, innerBox, doc.fonts);
const graphics = paragraph.graphics && layoutGraphics(paragraph.graphics, innerBox);
const contentHeight = (_c = (_b = text === null || text === void 0 ? void 0 : text.size) === null || _b === void 0 ? void 0 : _b.height) !== null && _c !== void 0 ? _c : 0;
const objects = [
...(graphics !== null && graphics !== void 0 ? graphics : []),
...(paragraph.id ? [createAnchorObject(paragraph.id)] : []),
];
return Object.assign(Object.assign(Object.assign(Object.assign({ type: 'paragraph' }, box), { width: fixedWidth !== null && fixedWidth !== void 0 ? fixedWidth : box.width, height: fixedHeight !== null && fixedHeight !== void 0 ? fixedHeight : (contentHeight !== null && contentHeight !== void 0 ? contentHeight : 0) + padding.top + padding.bottom }), (((_d = text === null || text === void 0 ? void 0 : text.rows) === null || _d === void 0 ? void 0 : _d.length) ? { children: text.rows } : undefined)), (objects.length ? { objects } : undefined));
}
export function createAnchorObject(name, pos) {
var _a, _b;
return {
type: 'anchor',
name,
x: (_a = pos === null || pos === void 0 ? void 0 : pos.x) !== null && _a !== void 0 ? _a : 0,
y: (_b = pos === null || pos === void 0 ? void 0 : pos.y) !== null && _b !== void 0 ? _b : 0,
};
}
function layoutText(paragraph, box, fonts) {
const { text, textAlign } = paragraph;
const textSpans = text;
const segments = extractTextSegments(textSpans, fonts);
const rows = [];
let remainingSegments = segments;
const remainingSpace = Object.assign({}, box);
const size = { width: 0, height: 0 };
while (remainingSegments === null || remainingSegments === void 0 ? void 0 : remainingSegments.length) {
const { row, remainder } = layoutTextRow(remainingSegments, remainingSpace, textAlign);
rows.push(row);
remainingSegments = remainder;
remainingSpace.height -= row.height;
remainingSpace.y += row.height;
size.width = Math.max(size.width, row.width);
size.height += row.height;
}
return { rows, size };
}
function layoutTextRow(segments, box, textAlign) {
const [lineSegments, remainder] = breakLine(segments, box.width);
const pos = { x: 0, y: 0 };
const size = { width: 0, height: 0 };
let maxLineHeight = 0;
let maxDescent = 0;
const links = [];
const objects = [];
flattenTextSegments(lineSegments).forEach((seg) => {
const { text, width, height, lineHeight, font, fontSize, link, color } = seg;
const object = Object.assign(Object.assign({ type: 'text' }, pos), { text, font, fontSize, color });
objects.push(object);
if (link) {
links.push(Object.assign(Object.assign({ type: 'link' }, pos), { width, height, url: link }));
}
pos.x += width;
size.width += width;
size.height = Math.max(size.height, height);
maxDescent = Math.max(maxDescent, getDescent(font, fontSize));
maxLineHeight = Math.max(maxLineHeight, height * lineHeight);
});
objects.forEach((obj) => (obj.y -= maxDescent));
flattenLinks(links).forEach((link) => objects.push(link));
const row = Object.assign(Object.assign({ type: 'row' }, alignRow(box, size, textAlign)), { width: size.width, height: maxLineHeight, objects });
return { row, remainder };
}
function getDescent(font, fontSize) {
var _a;
const fontkitFont = font.embedder.font;
return Math.abs((((_a = fontkitFont.descent) !== null && _a !== void 0 ? _a : 0) * fontSize) / fontkitFont.unitsPerEm);
}
function layoutGraphics(graphics, pos) {
return graphics.map((object) => {
return shiftGraphicsObject(object, pos);
});
}
/**
* Merge adjacent link objects that point to the same target. Without this step, a link that
* consists of multiple text segments, e.g. because it includes normal and italic text, would be
* rendered as multiple independent links in the PDF. Example:
* ```js
* {text: ['foo', {text: 'bar', italic: true}], link: 'https://www.example.com'}
* ```
*/
function flattenLinks(links) {
const result = [];
let prev;
links.forEach((link) => {
if ((prev === null || prev === void 0 ? void 0 : prev.url) === link.url && (prev === null || prev === void 0 ? void 0 : prev.x) + (prev === null || prev === void 0 ? void 0 : prev.width) === link.x && (prev === null || prev === void 0 ? void 0 : prev.y) === link.y) {
prev.width += link.width;
prev.height = Math.max(prev.height, link.height);
}
else {
prev = link;
result.push(prev);
}
});
return result;
}
function alignRow(box, textSize, textAlign) {
if (textAlign === 'right') {
return {
x: box.x + box.width - textSize.width,
y: box.y,
};
}
if (textAlign === 'center') {
return {
x: box.x + (box.width - textSize.width) / 2,
y: box.y,
};
}
return { x: box.x, y: box.y };
}
//# sourceMappingURL=layout.js.map