UNPKG

printmaker

Version:

Generate PDF documents and from JavaScript objects

215 lines 10.2 kB
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