import { upperFirst, capitalize, parseFloat as parseFloat$1, without, pick, compose, evolve, mapValues, matchPercent, castArray, isNil, omit, asyncCompose } from '@react-pdf/fns'; import * as P from '@react-pdf/primitives'; import resolveStyle, { transformColor, flatten } from '@react-pdf/stylesheet'; import layoutEngine, { fontSubstitution, wordHyphenation, scriptItemizer, textDecoration, justification, linebreaker, bidi, fromFragments } from '@react-pdf/textkit'; import * as Yoga from 'yoga-layout/load'; import { loadYoga as loadYoga$1 } from 'yoga-layout/load'; import emojiRegex from 'emoji-regex'; import resolveImage from '@react-pdf/image'; /** * Apply transformation to text string * * @param {string} text * @param {string} transformation type * @returns {string} transformed text */ const transformText = (text, transformation) => { switch (transformation) { case 'uppercase': return text.toUpperCase(); case 'lowercase': return text.toLowerCase(); case 'capitalize': return capitalize(text); case 'upperfirst': return upperFirst(text); default: return text; } }; const isTspan = (node) => node.type === P.Tspan; const isTextInstance$4 = (node) => node.type === P.TextInstance; const engines$1 = { bidi, linebreaker, justification, textDecoration, scriptItemizer, wordHyphenation, fontSubstitution, }; const engine$1 = layoutEngine(engines$1); const getFragments$1 = (fontStore, instance) => { if (!instance) return [{ string: '' }]; const fragments = []; const { fill = 'black', fontFamily = 'Helvetica', fontWeight, fontStyle, fontSize = 18, textDecorationColor, textDecorationStyle, textTransform, opacity, } = instance.props; const _textDecoration = instance.props.textDecoration; const fontFamilies = typeof fontFamily === 'string' ? [fontFamily] : [...(fontFamily || [])]; // Fallback font fontFamilies.push('Helvetica'); const font = fontFamilies.map((fontFamilyName) => { const opts = { fontFamily: fontFamilyName, fontWeight, fontStyle }; const obj = fontStore.getFont(opts); return obj?.data; }); const attributes = { font, opacity, fontSize, color: fill, underlineStyle: textDecorationStyle, underline: _textDecoration === 'underline' || _textDecoration === 'underline line-through' || _textDecoration === 'line-through underline', underlineColor: textDecorationColor || fill, strike: _textDecoration === 'line-through' || _textDecoration === 'underline line-through' || _textDecoration === 'line-through underline', strikeStyle: textDecorationStyle, strikeColor: textDecorationColor || fill, }; for (let i = 0; i < instance.children.length; i += 1) { const child = instance.children[i]; if (isTextInstance$4(child)) { fragments.push({ string: transformText(child.value, textTransform), attributes, }); } else if (child) { fragments.push(...getFragments$1(fontStore, child)); } } return fragments; }; const getAttributedString$1 = (fontStore, instance) => fromFragments(getFragments$1(fontStore, instance)); const AlmostInfinity = 999999999999; const shrinkWhitespaceFactor = { before: -0.5, after: -0.5 }; const layoutTspan = (fontStore) => (node, xOffset) => { const attributedString = getAttributedString$1(fontStore, node); const x = node.props.x === undefined ? xOffset : node.props.x; const y = node.props?.y || 0; const container = { x, y, width: AlmostInfinity, height: AlmostInfinity }; const hyphenationCallback = node.props.hyphenationCallback || fontStore?.getHyphenationCallback() || null; const layoutOptions = { hyphenationCallback, shrinkWhitespaceFactor }; const lines = engine$1(attributedString, container, layoutOptions).flat(); return Object.assign({}, node, { lines }); }; // Consecutive TSpan elements should be joined with a space const joinTSpanLines = (node) => { const children = node.children.map((child, index) => { if (!isTspan(child)) return child; const textInstance = child.children[0]; if (child.props.x === undefined && index < node.children.length - 1 && textInstance?.value) { return Object.assign({}, child, { children: [{ ...textInstance, value: `${textInstance.value} ` }], }); } return child; }, []); return Object.assign({}, node, { children }); }; const layoutText$1 = (fontStore, node) => { if (!node.children) return node; let currentXOffset = node.props?.x || 0; const layoutFn = layoutTspan(fontStore); const joinedNode = joinTSpanLines(node); const children = joinedNode.children.map((child) => { const childWithLayout = layoutFn(child, currentXOffset); currentXOffset += childWithLayout.lines[0].xAdvance; return childWithLayout; }); return Object.assign({}, node, { children }); }; const isDefs = (node) => node.type === P.Defs; const getDefs = (node) => { const children = node.children || []; const defs = children.find(isDefs); const values = defs?.children || []; return values.reduce((acc, value) => { const id = value.props?.id; if (id) acc[id] = value; return acc; }, {}); }; const isNotDefs = (node) => node.type !== P.Defs; const detachDefs = (node) => { if (!node.children) return node; const children = node.children.filter(isNotDefs); return Object.assign({}, node, { children }); }; const URL_REGEX = /url\(['"]?#([^'"]+)['"]?\)/; const replaceDef = (defs, value) => { if (!value) return undefined; if (!URL_REGEX.test(value)) return value; const match = value.match(URL_REGEX); return defs[match[1]]; }; const parseNodeDefs = (defs) => (node) => { const props = node.props; const fill = `fill` in props ? replaceDef(defs, props?.fill) : undefined; const clipPath = `clipPath` in props ? replaceDef(defs, props?.clipPath) : undefined; const newProps = Object.assign({}, node.props, { fill, clipPath }); const children = node.children ? node.children.map(parseNodeDefs(defs)) : undefined; return Object.assign({}, node, { props: newProps, children }); }; const parseDefs = (root) => { if (!root.children) return root; const defs = getDefs(root); const children = root.children.map(parseNodeDefs(defs)); return Object.assign({}, root, { children }); }; const replaceDefs = (node) => { return detachDefs(parseDefs(node)); }; const parseViewbox = (value) => { if (!value) return null; if (typeof value !== 'string') return value; const values = value.split(/[,\s]+/).map(parseFloat$1); if (values.length !== 4) return null; return { minX: values[0], minY: values[1], maxX: values[2], maxY: values[3] }; }; const getContainer$1 = (node) => { const viewbox = parseViewbox(node.props.viewBox); if (viewbox) { return { width: viewbox.maxX, height: viewbox.maxY }; } if (node.props.width && node.props.height) { return { width: parseFloat$1(node.props.width), height: parseFloat$1(node.props.height), }; } return { width: 0, height: 0 }; }; const BASE_SVG_INHERITED_PROPS = [ 'x', 'y', 'clipPath', 'clipRule', 'opacity', 'fill', 'fillOpacity', 'fillRule', 'stroke', 'strokeLinecap', 'strokeLinejoin', 'strokeOpacity', 'strokeWidth', 'textAnchor', 'dominantBaseline', 'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'letterSpacing', 'opacity', 'textDecoration', 'lineHeight', 'textAlign', 'visibility', 'wordSpacing', ]; // Do not inherit "x" for elements from parent const TEXT_SVG_INHERITED_PROPS = without(['x'], BASE_SVG_INHERITED_PROPS); const SVG_INHERITED_PROPS = { [P.Text]: TEXT_SVG_INHERITED_PROPS, }; const getInheritProps = (node) => { const props = node.props || {}; const svgInheritedProps = SVG_INHERITED_PROPS[node.type] ?? BASE_SVG_INHERITED_PROPS; return pick(svgInheritedProps, props); }; const inheritProps = (node) => { if (!node.children) return node; const inheritedProps = getInheritProps(node); const children = node.children.map((child) => { const props = Object.assign({}, inheritedProps, child.props || {}); const newChild = Object.assign({}, child, { props }); return inheritProps(newChild); }); return Object.assign({}, node, { children }); }; const parseAspectRatio = (value) => { if (typeof value !== 'string') return value; const match = value .replace(/[\s\r\t\n]+/gm, ' ') .replace(/^defer\s/, '') .split(' '); const align = (match[0] || 'xMidYMid'); const meetOrSlice = (match[1] || 'meet'); return { align, meetOrSlice }; }; const STYLE_PROPS = [ 'width', 'height', 'color', 'stroke', 'strokeWidth', 'opacity', 'fillOpacity', 'strokeOpacity', 'fill', 'fillRule', 'clipPath', 'offset', 'transform', 'strokeLinejoin', 'strokeLinecap', 'strokeDasharray', 'gradientUnits', 'gradientTransform', ]; const VERTICAL_PROPS = ['y', 'y1', 'y2', 'height', 'cy', 'ry']; const HORIZONTAL_PROPS = ['x', 'x1', 'x2', 'width', 'cx', 'rx']; const isSvg$3 = (node) => node.type === P.Svg; const isText$5 = (node) => node.type === P.Text; const isTextInstance$3 = (node) => node.type === P.TextInstance; const transformPercent = (container) => (props) => mapValues(props, (value, key) => { const match = matchPercent(value); if (match && VERTICAL_PROPS.includes(key)) { return match.percent * container.height; } if (match && HORIZONTAL_PROPS.includes(key)) { return match.percent * container.width; } return value; }); const parsePercent = (value) => { const match = matchPercent(value); return match ? match.percent : parseFloat$1(value); }; const parseTransform = (container) => (value) => { return resolveStyle(container, { transform: value }).transform; }; const parseProps = (container) => (node) => { let props = transformPercent(container)(node.props); props = evolve({ x: parseFloat$1, x1: parseFloat$1, x2: parseFloat$1, y: parseFloat$1, y1: parseFloat$1, y2: parseFloat$1, r: parseFloat$1, rx: parseFloat$1, ry: parseFloat$1, cx: parseFloat$1, cy: parseFloat$1, width: parseFloat$1, height: parseFloat$1, offset: parsePercent, fill: transformColor, opacity: parsePercent, stroke: transformColor, stopOpacity: parsePercent, stopColor: transformColor, transform: parseTransform(container), gradientTransform: parseTransform(container), }, props); return Object.assign({}, node, { props }); }; const mergeStyles$1 = (node) => { const style = node.style || {}; const props = Object.assign({}, style, node.props); return Object.assign({}, node, { props }); }; const removeNoneValues = (node) => { const removeNone = (value) => (value === 'none' ? null : value); const props = mapValues(node.props, removeNone); return Object.assign({}, node, { props }); }; const pickStyleProps = (node) => { const props = node.props || {}; const styleProps = pick(STYLE_PROPS, props); const style = Object.assign({}, styleProps, node.style || {}); return Object.assign({}, node, { style }); }; const parseSvgProps = (node) => { const props = evolve({ width: parseFloat$1, height: parseFloat$1, viewBox: parseViewbox, preserveAspectRatio: parseAspectRatio, }, node.props); return Object.assign({}, node, { props }); }; const wrapBetweenTspan = (node) => ({ type: P.Tspan, props: {}, style: {}, children: [node], }); const addMissingTspan = (node) => { if (!isText$5(node)) return node; if (!node.children) return node; const resolveChild = (child) => isTextInstance$3(child) ? wrapBetweenTspan(child) : child; const children = node.children.map(resolveChild); return Object.assign({}, node, { children }); }; const parseText = (fontStore) => (node) => { if (isText$5(node)) return layoutText$1(fontStore, node); if (!node.children) return node; const children = node.children.map(parseText(fontStore)); return Object.assign({}, node, { children }); }; const resolveSvgNode = (container) => compose(parseProps(container), addMissingTspan, removeNoneValues, mergeStyles$1); const resolveChildren = (container) => (node) => { if (!node.children) return node; const resolveChild = compose(resolveChildren(container), resolveSvgNode(container)); const children = node.children.map(resolveChild); return Object.assign({}, node, { children }); }; const buildXLinksIndex = (node) => { const idIndex = {}; const listToExplore = node.children?.slice(0) || []; while (listToExplore.length > 0) { const child = listToExplore.shift(); if (child.props && 'id' in child.props) { idIndex[child.props.id] = child; } if (child.children) listToExplore.push(...child.children); } return idIndex; }; const replaceXLinks = (node, idIndex) => { if (node.props && 'xlinkHref' in node.props) { const linkedNode = idIndex[node.props.xlinkHref.replace(/^#/, '')]; // No node to extend from if (!linkedNode) return node; const newProps = Object.assign({}, linkedNode.props, node.props); delete newProps.xlinkHref; return Object.assign({}, linkedNode, { props: newProps }); } const children = node.children?.map((child) => replaceXLinks(child, idIndex)); return Object.assign({}, node, { children }); }; const resolveXLinks = (node) => { const idIndex = buildXLinksIndex(node); return replaceXLinks(node, idIndex); }; const resolveSvgRoot = (node, fontStore) => { const container = getContainer$1(node); return compose(replaceDefs, parseText(fontStore), parseSvgProps, pickStyleProps, inheritProps, resolveChildren(container), resolveXLinks)(node); }; /** * Pre-process SVG nodes so they can be rendered in the next steps * * @param node - Root node * @param fontStore - Font store * @returns Root node */ const resolveSvg = (node, fontStore) => { if (!('children' in node)) return node; const resolveChild = (child) => resolveSvg(child, fontStore); const root = isSvg$3(node) ? resolveSvgRoot(node, fontStore) : node; const children = root.children?.map(resolveChild); return Object.assign({}, root, { children }); }; let instancePromise; const loadYoga = async () => { // Yoga WASM binaries must be asynchronously compiled and loaded // to prevent Event emitter memory leak warnings, Yoga must be loaded only once const instance = await (instancePromise ??= loadYoga$1()); const config = instance.Config.create(); config.setPointScaleFactor(0); const node = { create: () => instance.Node.createWithConfig(config) }; return { node }; }; const resolveYoga = async (root) => { const yoga = await loadYoga(); return Object.assign({}, root, { yoga }); }; const getZIndex = (node) => node.style.zIndex; const shouldSort = (node) => node.type !== P.Document && node.type !== P.Svg; const sortZIndex = (a, b) => { const za = getZIndex(a); const zb = getZIndex(b); if (!za && !zb) return 0; if (!za) return 1; if (!zb) return -1; return zb - za; }; /** * Sort children by zIndex value * * @param node * @returns Node */ const resolveNodeZIndex = (node) => { if (!node.children) return node; const sortedChildren = shouldSort(node) ? node.children.sort(sortZIndex) : node.children; const children = sortedChildren.map(resolveNodeZIndex); return Object.assign({}, node, { children }); }; /** * Sort children by zIndex value * * @param node * @returns Node */ const resolveZIndex = (root) => resolveNodeZIndex(root); // Caches emoji images data const emojis = {}; const regex = emojiRegex(); /** * When an emoji as no variations, it might still have 2 parts, * the canonical emoji and an empty string. * ex. * (no color) Array.from('❤️') => ["❤", "️"] * (w/ color) Array.from('👍🏿') => ["👍", "🏿"] * * The empty string needs to be removed otherwise the generated * url will be incorect. */ const removeVariationSelectors = (x) => x !== '️'; const getCodePoints = (string, withVariationSelectors = false) => Array.from(string) .filter(withVariationSelectors ? () => true : removeVariationSelectors) .map((char) => char.codePointAt(0).toString(16)) .join('-'); const buildEmojiUrl = (emoji, source) => { if ('builder' in source) { return source.builder(getCodePoints(emoji, source.withVariationSelectors)); } const { url, format = 'png', withVariationSelectors } = source; return `${url}${getCodePoints(emoji, withVariationSelectors)}.${format}`; }; const fetchEmojis = (string, source) => { if (!source) return []; const promises = []; Array.from(string.matchAll(regex)).forEach((match) => { const emoji = match[0]; if (!emojis[emoji] || emojis[emoji].loading) { const emojiUrl = buildEmojiUrl(emoji, source); emojis[emoji] = { loading: true }; promises.push(resolveImage({ uri: emojiUrl }).then((image) => { emojis[emoji].loading = false; emojis[emoji].data = image.data; })); } }); return promises; }; const embedEmojis = (fragments) => { const result = []; for (let i = 0; i < fragments.length; i += 1) { const fragment = fragments[i]; let lastIndex = 0; Array.from(fragment.string.matchAll(regex)).forEach((match) => { const { index } = match; const emoji = match[0]; const emojiSize = fragment.attributes.fontSize; const chunk = fragment.string.slice(lastIndex, index + match[0].length); // If emoji image was found, we create a new fragment with the // correct attachment and object substitution character; if (emojis[emoji] && emojis[emoji].data) { result.push({ string: chunk.replace(match[0], String.fromCharCode(0xfffc)), attributes: { ...fragment.attributes, attachment: { width: emojiSize, height: emojiSize, yOffset: Math.floor(emojiSize * 0.1), image: emojis[emoji].data, }, }, }); } else { // If no emoji data, we try to use emojis in the font result.push({ string: chunk, attributes: fragment.attributes, }); } lastIndex = index + emoji.length; }); if (lastIndex < fragment.string.length) { result.push({ string: fragment.string.slice(lastIndex), attributes: fragment.attributes, }); } } return result; }; /** * Get image source * * @param node - Image node * @returns Image src */ const getSource = (node) => { if (node.props.src) return node.props.src; if (node.props.source) return node.props.source; }; /** * Resolves `src` to `@react-pdf/image` interface. * * Also it handles factories and async sources. * * @param src * @returns Resolved src */ const resolveSource = async (src) => { const source = typeof src === 'function' ? await src() : await src; return typeof source === 'string' ? { uri: source } : source; }; /** * Fetches image and append data to node * Ideally this fn should be immutable. * * @param node */ const fetchImage = async (node) => { const src = getSource(node); const { cache } = node.props; if (!src) { console.warn(false, 'Image should receive either a "src" or "source" prop'); return; } try { const source = await resolveSource(src); if (!source) { throw new Error(`Image's "src" or "source" prop returned ${source}`); } node.image = await resolveImage(source, { cache }); if (Buffer.isBuffer(source) || source instanceof Blob) return; node.image.key = 'data' in source ? source.data.toString() : source.uri; } catch (e) { console.warn(e.message); } }; const isImage$2 = (node) => node.type === P.Image; /** * Get all asset promises that need to be resolved * * @param fontStore - Font store * @param node - Root node * @returns Asset promises */ const fetchAssets = (fontStore, node) => { const promises = []; const listToExplore = node.children?.slice(0) || []; const emojiSource = fontStore ? fontStore.getEmojiSource() : null; while (listToExplore.length > 0) { const n = listToExplore.shift(); if (isImage$2(n)) { promises.push(fetchImage(n)); } if (fontStore && n.style?.fontFamily) { const fontFamilies = castArray(n.style.fontFamily); promises.push(...fontFamilies.map((fontFamily) => fontStore.load({ fontFamily, fontStyle: n.style.fontStyle, fontWeight: n.style.fontWeight, }))); } if (typeof n === 'string') { promises.push(...fetchEmojis(n, emojiSource)); } if ('value' in n && typeof n.value === 'string') { promises.push(...fetchEmojis(n.value, emojiSource)); } if (n.children) { n.children.forEach((childNode) => { listToExplore.push(childNode); }); } } return promises; }; /** * Fetch image, font and emoji assets in parallel. * Layout process will not be resumed until promise resolves. * * @param node root node * @param fontStore font store * @returns Root node */ const resolveAssets = async (node, fontStore) => { const promises = fetchAssets(fontStore, node); await Promise.all(promises); return node; }; const isLink$1 = (node) => node.type === P.Link; const DEFAULT_LINK_STYLES = { color: 'blue', textDecoration: 'underline', }; /** * Computes styles using stylesheet * * @param container * @param node - Document node * @returns Computed styles */ const computeStyle = (container, node) => { let baseStyle = [node.style]; if (isLink$1(node)) { baseStyle = Array.isArray(node.style) ? [DEFAULT_LINK_STYLES, ...node.style] : [DEFAULT_LINK_STYLES, node.style]; } return resolveStyle(container, baseStyle); }; /** * Resolves node styles * * @param container * @returns Resolve node styles */ const resolveNodeStyles = (container) => (node) => { const style = computeStyle(container, node); if (!node.children) return Object.assign({}, node, { style }); const children = node.children.map(resolveNodeStyles(container)); return Object.assign({}, node, { style, children }); }; /** * Resolves page styles * * @param page Document page * @returns Document page with resolved styles */ const resolvePageStyles = (page) => { const dpi = page.props?.dpi || 72; const style = page.style; const width = page.box?.width || style.width; const height = page.box?.height || style.height; const orientation = page.props?.orientation || 'portrait'; const remBase = style?.fontSize || 18; const container = { width, height, orientation, dpi, remBase }; return resolveNodeStyles(container)(page); }; /** * Resolves document styles * * @param root - Document root * @returns Document root with resolved styles */ const resolveStyles = (root) => { if (!root.children) return root; const children = root.children.map(resolvePageStyles); return Object.assign({}, root, { children }); }; const getTransformStyle = (s) => (node) => isNil(node.style?.[s]) ? '50%' : node.style?.[s] ?? null; /** * Get node origin * * @param node * @returns {{ left?: number, top?: number }} node origin */ const getOrigin = (node) => { if (!node.box) return null; const { left, top, width, height } = node.box; const transformOriginX = getTransformStyle('transformOriginX')(node); const transformOriginY = getTransformStyle('transformOriginY')(node); const percentX = matchPercent(transformOriginX); const percentY = matchPercent(transformOriginY); const offsetX = percentX ? width * percentX.percent : transformOriginX; const offsetY = percentY ? height * percentY.percent : transformOriginY; if (isNil(offsetX) || typeof offsetX === 'string') throw new Error(`Invalid origin offsetX: ${offsetX}`); if (isNil(offsetY) || typeof offsetY === 'string') throw new Error(`Invalid origin offsetY: ${offsetY}`); return { left: left + offsetX, top: top + offsetY }; }; /** * Resolve node origin * * @param node * @returns Node with origin attribute */ const resolveNodeOrigin = (node) => { const origin = getOrigin(node); const newNode = Object.assign({}, node, { origin }); if (!node.children) return newNode; const children = node.children.map(resolveNodeOrigin); return Object.assign({}, newNode, { children }); }; /** * Resolve document origins * * @param root - Document root * @returns Document root */ const resolveOrigin = (root) => { if (!root.children) return root; const children = root.children.map(resolveNodeOrigin); return Object.assign({}, root, { children }); }; const getBookmarkValue = (bookmark) => { return typeof bookmark === 'string' ? { title: bookmark, fit: false, expanded: false } : bookmark; }; const resolveBookmarks = (node) => { let refs = 0; const children = (node.children || []).slice(0); const listToExplore = children.map((value) => ({ value, parent: null, })); while (listToExplore.length > 0) { const element = listToExplore.shift(); if (!element) break; const child = element.value; let parent = element.parent; if (child.props && 'bookmark' in child.props) { const bookmark = getBookmarkValue(child.props.bookmark); const ref = refs++; const newHierarchy = { ref, parent: parent?.ref, ...bookmark }; child.props.bookmark = newHierarchy; parent = newHierarchy; } if (child.children) { child.children.forEach((childNode) => { listToExplore.push({ value: childNode, parent }); }); } } return node; }; const VALID_ORIENTATIONS = ['portrait', 'landscape']; /** * Get page orientation. Defaults to portrait * * @param page - Page object * @returns Page orientation */ const getOrientation = (page) => { const value = page.props?.orientation || 'portrait'; return VALID_ORIENTATIONS.includes(value) ? value : 'portrait'; }; /** * Return true if page is landscape * * @param page - Page instance * @returns Is page landscape */ const isLandscape = (page) => getOrientation(page) === 'landscape'; // Page sizes for 72dpi. 72dpi is used internally by pdfkit. const PAGE_SIZES = { '4A0': [4767.87, 6740.79], '2A0': [3370.39, 4767.87], A0: [2383.94, 3370.39], A1: [1683.78, 2383.94], A2: [1190.55, 1683.78], A3: [841.89, 1190.55], A4: [595.28, 841.89], A5: [419.53, 595.28], A6: [297.64, 419.53], A7: [209.76, 297.64], A8: [147.4, 209.76], A9: [104.88, 147.4], A10: [73.7, 104.88], B0: [2834.65, 4008.19], B1: [2004.09, 2834.65], B2: [1417.32, 2004.09], B3: [1000.63, 1417.32], B4: [708.66, 1000.63], B5: [498.9, 708.66], B6: [354.33, 498.9], B7: [249.45, 354.33], B8: [175.75, 249.45], B9: [124.72, 175.75], B10: [87.87, 124.72], C0: [2599.37, 3676.54], C1: [1836.85, 2599.37], C2: [1298.27, 1836.85], C3: [918.43, 1298.27], C4: [649.13, 918.43], C5: [459.21, 649.13], C6: [323.15, 459.21], C7: [229.61, 323.15], C8: [161.57, 229.61], C9: [113.39, 161.57], C10: [79.37, 113.39], RA0: [2437.8, 3458.27], RA1: [1729.13, 2437.8], RA2: [1218.9, 1729.13], RA3: [864.57, 1218.9], RA4: [609.45, 864.57], SRA0: [2551.18, 3628.35], SRA1: [1814.17, 2551.18], SRA2: [1275.59, 1814.17], SRA3: [907.09, 1275.59], SRA4: [637.8, 907.09], EXECUTIVE: [521.86, 756.0], FOLIO: [612.0, 936.0], LEGAL: [612.0, 1008.0], LETTER: [612.0, 792.0], TABLOID: [792.0, 1224.0], ID1: [153, 243], }; /** * Parses scalar value in value and unit pairs * * @param value - Scalar value * @returns Parsed value */ const parseValue = (value) => { if (typeof value === 'number') return { value, unit: undefined }; const match = /^(-?\d*\.?\d+)(in|mm|cm|pt|px)?$/g.exec(value); return match ? { value: parseFloat(match[1]), unit: match[2] || 'pt' } : { value, unit: undefined }; }; /** * Transform given scalar value to 72dpi equivalent of size * * @param value - Styles value * @param inputDpi - User defined dpi * @returns Transformed value */ const transformUnit = (value, inputDpi) => { if (!value) return 0; const scalar = parseValue(value); const outputDpi = 72; const mmFactor = (1 / 25.4) * outputDpi; const cmFactor = (1 / 2.54) * outputDpi; if (typeof scalar.value === 'string') throw new Error(`Invalid page size: ${value}`); switch (scalar.unit) { case 'in': return scalar.value * outputDpi; case 'mm': return scalar.value * mmFactor; case 'cm': return scalar.value * cmFactor; case 'px': return Math.round(scalar.value * (outputDpi / inputDpi)); default: return scalar.value; } }; const transformUnits = ({ width, height }, dpi) => ({ width: transformUnit(width, dpi), height: transformUnit(height, dpi), }); /** * Transforms array into size object * * @param v - Values array * @returns Size object with width and height */ const toSizeObject = (v) => ({ width: v[0], height: v[1], }); /** * Flip size object * * @param v - Size object * @returns Flipped size object */ const flipSizeObject = (v) => ({ width: v.height, height: v.width, }); /** * Returns size object from a given string * * @param v - Page size string * @returns Size object with width and height */ const getStringSize = (v) => { return toSizeObject(PAGE_SIZES[v.toUpperCase()]); }; /** * Returns size object from a single number * * @param n - Page size number * @returns Size object with width and height */ const getNumberSize = (n) => toSizeObject([n, n]); /** * Return page size in an object { width, height } * * @param page - Page node * @returns Size object with width and height */ const getSize = (page) => { const value = page.props?.size || 'A4'; const dpi = page.props?.dpi || 72; let size; if (typeof value === 'string') { size = getStringSize(value); } else if (Array.isArray(value)) { size = transformUnits(toSizeObject(value), dpi); } else if (typeof value === 'number') { size = transformUnits(getNumberSize(value), dpi); } else { size = transformUnits(value, dpi); } return isLandscape(page) ? flipSizeObject(size) : size; }; /** * Resolves page size * * @param page * @returns Page with resolved size in style attribute */ const resolvePageSize = (page) => { const size = getSize(page); const style = flatten(page.style || {}); return { ...page, style: { ...style, ...size } }; }; /** * Resolves page sizes * * @param root -Document root * @returns Document root with resolved page sizes */ const resolvePageSizes = (root) => { if (!root.children) return root; const children = root.children.map(resolvePageSize); return Object.assign({}, root, { children }); }; const isFixed = (node) => { if (!node.props) return false; return 'fixed' in node.props ? node.props.fixed === true : false; }; /** * Get line index at given height * * @param node * @param height */ const lineIndexAtHeight = (node, height) => { let y = 0; if (!node.lines) return 0; for (let i = 0; i < node.lines.length; i += 1) { const line = node.lines[i]; if (y + line.box.height > height) return i; y += line.box.height; } return node.lines.length; }; /** * Get height for given text line index * * @param node * @param index */ const heightAtLineIndex = (node, index) => { let counter = 0; if (!node.lines) return counter; for (let i = 0; i < index; i += 1) { const line = node.lines[i]; if (!line) break; counter += line.box.height; } return counter; }; const getLineBreak = (node, height) => { const top = node.box?.top || 0; const widows = node.props.widows || 2; const orphans = node.props.orphans || 2; const linesQuantity = node.lines.length; const slicedLine = lineIndexAtHeight(node, height - top); if (slicedLine === 0) { return 0; } if (linesQuantity < orphans) { return linesQuantity; } if (slicedLine < orphans || linesQuantity < orphans + widows) { return 0; } if (linesQuantity === orphans + widows) { return orphans; } if (linesQuantity - slicedLine < widows) { return linesQuantity - widows; } return slicedLine; }; // Also receives contentArea in case it's needed const splitText = (node, height) => { const slicedLineIndex = getLineBreak(node, height); const currentHeight = heightAtLineIndex(node, slicedLineIndex); const nextHeight = node.box.height - currentHeight; const current = Object.assign({}, node, { box: { ...node.box, height: currentHeight, borderBottomWidth: 0, }, style: { ...node.style, marginBottom: 0, paddingBottom: 0, borderBottomWidth: 0, borderBottomLeftRadius: 0, borderBottomRightRadius: 0, }, lines: node.lines.slice(0, slicedLineIndex), }); const next = Object.assign({}, node, { box: { ...node.box, top: 0, height: nextHeight, borderTopWidth: 0, }, style: { ...node.style, marginTop: 0, paddingTop: 0, borderTopWidth: 0, borderTopLeftRadius: 0, borderTopRightRadius: 0, }, lines: node.lines.slice(slicedLineIndex), }); return [current, next]; }; const getTop$1 = (node) => node.box?.top || 0; const hasFixedHeight = (node) => !isNil(node.style?.height); const splitNode = (node, height) => { if (!node) return [null, null]; const nodeTop = getTop$1(node); const current = Object.assign({}, node, { box: { ...node.box, borderBottomWidth: 0, }, style: { ...node.style, marginBottom: 0, paddingBottom: 0, borderBottomWidth: 0, borderBottomLeftRadius: 0, borderBottomRightRadius: 0, }, }); current.style.height = height - nodeTop; const nextHeight = hasFixedHeight(node) ? node.box.height - (height - nodeTop) : null; const next = Object.assign({}, node, { box: { ...node.box, top: 0, borderTopWidth: 0, }, style: { ...node.style, marginTop: 0, paddingTop: 0, borderTopWidth: 0, borderTopLeftRadius: 0, borderTopRightRadius: 0, }, }); if (nextHeight) { next.style.height = nextHeight; } return [current, next]; }; const NON_WRAP_TYPES = [P.Svg, P.Note, P.Image, P.Canvas]; const getWrap = (node) => { if (NON_WRAP_TYPES.includes(node.type)) return false; if (!node.props) return true; return 'wrap' in node.props ? node.props.wrap : true; }; const getComputedPadding = (node, edge) => { const { yogaNode } = node; return yogaNode ? yogaNode.getComputedPadding(edge) : null; }; /** * Get Yoga computed paddings. Zero otherwise * * @param node * @returns paddings */ const getPadding = (node) => { const { style, box } = node; const paddingTop = getComputedPadding(node, Yoga.Edge.Top) || box?.paddingTop || style?.paddingTop || 0; const paddingRight = getComputedPadding(node, Yoga.Edge.Right) || box?.paddingRight || style?.paddingRight || 0; const paddingBottom = getComputedPadding(node, Yoga.Edge.Bottom) || box?.paddingBottom || style?.paddingBottom || 0; const paddingLeft = getComputedPadding(node, Yoga.Edge.Left) || box?.paddingLeft || style?.paddingLeft || 0; return { paddingTop, paddingRight, paddingBottom, paddingLeft }; }; const getWrapArea = (page) => { const height = page.style?.height; const { paddingBottom } = getPadding(page); return height - paddingBottom; }; const getContentArea = (page) => { const height = page.style?.height; const { paddingTop, paddingBottom } = getPadding(page); return height - paddingBottom - paddingTop; }; const isString = (value) => typeof value === 'string'; const isNumber = (value) => typeof value === 'number'; const isBoolean = (value) => typeof value === 'boolean'; const isFragment = (value) => value && value.type === Symbol.for('react.fragment'); /** * Transforms a react element instance to internal element format. * * Can return multiple instances in the case of arrays or fragments. * * @param element - React element * @returns Parsed React elements */ const createInstances = (element) => { if (!element) return []; if (Array.isArray(element)) { return element.reduce((acc, el) => acc.concat(createInstances(el)), []); } if (isBoolean(element)) { return []; } if (isString(element) || isNumber(element)) { return [{ type: P.TextInstance, value: `${element}` }]; } if (isFragment(element)) { // @ts-expect-error figure out why this is complains return createInstances(element.props.children); } if (!isString(element.type)) { // @ts-expect-error figure out why this is complains return createInstances(element.type(element.props)); } const { type, props: { style = {}, children, ...props }, } = element; const nextChildren = castArray(children).reduce((acc, child) => acc.concat(createInstances(child)), []); return [ { type, style, props, children: nextChildren, }, ]; }; const getBreak = (node) => 'break' in node.props ? node.props.break : false; const getMinPresenceAhead = (node) => 'minPresenceAhead' in node.props ? node.props.minPresenceAhead : 0; const getFurthestEnd = (elements) => Math.max(...elements.map((node) => node.box.top + node.box.height)); const getEndOfMinPresenceAhead = (child) => { return (child.box.top + child.box.height + child.box.marginBottom + getMinPresenceAhead(child)); }; const getEndOfPresence = (child, futureElements) => { const afterMinPresenceAhead = getEndOfMinPresenceAhead(child); const endOfFurthestFutureElement = getFurthestEnd(futureElements.filter((node) => !('fixed' in node.props))); return Math.min(afterMinPresenceAhead, endOfFurthestFutureElement); }; const shouldBreak = (child, futureElements, height) => { if ('fixed' in child.props) return false; const shouldSplit = height < child.box.top + child.box.height; const canWrap = getWrap(child); // Calculate the y coordinate where the desired presence of the child ends const endOfPresence = getEndOfPresence(child, futureElements); // If the child is already at the top of the page, breaking won't improve its presence // (as long as react-pdf does not support breaking into differently sized containers) const breakingImprovesPresence = child.box.top > child.box.marginTop; return (getBreak(child) || (shouldSplit && !canWrap) || (!shouldSplit && endOfPresence > height && breakingImprovesPresence)); }; const IGNORABLE_CODEPOINTS = [ 8232, // LINE_SEPARATOR 8233, // PARAGRAPH_SEPARATOR ]; const buildSubsetForFont = (font) => IGNORABLE_CODEPOINTS.reduce((acc, codePoint) => { if (font && font.hasGlyphForCodePoint && font.hasGlyphForCodePoint(codePoint)) { return acc; } return [...acc, String.fromCharCode(codePoint)]; }, []); const ignoreChars = (fragments) => fragments.map((fragment) => { const charSubset = buildSubsetForFont(fragment.attributes.font[0]); const subsetRegex = new RegExp(charSubset.join('|')); return { string: fragment.string.replace(subsetRegex, ''), attributes: fragment.attributes, }; }); const PREPROCESSORS = [ignoreChars, embedEmojis]; const isImage$1 = (node) => node.type === P.Image; const isTextInstance$2 = (node) => node.type === P.TextInstance; /** * Get textkit fragments of given node object * * @param fontStore - Font store * @param instance - Node * @param parentLink - Parent link * @param level - Fragment level * @returns Text fragments */ const getFragments = (fontStore, instance, parentLink = null, level = 0) => { if (!instance) return [{ string: '' }]; let fragments = []; const { color = 'black', direction = 'ltr', fontFamily = 'Helvetica', fontWeight, fontStyle, fontSize = 18, textAlign, lineHeight, textDecoration, textDecorationColor, textDecorationStyle, textTransform, letterSpacing, textIndent, opacity, verticalAlign, } = instance.style; const fontFamilies = typeof fontFamily === 'string' ? [fontFamily] : [...(fontFamily || [])]; // Fallback font fontFamilies.push('Helvetica'); const font = fontFamilies.map((fontFamilyName) => { const opts = { fontFamily: fontFamilyName, fontWeight, fontStyle }; const obj = fontStore.getFont(opts); return obj?.data; }); // Don't pass main background color to textkit. Will be rendered by the render package instead const backgroundColor = level === 0 ? null : instance.style.backgroundColor; const attributes = { font, color, opacity, fontSize, lineHeight, direction, verticalAlign, backgroundColor, indent: textIndent, characterSpacing: letterSpacing, strikeStyle: textDecorationStyle, underlineStyle: textDecorationStyle, underline: textDecoration === 'underline' || textDecoration === 'underline line-through' || textDecoration === 'line-through underline', strike: textDecoration === 'line-through' || textDecoration === 'underline line-through' || textDecoration === 'line-through underline', strikeColor: textDecorationColor || color, underlineColor: textDecorationColor || color, // @ts-expect-error allow this props access link: parentLink || instance.props?.src || instance.props?.href, align: textAlign || (direction === 'rtl' ? 'right' : 'left'), }; for (let i = 0; i < instance.children.length; i += 1) { const child = instance.children[i]; if (isImage$1(child)) { fragments.push({ string: String.fromCharCode(0xfffc), attributes: { ...attributes, attachment: { width: (child.style.width || fontSize), height: (child.style.height || fontSize), image: child.image.data, }, }, }); } else if (isTextInstance$2(child)) { fragments.push({ string: transformText(child.value, textTransform), attributes, }); } else if (child) { fragments.push(...getFragments(fontStore, child, attributes.link, level + 1)); } } for (let i = 0; i < PREPROCESSORS.length; i += 1) { const preprocessor = PREPROCESSORS[i]; fragments = preprocessor(fragments); } return fragments; }; /** * Get textkit attributed string from text node * * @param fontStore - Font store * @param instance Node * @returns Attributed string */ const getAttributedString = (fontStore, instance) => { const fragments = getFragments(fontStore, instance); return fromFragments(fragments); }; const engines = { bidi, linebreaker, justification, textDecoration, scriptItemizer, wordHyphenation, fontSubstitution, }; const engine = layoutEngine(engines); const getMaxLines = (node) => node.style?.maxLines; const getTextOverflow = (node) => node.style?.textOverflow; /** * Get layout container for specific text node * * @param {number} width * @param {number} height * @param {Object} node * @returns {Object} layout container */ const getContainer = (width, height, node) => { const maxLines = getMaxLines(node); const textOverflow = getTextOverflow(node); return { x: 0, y: 0, width, maxLines, height: height || Infinity, truncateMode: textOverflow, }; }; /** * Get text layout options for specific text node * * @param {Object} node instance * @returns {Object} layout options */ const getLayoutOptions = (fontStore, node) => ({ hyphenationPenalty: node.props.hyphenationPenalty, shrinkWhitespaceFactor: { before: -0.5, after: -0.5 }, hyphenationCallback: node.props.hyphenationCallback || fontStore?.getHyphenationCallback() || null, }); /** * Get text lines for given node * * @param node - Node * @param width - Container width * @param height - Container height * @param fontStore - Font store * @returns Layout lines */ const layoutText = (node, width, height, fontStore) => { const attributedString = getAttributedString(fontStore, node); const container = getContainer(width, height, node); const options = getLayoutOptions(fontStore, node); const lines = engine(attributedString, container, options); return lines.reduce((acc, line) => [...acc, ...line], []); }; const isSvg$2 = (node) => node.type === P.Svg; const isText$4 = (node) => node.type === P.Text; const shouldIterate = (node) => !isSvg$2(node) && !isText$4(node); const shouldLayoutText = (node) => isText$4(node) && !node.lines; /** * Performs text layout on text node if wasn't calculated before. * Text layout is usually performed on Yoga's layout process (via setMeasureFunc), * but we need to layout those nodes with fixed width and height. * * @param node * @returns Layout node */ const resolveTextLayout = (node, fontStore) => { if (shouldLayoutText(node)) { const width = node.box.width - (node.box.paddingRight + node.box.paddingLeft); const height = node.box.height - (node.box.paddingTop + node.box.paddingBottom); node.lines = layoutText(node, width, height, fontStore); } if (shouldIterate(node)) { if (!node.children) return node; const mapChild = (child) => resolveTextLayout(child, fontStore); const children = node.children.map(mapChild); return Object.assign({}, node, { children }); } return node; }; const BASE_INHERITABLE_PROPERTIES = [ 'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'letterSpacing', 'opacity', 'textDecoration', 'textTransform', 'lineHeight', 'textAlign', 'visibility', 'wordSpacing', ]; const TEXT_INHERITABLE_PROPERTIES = [ ...BASE_INHERITABLE_PROPERTIES, 'backgroundColor', ]; const isType$2 = (type) => (node) => node.type === type; const isSvg$1 = isType$2(P.Svg); const isText$3 = isType$2(P.Text); // Merge style values const mergeValues = (styleName, value, inheritedValue) => { switch (styleName) { case 'textDecoration': { // merge not none and not false textDecoration values to one rule return [inheritedValue, value].filter((v) => v && v !== 'none').join(' '); } default: return value; } }; // Merge inherited and node styles const merge = (inheritedStyles, style) => { const mergedStyles = { ...inheritedStyles }; Object.entries(style).forEach(([styleName, value]) => { mergedStyles[styleName] = mergeValues(styleName, value, inheritedStyles[styleName]); }); return mergedStyles; }; /** * Merges styles with node * * @param inheritedStyles - Style object * @returns Merge styles function */ const mergeStyles = (inheritedStyles) => (node) => { const style = merge(inheritedStyles, node.style || {}); return Object.assign({}, node, { style }); }; /** * Inherit style values from the root to the leafs * * @param node - Document root * @returns Document root with inheritance * */ const resolveInheritance = (node) => { if (isSvg$1(node)) return node; if (!('children' in node)) return node; const inheritableProperties = isText$3(node) ? TEXT_INHERITABLE_PROPERTIES : BASE_INHERITABLE_PROPERTIES; const inheritStyles = pick(inheritableProperties, node.style || {}); const resolveChild = compose(resolveInheritance, mergeStyles(inheritStyles)); const children = node.children.map(resolveChild); return Object.assign({}, node, { children }); }; const getComputedMargin = (node, edge) => { const { yogaNode } = node; return yogaNode ? yogaNode.getComputedMargin(edge) : null; }; /** * Get Yoga computed magins. Zero otherwise * * @param node * @returns Margins */ const getMargin = (node) => { const { style, box } = node; const marginTop = getComputedMargin(node, Yoga.Edge.Top) || box?.marginTop || style?.marginTop || 0; const marginRight = getComputedMargin(node, Yoga.Edge.Right) || box?.marginRight || style?.marginRight || 0; const marginBottom = getComputedMargin(node, Yoga.Edge.Bottom) || box?.marginBottom || style?.marginBottom || 0; const marginLeft = getComputedMargin(node, Yoga.Edge.Left) || box?.marginLeft || style?.marginLeft || 0; return { marginTop, marginRight, marginBottom, marginLeft }; }; /** * Get Yoga computed position. Zero otherwise * * @param node * @returns Position */ const getPosition = (node) => { const { yogaNode } = node; return { top: yogaNode?.getComputedTop() || 0, right: yogaNode?.getComputedRight() || 0, bottom: yogaNode?.getComputedBottom() || 0, left: yogaNode?.getComputedLeft() || 0, }; }; const DEFAULT_DIMENSION = { width: 0, height: 0, }; /** * Get Yoga computed dimensions. Zero otherwise * * @param node * @returns Dimensions */ const getDimension = (node) => { const { yogaNode } = node; if (!yogaNode) return DEFAULT_DIMENSION; return { width: yogaNode.getComputedWidth(), height: yogaNode.getComputedHeight(), }; }; const getComputedBorder = (yogaNode, edge) => (yogaNode ? yogaNode.getComputedBorder(edge) : 0); /** * Get Yoga computed border width. Zero otherwise * * @param node * @returns Border widths */ const getBorderWidth = (node) => { const { yogaNode } = node; return { borderTopWidth: getComputedBorder(yogaNode, Yoga.Edge.Top), borderRightWidth: getComputedBorder(yogaNode, Yoga.Edge.Right), borderBottomWidth: getComputedBorder(yogaNode, Yoga.Edge.Bottom), borderLeftWidth: getComputedBorder(yogaNode, Yoga.Edge.Left), }; }; /** * Set display attribute to node's Yoga instance * * @param value - Display * @returns Node instance wrapper */ const setDisplay = (value) => (node) => { const { yogaNode } = node; if (yogaNode) { yogaNode.setDisplay(value === 'none' ? Yoga.Display.None : Yoga.Display.Flex); } return node; }; const OVERFLOW = { hidden: Yoga.Overflow.Hidden, scroll: Yoga.Overflow.Scroll, }; /** * Set overflow attribute to node's Yoga instance * * @param value - Overflow value * @returns Node instance wrapper */ const setOverflow = (value) => (node) => { const { yogaNode } = node; if (!isNil(value) && yogaNode) { const overflow = OVERFLOW[value] || Yoga.Overflow.Visible; yogaNode.setOverflow(overflow); } return node; }; const FLEX_WRAP = { wrap: Yoga.Wrap.Wrap, 'wrap-reverse': Yoga.Wrap.WrapReverse, }; /** * Set flex wrap attribute to node's Yoga instance * * @param value - Flex wrap value * @returns Node instance wrapper */ const setFlexWrap = (value) => (node) => { const { yogaNode } = node; if (yogaNode) { const flexWrap = FLEX_WRAP[value] || Yoga.Wrap.NoWrap; yogaNode.setFlexWrap(flexWrap); } return node; }; /** * Set generic yoga attribute to node's Yoga instance, handing `auto`, edges and percentage cases * * @param attr - Property * @param edge - Edge * @returns Node instance wrapper */ const setYogaValue = (attr, edge) => (value) => (node) => { const { yogaNode } = node; if (!isNil(value) && yogaNode) { const hasEdge = !isNil(edge); const fixedMethod = `set${upperFirst(attr)}`; const autoMethod = `${fixedMethod}Auto`; const percentMethod = `${fixedMethod}Percent`; const percent = matchPercent(value); if (percent && !yogaNode[percentMethod]) { throw new Error(`You can't pass percentage values to ${attr} property`); } if (percent) { if (hasEdge) { yogaNode[percentMethod]?.(edge, percent.value); } else { yogaNode[percentMethod]?.(percent.value); } } else if (value === 'auto') { if (hasEdge) { yogaNode[autoMethod]?.(edge); } else { yogaNode[autoMethod]?.(); } } else if (hasEdge) { yogaNode[fixedMethod]?.(edge, value); } else { yogaNode[fixedMethod]?.(value); } } return node; }; /** * Set flex grow attribute to node's Yoga instance * * @param value - Flex grow value * @returns Node instance wrapper */ const setFlexGrow = (value) => (node) => { return setYogaValue('flexGrow')(value || 0)(node); }; /** * Set flex basis attribute to node's Yoga instance * * @param flex - Basis value * @param node - Node instance * @returns Node instance */ const setFlexBasis = setYogaValue('flexBasis'); const ALIGN = { 'flex-start': Yoga.Align.FlexStart, center: Yoga.Align.Center, 'flex-end': Yoga.Align.FlexEnd, stretch: Yoga.Align.Stretch, baseline: Yoga.Align.Baseline, 'space-between': Yoga.Align.SpaceBetween, 'space-around': Yoga.Align.SpaceAround, 'space-evenly': Yoga.Align.SpaceEvenly, }; /** * Set generic align attribute to node's Yoga instance * * @param attr - Specific align property * @param value - Specific align value * @param node - Node * @returns Node */ const setAlign = (attr) => (value) => (node) => { const { yogaNode } = node; const defaultValue = attr === 'items' ? Yoga.Align.Stretch : Yoga.Align.Auto; if (yogaNode) { const align = ALIGN[value] || defaultValue; yogaNode[`setAlign${upperFirst(attr)}`](align); } return node; }; /** * Set align self attribute to node's Yoga instance * * @param align - Value * @param node - Node instance * @returns Node instance */ const setAlignSelf = setAlign('self'); /** * Set align items attribute to node's Yoga instance * * @param align - Value * @param node - Node instance * @returns Node instance */ const setAlignItems = setAlign('items'); /** * Set flex shrink attribute to node's Yoga instance * * @param value - Flex shrink value * @returns Node instance wrapper */ const setFlexShrink = (value) => (node) => { return setYogaValue('flexShrink')(value || 1)(node); }; /** * Set aspect ratio attribute to node's Yoga instance * * @param value - Ratio * @returns Node instance */ const setAspectRatio = (value) => (node) => { const { yogaNode } = node; if (!isNil(value) && yogaNode) { yogaNode.setAspectRatio(value); } return node; }; /** * Set align content attribute to node's Yoga instance * * @param align - Value * @param node - Instance * @returns Node instance */ const setAlignContent = setAlign('content'); const POSITION = { absolute: Yoga.PositionType.Absolute, relative: Yoga.PositionType.Relative, static: Yoga.PositionType.Static, }; /** * Set position type attribute to node's Yoga instance * * @param value - Position position type * @returns Node instance */ const setPositionType = (value) => (node) => { const { yogaNode } = node; if (!isNil(value) && yogaNode) { yogaNode.setPositionType(POSITION[value]); } return node; }; const FLEX_DIRECTIONS = { row: Yoga.FlexDirection.Row, 'row-reverse': Yoga.FlexDirection.RowReverse, 'column-reverse': Yoga.FlexDirection.ColumnReverse, }; /** * Set flex direction attribute to node's Yoga instance * * @param value - Flex direction value * @returns Node instance wrapper */ const setFlexDirection = (value) => (node) => { const { yogaNode } = node; if (yogaNode) { const flexDirection = FLEX_DIRECTIONS[value] || Yoga.FlexDirection.Column; yogaNode.setFlexDirection(flexDirection); } return node; }; const JUSTIFY_CONTENT = { center: Yoga.Justify.Center, 'flex-end': Yoga.Justify.FlexEnd, 'space-between': Yoga.Justify.SpaceBetween, 'space-around': Yoga.Justify.SpaceAround, 'space-evenly': Yoga.Justify.SpaceEvenly, }; /** * Set justify content attribute to node's Yoga instance * * @param value - Justify content value * @returns Node instance wrapper */ const setJustifyContent = (value) => (node) => { const { yogaNode } = node; if (!isNil(value) && yogaNode) { const justifyContent = JUSTIFY_CONTENT[value] || Yoga.Justify.FlexStart; yogaNode.setJustifyContent(justifyContent); } return node; }; /** * Set margin top attribute to node's Yoga instance * * @param margin - Margin top * @param node - Node instance * @returns Node instance */ const setMarginTop = setYogaValue('margin', Yoga.Edge.Top); /** * Set margin right attribute to node's Yoga instance * * @param margin - Margin right * @param node - Node instance * @returns Node instance */ const setMarginRight = setYogaValue('margin', Yoga.Edge.Right); /** * Set margin bottom attribute to node's Yoga instance * * @param margin - Margin bottom * @param node - Node instance * @returns Node instance */ const setMarginBottom = setYogaValue('margin', Yoga.Edge.Bottom); /** * Set margin left attribute to node's Yoga instance * * @param margin - Margin left * @param node - Node instance * @returns Node instance */ const setMarginLeft = setYogaValue('margin', Yoga.Edge.Left); /** * Set padding top attribute to node's Yoga instance * * @param padding - Padding top * @param node - Node instance * @returns Node instance */ const setPaddingTop = setYogaValue('padding', Yoga.Edge.Top); /** * Set padding right attribute to node's Yoga instance * * @param padding - Padding right * @param node - Node instance * @returns Node instance */ const setPaddingRight = setYogaValue('padding', Yoga.Edge.Right); /** * Set padding bottom attribute to node's Yoga instance * * @param padding - Padding bottom * @param node Node instance * @returns Node instance */ const setPaddingBottom = setYogaValue('padding', Yoga.Edge.Bottom); /** * Set padding left attribute to node's Yoga instance * * @param padding - Padding left * @param node - Node instance * @returns Node instance */ const setPaddingLeft = setYogaValue('padding', Yoga.Edge.Left); /** * Set border top attribute to node's Yoga instance * * @param border - Border top width * @param node - Node instance * @returns Node instance */ const setBorderTop = setYogaValue('border', Yoga.Edge.Top); /** * Set border right attribute to node's Yoga instance * * @param border - Border right width * @param node - Node instance * @returns Node instance */ const setBorderRight = setYogaValue('border', Yoga.Edge.Right); /** * Set border bottom attribute to node's Yoga instance * * @param border - Border bottom width * @param node - Node instance * @returns Node instance */ const setBorderBottom = setYogaValue('border', Yoga.Edge.Bottom); /** * Set border left attribute to node's Yoga instance * * @param border - Border left width * @param node - Node instance * @returns Node instance */ const setBorderLeft = setYogaValue('border', Yoga.Edge.Left); /** * Set position top attribute to node's Yoga instance * * @param position - Position top * @param node - Node instance * @returns Node instance */ const setPositionTop = setYogaValue('position', Yoga.Edge.Top); /** * Set position right attribute to node's Yoga instance * * @param position - Position right * @param node - Node instance * @returns Node instance */ const setPositionRight = setYogaValue('position', Yoga.Edge.Right); /** * Set position bottom attribute to node's Yoga instance * * @param position - Position bottom * @param node - Node instance * @returns Node instance */ const setPositionBottom = setYogaValue('position', Yoga.Edge.Bottom); /** * Set position left attribute to node's Yoga instance * * @param position - Position left * @param node - Node instance * @returns Node instance */ const setPositionLeft = setYogaValue('position', Yoga.Edge.Left); /** * Set width to node's Yoga instance * * @param width - Width * @param node - Node instance * @returns Node instance */ const setWidth = setYogaValue('width'); /** * Set min width to node's Yoga instance * * @param min - Width * @param node - Node instance * @returns Node instance */ const setMinWidth = setYogaValue('minWidth'); /** * Set max width to node's Yoga instance * * @param max - Width * @param node - Node instance * @returns Node instance */ const setMaxWidth = setYogaValue('maxWidth'); /** * Set height to node's Yoga instance * * @param height - Height * @param node - Node instance * @returns Node instance */ const setHeight = setYogaValue('height'); /** * Set min height to node's Yoga instance * * @param min - Height * @param node - Node instance * @returns Node instance */ const setMinHeight = setYogaValue('minHeight'); /** * Set max height to node's Yoga instance * * @param max - Height * @param node - Node instance * @returns Node instance */ const setMaxHeight = setYogaValue('maxHeight'); /** * Set rowGap value to node's Yoga instance * * @param value - Gap value * @returns Node instance wrapper */ const setRowGap = setYogaValue('gap', Yoga.Gutter.Row); /** * Set columnGap value to node's Yoga instance * * @param value - Gap value * @returns Node instance wrapper */ const setColumnGap = setYogaValue('gap', Yoga.Gutter.Column); const getAspectRatio = (viewbox) => { if (!viewbox) return null; if (typeof viewbox === 'string') return null; return (viewbox.maxX - viewbox.minX) / (viewbox.maxY - viewbox.minY); }; /** * Yoga svg measure function * * @param page * @param node * @returns Measure svg */ const measureCanvas$1 = (page, node) => (width, widthMode, height, heightMode) => { const aspectRatio = getAspectRatio(node.props.viewBox) || 1; if (widthMode === Yoga.MeasureMode.Exactly || widthMode === Yoga.MeasureMode.AtMost) { return { width, height: width / aspectRatio }; } if (heightMode === Yoga.MeasureMode.Exactly) { return { width: height * aspectRatio }; } return {}; }; /** * Get lines width (if any) * * @param node * @returns Lines width */ const linesWidth = (node) => { if (!node.lines) return 0; return Math.max(0, ...node.lines.map((line) => line.xAdvance)); }; /** * Get lines height (if any) * * @param node * @returns Lines height */ const linesHeight = (node) => { if (!node.lines) return -1; return node.lines.reduce((acc, line) => acc + line.box.height, 0); }; const ALIGNMENT_FACTORS = { center: 0.5, right: 1 }; /** * Yoga text measure function * * @param page * @param node * @param fontStore * @returns {MeasureText} measure text function */ const measureText = (page, node, fontStore) => (width, widthMode, height) => { if (widthMode === Yoga.MeasureMode.Exactly) { if (!node.lines) node.lines = layoutText(node, width, height, fontStore); return { height: linesHeight(node) }; } if (widthMode === Yoga.MeasureMode.AtMost) { const alignFactor = ALIGNMENT_FACTORS[node.style?.textAlign] || 0; if (!node.lines) { node.lines = layoutText(node, width, height, fontStore); node.alignOffset = (width - linesWidth(node)) * alignFactor; // Compensate align in variable width containers } return { height: linesHeight(node), width: Math.min(width, linesWidth(node)), }; } return {}; }; /** * Get image ratio * * @param node - Image node * @returns Image ratio */ const getRatio = (node) => { return node.image?.data ? node.image.width / node.image.height : 1; }; /** * Checks if page has auto height * * @param page * @returns Is page height auto */ const isHeightAuto = (page) => isNil(page.box?.height); const SAFETY_HEIGHT$1 = 10; /** * Yoga image measure function * * @param page - Page * @param node - Node * @returns Measure image */ const measureImage = (page, node) => (width, widthMode, height, heightMode) => { const imageRatio = getRatio(node); const imageMargin = getMargin(node); const pagePadding = getPadding(page); // TODO: Check image percentage margins const pageArea = isHeightAuto(page) ? Infinity : (page.box?.height || 0) - pagePadding.paddingTop - pagePadding.paddingBottom - imageMargin.marginTop - imageMargin.marginBottom - SAFETY_HEIGHT$1; // Skip measure if image data not present yet if (!node.image) return { width: 0, height: 0 }; if (widthMode === Yoga.MeasureMode.Exactly && heightMode === Yoga.MeasureMode.Undefined) { const scaledHeight = width / imageRatio; return { height: Math.min(pageArea, scaledHeight) }; } if (heightMode === Yoga.MeasureMode.Exactly && (widthMode === Yoga.MeasureMode.AtMost || widthMode === Yoga.MeasureMode.Undefined)) { return { width: Math.min(height * imageRatio, width) }; } if (widthMode === Yoga.MeasureMode.Exactly && heightMode === Yoga.MeasureMode.AtMost) { const scaledHeight = width / imageRatio; return { height: Math.min(height, pageArea, scaledHeight) }; } if (widthMode === Yoga.MeasureMode.AtMost && heightMode === Yoga.MeasureMode.AtMost) { if (imageRatio > 1) { return { width, height: Math.min(width / imageRatio, height), }; } return { height, width: Math.min(height * imageRatio, width), }; } return { height, width }; }; const SAFETY_HEIGHT = 10; const getMax = (values) => Math.max(-Infinity, ...values); /** * Helper object to predict canvas size * TODO: Implement remaining functions (as close as possible); */ const measureCtx = () => { const ctx = {}; const points = []; const nil = () => ctx; const addPoint = (x, y) => points.push([x, y]); const moveTo = (x, y) => { addPoint(x, y); return ctx; }; const rect = (x, y, w, h) => { addPoint(x, y); addPoint(x + w, y); addPoint(x, y + h); addPoint(x + w, y + h); return ctx; }; const ellipse = (x, y, rx, ry) => { ry = ry || rx; addPoint(x - rx, y - ry); addPoint(x + rx, y - ry); addPoint(x + rx, y + ry); addPoint(x - rx, y + ry); return ctx; }; const polygon = (...pts) => { points.push(...pts); return ctx; }; // Change dimensions ctx.rect = rect; ctx.moveTo = moveTo; ctx.lineTo = moveTo; ctx.circle = ellipse; ctx.polygon = polygon; ctx.ellipse = ellipse; ctx.roundedRect = rect; // To be implemented ctx.text = nil; ctx.path = nil; ctx.lineWidth = nil; ctx.bezierCurveTo = nil; ctx.quadraticCurveTo = nil; ctx.scale = nil; ctx.rotate = nil; ctx.translate = nil; // These don't change dimensions ctx.dash = nil; ctx.clip = nil; ctx.save = nil; ctx.fill = nil; ctx.font = nil; ctx.stroke = nil; ctx.lineCap = nil; ctx.opacity = nil; ctx.restore = nil; ctx.lineJoin = nil; ctx.fontSize = nil; ctx.fillColor = nil; ctx.miterLimit = nil; ctx.strokeColor = nil; ctx.fillOpacity = nil; ctx.strokeOpacity = nil; ctx.linearGradient = nil; ctx.radialGradient = nil; ctx.getWidth = () => getMax(points.map((p) => p[0])); ctx.getHeight = () => getMax(points.map((p) => p[1])); return ctx; }; /** * @typedef {Function} MeasureCanvas * @returns {{ width: number, height: number }} canvas width and height */ /** * Yoga canvas measure function * * @param {Object} page * @param {Object} node * @returns {MeasureCanvas} measure canvas */ const measureCanvas = (page, node) => () => { const imageMargin = getMargin(node); const pagePadding = getPadding(page); // TODO: Check image percentage margins const pageArea = isHeightAuto(page) ? Infinity : (page.box?.height || 0) - pagePadding.paddingTop - pagePadding.paddingBottom - imageMargin.marginTop - imageMargin.marginBottom - SAFETY_HEIGHT; const ctx = measureCtx(); node.props.paint(ctx); const width = ctx.getWidth(); const height = Math.min(pageArea, ctx.getHeight()); return { width, height }; }; const isType$1 = (type) => (node) => node.type === type; const isSvg = isType$1(P.Svg); const isText$2 = isType$1(P.Text); const isNote = isType$1(P.Note); const isPage = isType$1(P.Page); const isImage = isType$1(P.Image); const isCanvas = isType$1(P.Canvas); const isTextInstance$1 = isType$1(P.TextInstance); const setNodeHeight = (node) => { const value = isPage(node) ? node.box?.height : node.style?.height; return setHeight(value); }; /** * Set styles valeus into yoga node before layout calculation * * @param node */ const setYogaValues = (node) => { compose(setNodeHeight(node), setWidth(node.style.width), setMinWidth(node.style.minWidth), setMaxWidth(node.style.maxWidth), setMinHeight(node.style.minHeight), setMaxHeight(node.style.maxHeight), setMarginTop(node.style.marginTop), setMarginRight(node.style.marginRight), setMarginBottom(node.style.marginBottom), setMarginLeft(node.style.marginLeft), setPaddingTop(node.style.paddingTop), setPaddingRight(node.style.paddingRight), setPaddingBottom(node.style.paddingBottom), setPaddingLeft(node.style.paddingLeft), setPositionType(node.style.position), setPositionTop(node.style.top), setPositionRight(node.style.right), setPositionBottom(node.style.bottom), setPositionLeft(node.style.left), setBorderTop(node.style.borderTopWidth), setBorderRight(node.style.borderRightWidth), setBorderBottom(node.style.borderBottomWidth), setBorderLeft(node.style.borderLeftWidth), setDisplay(node.style.display), setFlexDirection(node.style.flexDirection), setAlignSelf(node.style.alignSelf), setAlignContent(node.style.alignContent), setAlignItems(node.style.alignItems), setJustifyContent(node.style.justifyContent), setFlexWrap(node.style.flexWrap), setOverflow(node.style.overflow), setAspectRatio(node.style.aspectRatio), setFlexBasis(node.style.flexBasis), setFlexGrow(node.style.flexGrow), setFlexShrink(node.style.flexShrink), setRowGap(node.style.rowGap), setColumnGap(node.style.columnGap))(node); }; /** * Inserts child into parent' yoga node * * @param parent parent * @returns Insert yoga nodes */ const insertYogaNodes = (parent) => (child) => { parent.insertChild(child.yogaNode, parent.getChildCount()); return child; }; const setMeasureFunc = (node, page, fontStore) => { const { yogaNode } = node; if (isText$2(node)) { yogaNode.setMeasureFunc(measureText(page, node, fontStore)); } if (isImage(node)) { yogaNode.setMeasureFunc(measureImage(page, node)); } if (isCanvas(node)) { yogaNode.setMeasureFunc(measureCanvas(page, node)); } if (isSvg(node)) { yogaNode.setMeasureFunc(measureCanvas$1(page, node)); } return node; }; const isLayoutElement = (node) => !isText$2(node) && !isNote(node) && !isSvg(node); /** * @typedef {Function} CreateYogaNodes * @param {Object} node * @returns {Object} node with appended yoga node */ /** * Creates and add yoga node to document tree * Handles measure function for text and image nodes * * @returns Create yoga nodes */ const createYogaNodes = (page, fontStore, yoga) => (node) => { const yogaNode = yoga.node.create(); const result = Object.assign({}, node, { yogaNode }); setYogaValues(result); if (isLayoutElement(node) && node.children) { const resolveChild = compose(insertYogaNodes(yogaNode), createYogaNodes(page, fontStore, yoga)); result.children = node.children.map(resolveChild); } setMeasureFunc(result, page, fontStore); return result; }; /** * Performs yoga calculation * * @param page - Page node * @returns Page node */ const calculateLayout = (page) => { page.yogaNode.calculateLayout(); return page; }; /** * Saves Yoga layout result into 'box' attribute of node * * @param node * @returns Node with box data */ const persistDimensions = (node) => { if (isTextInstance$1(node)) return node; const box = Object.assign(getPadding(node), getMargin(node), getBorderWidth(node), getPosition(node), getDimension(node)); const newNode = Object.assign({}, node, { box }); if (!node.children) return newNode; const children = node.children.map(persistDimensions); return Object.assign({}, newNode, { children }); }; /** * Removes yoga node from document tree * * @param node * @returns Node without yoga node */ const destroyYogaNodes = (node) => { const newNode = Object.assign({}, node); delete newNode.yogaNode; if (!node.children) return newNode; const children = node.children.map(destroyYogaNodes); return Object.assign({}, newNode, { children }); }; /** * Free yoga node from document tree * * @param node * @returns Node without yoga node */ const freeYogaNodes = (node) => { if (node.yogaNode) node.yogaNode.freeRecursive(); return node; }; /** * Calculates page object layout using Yoga. * Takes node values from 'box' and 'style' attributes, and persist them back into 'box' * Destroy yoga values at the end. * * @param page - Object * @returns Page object with correct 'box' layout attributes */ const resolvePageDimensions = (page, fontStore, yoga) => { if (isNil(page)) return null; return compose(destroyYogaNodes, freeYogaNodes, persistDimensions, calculateLayout, createYogaNodes(page, fontStore, yoga))(page); }; /** * Calculates root object layout using Yoga. * * @param node - Root object * @param fontStore - Font store * @returns Root object with correct 'box' layout attributes */ const resolveDimensions = (node, fontStore) => { if (!node.children) return node; const resolveChild = (child) => resolvePageDimensions(child, fontStore, node.yoga); const children = node.children.map(resolveChild); return Object.assign({}, node, { children }); }; const isText$1 = (node) => node.type === P.Text; // Prevent splitting elements by low decimal numbers const SAFETY_THRESHOLD = 0.001; const assingChildren = (children, node) => Object.assign({}, node, { children }); const getTop = (node) => node.box?.top || 0; const allFixed = (nodes) => nodes.every(isFixed); const isDynamic = (node) => node.props && 'render' in node.props; const relayoutPage = compose(resolveTextLayout, resolvePageDimensions, resolveInheritance, resolvePageStyles); const warnUnavailableSpace = (node) => { console.warn(`Node of type ${node.type} can't wrap between pages and it's bigger than available page height`); }; const splitNodes = (height, contentArea, nodes) => { const currentChildren = []; const nextChildren = []; for (let i = 0; i < nodes.length; i += 1) { const child = nodes[i]; const futureNodes = nodes.slice(i + 1); const futureFixedNodes = futureNodes.filter(isFixed); const nodeTop = getTop(child); const nodeHeight = child.box.height; const isOutside = height <= nodeTop; const shouldBreak$1 = shouldBreak(child, futureNodes, height); const shouldSplit = height + SAFETY_THRESHOLD < nodeTop + nodeHeight; const canWrap = getWrap(child); const fitsInsidePage = nodeHeight <= contentArea; if (isFixed(child)) { nextChildren.push(child); currentChildren.push(child); continue; } if (isOutside) { const box = Object.assign({}, child.box, { top: child.box.top - height }); const next = Object.assign({}, child, { box }); nextChildren.push(next); continue; } if (!fitsInsidePage && !canWrap) { currentChildren.push(child); nextChildren.push(...futureNodes); warnUnavailableSpace(child); break; } if (shouldBreak$1) { const box = Object.assign({}, child.box, { top: child.box.top - height }); const props = Object.assign({}, child.props, { wrap: true, break: false, }); const next = Object.assign({}, child, { box, props }); currentChildren.push(...futureFixedNodes); nextChildren.push(next, ...futureNodes); break; } if (shouldSplit) { const [currentChild, nextChild] = split(child, height, contentArea); // All children are moved to the next page, it doesn't make sense to show the parent on the current page if (child.children.length > 0 && currentChild.children.length === 0) { // But if the current page is empty then we can just include the parent on the current page if (currentChildren.length === 0) { currentChildren.push(child, ...futureFixedNodes); nextChildren.push(...futureNodes); } else { const box = Object.assign({}, child.box, { top: child.box.top - height, }); const next = Object.assign({}, child, { box }); currentChildren.push(...futureFixedNodes); nextChildren.push(next, ...futureNodes); } break; } if (currentChild) currentChildren.push(currentChild); if (nextChild) nextChildren.push(nextChild); continue; } currentChildren.push(child); } return [currentChildren, nextChildren]; }; const splitChildren = (height, contentArea, node) => { const children = node.children || []; const availableHeight = height - getTop(node); return splitNodes(availableHeight, contentArea, children); }; const splitView = (node, height, contentArea) => { const [currentNode, nextNode] = splitNode(node, height); const [currentChilds, nextChildren] = splitChildren(height, contentArea, node); return [ assingChildren(currentChilds, currentNode), assingChildren(nextChildren, nextNode), ]; }; const split = (node, height, contentArea) => isText$1(node) ? splitText(node, height) : splitView(node, height, contentArea); const shouldResolveDynamicNodes = (node) => { const children = node.children || []; return isDynamic(node) || children.some(shouldResolveDynamicNodes); }; const resolveDynamicNodes = (props, node) => { const isNodeDynamic = isDynamic(node); // Call render prop on dynamic nodes and append result to children const resolveChildren = (children = []) => { if (isNodeDynamic) { const res = node.props.render(props); return (createInstances(res) .filter(Boolean) // @ts-expect-error rework dynamic nodes. conflicting types .map((n) => resolveDynamicNodes(props, n))); } return children.map((c) => resolveDynamicNodes(props, c)); }; // We reset dynamic text box so it can be computed again later on const resetHeight = isNodeDynamic && isText$1(node); const box = resetHeight ? { ...node.box, height: 0 } : node.box; const children = resolveChildren(node.children); // @ts-expect-error handle text here specifically const lines = isNodeDynamic ? null : node.lines; return Object.assign({}, node, { box, lines, children }); }; const resolveDynamicPage = (props, page, fontStore, yoga) => { if (shouldResolveDynamicNodes(page)) { const resolvedPage = resolveDynamicNodes(props, page); return relayoutPage(resolvedPage, fontStore, yoga); } return page; }; const splitPage = (page, pageNumber, fontStore, yoga) => { const wrapArea = getWrapArea(page); const contentArea = getContentArea(page); const dynamicPage = resolveDynamicPage({ pageNumber }, page, fontStore, yoga); const height = page.style.height; const [currentChilds, nextChilds] = splitNodes(wrapArea, contentArea, dynamicPage.children); const relayout = (node) => // @ts-expect-error rework pagination relayoutPage(node, fontStore, yoga); const currentBox = { ...page.box, height }; const currentPage = relayout(Object.assign({}, page, { box: currentBox, children: currentChilds })); if (nextChilds.length === 0 || allFixed(nextChilds)) return [currentPage, null]; const nextBox = omit('height', page.box); const nextProps = omit('bookmark', page.props); const nextPage = relayout(Object.assign({}, page, { props: nextProps, box: nextBox, children: nextChilds, })); return [currentPage, nextPage]; }; const resolvePageIndices = (fontStore, yoga, page, pageNumber, pages) => { const totalPages = pages.length; const props = { totalPages, pageNumber: pageNumber + 1, subPageNumber: page.subPageNumber + 1, subPageTotalPages: page.subPageTotalPages, }; return resolveDynamicPage(props, page, fontStore, yoga); }; const assocSubPageData = (subpages) => { return subpages.map((page, i) => ({ ...page, subPageNumber: i, subPageTotalPages: subpages.length, })); }; const dissocSubPageData = (page) => { return omit(['subPageNumber', 'subPageTotalPages'], page); }; const paginate = (page, pageNumber, fontStore, yoga) => { if (!page) return []; if (page.props?.wrap === false) return [page]; let splittedPage = splitPage(page, pageNumber, fontStore, yoga); const pages = [splittedPage[0]]; let nextPage = splittedPage[1]; while (nextPage !== null) { splittedPage = splitPage(nextPage, pageNumber + pages.length, fontStore, yoga); pages.push(splittedPage[0]); nextPage = splittedPage[1]; } return pages; }; /** * Performs pagination. This is the step responsible of breaking the whole document * into pages following pagiation rules, such as `fixed`, `break` and dynamic nodes. * * @param root - Document node * @param fontStore - Font store * @returns Layout node */ const resolvePagination = (root, fontStore) => { let pages = []; let pageNumber = 1; for (let i = 0; i < root.children.length; i += 1) { const page = root.children[i]; let subpages = paginate(page, pageNumber, fontStore, root.yoga); subpages = assocSubPageData(subpages); pageNumber += subpages.length; pages = pages.concat(subpages); } pages = pages.map((...args) => dissocSubPageData(resolvePageIndices(fontStore, root.yoga, ...args))); return assingChildren(pages, root); }; /** * Translates page percentage horizontal paddings in fixed ones * * @param container - Page container * @returns Resolve page horizontal padding */ const resolvePageHorizontalPadding = (container) => (value) => { const match = matchPercent(value); const width = container.width; return match ? match.percent * width : value; }; /** * Translates page percentage vertical paddings in fixed ones * * @param container - Page container * @returns Resolve page vertical padding */ const resolvePageVerticalPadding = (container) => (value) => { const match = matchPercent(value); const height = container.height; return match ? match.percent * height : value; }; /** * Translates page percentage paddings in fixed ones * * @param page * @returns Page with fixed paddings */ const resolvePagePaddings = (page) => { const container = page.style; const style = evolve({ paddingTop: resolvePageVerticalPadding(container), paddingLeft: resolvePageHorizontalPadding(container), paddingRight: resolvePageHorizontalPadding(container), paddingBottom: resolvePageVerticalPadding(container), }, page.style); return Object.assign({}, page, { style }); }; /** * Translates all pages percentage paddings in fixed ones * This has to be computed from pages calculated size and not by Yoga * because at this point we didn't performed pagination yet. * * @param root - Document root * @returns Document root with translated page paddings */ const resolvePagesPaddings = (root) => { if (!root.children) return root; const children = root.children.map(resolvePagePaddings); return Object.assign({}, root, { children }); }; const resolveRadius = (box) => (value) => { if (!value) return undefined; const match = matchPercent(value); return match ? match.percent * Math.min(box.width, box.height) : value; }; /** * Transforms percent border radius into fixed values * * @param node * @returns Node */ const resolvePercentRadius = (node) => { const style = evolve({ borderTopLeftRadius: resolveRadius(node.box), borderTopRightRadius: resolveRadius(node.box), borderBottomRightRadius: resolveRadius(node.box), borderBottomLeftRadius: resolveRadius(node.box), }, node.style || {}); const newNode = Object.assign({}, node, { style }); if (!node.children) return newNode; const children = node.children.map(resolvePercentRadius); return Object.assign({}, newNode, { children }); }; /** * Transform percent height into fixed * * @param height * @returns Height */ const transformHeight = (pageArea, height) => { const match = matchPercent(height); return match ? match.percent * pageArea : height; }; /** * Get page area (height minus paddings) * * @param page * @returns Page area */ const getPageArea = (page) => { const pageHeight = page.style.height; const pagePaddingTop = (page.style?.paddingTop || 0); const pagePaddingBottom = (page.style?.paddingBottom || 0); return pageHeight - pagePaddingTop - pagePaddingBottom; }; /** * Transform node percent height to fixed * * @param page * @param node * @returns Transformed node */ const resolveNodePercentHeight = (page, node) => { if (isNil(page.style?.height)) return node; if (isNil(node.style?.height)) return node; const pageArea = getPageArea(page); const height = transformHeight(pageArea, node.style.height); const style = Object.assign({}, node.style, { height }); return Object.assign({}, node, { style }); }; /** * Transform page immediate children with percent height to fixed * * @param page * @returns Transformed page */ const resolvePagePercentHeight = (page) => { if (!page.children) return page; const resolveChild = (child) => resolveNodePercentHeight(page, child); const children = page.children.map(resolveChild); return Object.assign({}, page, { children }); }; /** * Transform all page immediate children with percent height to fixed. * This is needed for computing correct dimensions on pre-pagination layout. * * @param root - Document root * @returns Transformed document root */ const resolvePercentHeight = (root) => { if (!root.children) return root; const children = root.children.map(resolvePagePercentHeight); return Object.assign({}, root, { children }); }; const isType = (type) => (node) => node.type === type; const isLink = isType(P.Link); const isText = isType(P.Text); const isTextInstance = isType(P.TextInstance); /** * Checks if node has render prop * * @param node * @returns Has render prop? */ const hasRenderProp = (node) => 'render' in node.props; /** * Checks if node is text type (Text or TextInstance) * * @param node * @returns Are all children text instances? */ const isTextType = (node) => isText(node) || isTextInstance(node); /** * Checks if is tet link that needs to be wrapped in Text * * @param node * @returns Are all children text instances? */ const isTextLink = (node) => { const children = node.children || []; // Text string inside a Link if (children.every(isTextInstance)) return true; // Text node inside a Link if (children.every(isText)) return false; return children.every(isTextType); }; /** * Wraps node children inside Text node * * @param node * @returns Node with intermediate Text child */ const wrapText = (node) => { const textElement = { type: P.Text, props: {}, style: {}, box: {}, children: node.children, }; return Object.assign({}, node, { children: [textElement] }); }; const transformLink = (node) => { if (!isLink(node)) return node; // If has render prop substitute the instance by a Text, that will // ultimately render the inline Link via the textkit PDF renderer. if (hasRenderProp(node)) return Object.assign({}, node, { type: P.Text }); // If is a text link (either contains Text or TextInstance), wrap it // inside a Text element so styles are applied correctly if (isTextLink(node)) return wrapText(node); return node; }; /** * Transforms Link layout to correctly render text and dynamic rendered links * * @param node * @returns Node with link substitution */ const resolveLinkSubstitution = (node) => { if (!node.children) return node; const resolveChild = compose(transformLink, resolveLinkSubstitution); const children = node.children.map(resolveChild); return Object.assign({}, node, { children }); }; const layout = asyncCompose(resolveZIndex, resolveOrigin, resolveAssets, resolvePagination, resolveTextLayout, resolvePercentRadius, resolveDimensions, resolveSvg, resolveAssets, resolveInheritance, resolvePercentHeight, resolvePagesPaddings, resolveStyles, resolveLinkSubstitution, resolveBookmarks, resolvePageSizes, resolveYoga); export { layout as default };