UNPKG

@knoopx/react-pdf

Version:

<p align="center"> <img src="https://user-images.githubusercontent.com/5600341/27505816-c8bc37aa-587f-11e7-9a86-08a2d081a8b9.png" height="280px"> <p align="center">React renderer for creating PDF files on the browser and server<p> <p align="center">

1,809 lines (1,487 loc) 113 kB
import fs from 'fs'; import BlobStream from 'blob-stream'; import ReactFiberReconciler from 'react-reconciler'; import PDFDocument, { PDFFont } from '@react-pdf/pdfkit'; import { Fragment } from 'react'; import Yoga from 'yoga-layout-prebuilt'; import { mergeDeepWith, isNil, compose, equals, type, toPairsIn, pick, pathOr, last, propEq, complement, prop } from 'ramda'; import matchMedia from 'media-engine'; import PDFRenderer$1 from '@react-pdf/textkit/renderers/pdf'; import AttributedString from '@react-pdf/textkit/attributedString'; import isUrl from 'is-url'; import fontkit from '@react-pdf/fontkit'; import fetch from 'cross-fetch'; import layoutEngine from '@react-pdf/textkit/layout'; import linebreaker from '@react-pdf/textkit/engines/linebreaker'; import justification from '@react-pdf/textkit/engines/justification'; import textDecoration from '@react-pdf/textkit/engines/textDecoration'; import scriptItemizer from '@react-pdf/textkit/engines/scriptItemizer'; import wordHyphenation from '@react-pdf/textkit/engines/wordHyphenation'; import emojiRegex from 'emoji-regex'; import url from 'url'; import path from 'path'; import PNG from '@react-pdf/png-js'; import wrapPages from 'page-wrapping'; class Root { constructor() { this.isDirty = false; this.document = null; this.instance = null; } get name() { return 'Root'; } appendChild(child) { this.document = child; } removeChild() { this.document.cleanup(); this.document = null; } markDirty() { this.isDirty = true; } cleanup() { this.document.cleanup(); } finish() { this.document.finish(); } async render() { this.instance = new PDFDocument({ autoFirstPage: false }); await this.document.render(); this.cleanup(); this.isDirty = false; } } const upperFirst = value => value.charAt(0).toUpperCase() + value.slice(1); const isPercent = value => /((-)?\d+\.?\d*)%/g.exec(value); const matchPercent = value => { const match = isPercent(value); if (match) { const value = parseFloat(match[1], 10); const percent = value / 100; return { value, percent, absValue: Math.abs(value), absPercent: Math.abs(percent) }; } return null; }; class Node { constructor() { this.parent = null; this.children = []; this.computed = false; this.layout = Yoga.Node.createDefault(); } appendChild(child) { if (child) { child.parent = this; this.children.push(child); this.layout.insertChild(child.layout, this.layout.getChildCount()); } } appendChildBefore(child, beforeChild) { const index = this.children.indexOf(beforeChild); if (index !== -1 && child) { child.parent = this; this.children.splice(index, 0, child); this.layout.insertChild(child.layout, index); } } removeChild(child) { const index = this.children.indexOf(child); if (index !== -1) { child.parent = null; this.children.splice(index, 1); this.layout.removeChild(child.layout); } child.cleanup(); } removeAllChilds() { const children = [...this.children]; for (let i = 0; i < children.length; i++) { children[i].remove(); } } remove() { this.parent.removeChild(this); } setDimension(attr, value) { const fixedMethod = `set${upperFirst(attr)}`; const percentMethod = `${fixedMethod}Percent`; const percent = matchPercent(value); if (percent) { this.layout[percentMethod](percent.value); } else { this.layout[fixedMethod](value); } } setPosition(edge, value) { const percent = matchPercent(value); if (percent) { this.layout.setPositionPercent(edge, percent.value); } else { this.layout.setPosition(edge, value); } } setPadding(edge, value) { const percent = matchPercent(value); if (percent) { this.layout.setPaddingPercent(edge, percent.value); } else { this.layout.setPadding(edge, value); } } setMargin(edge, value) { const percent = matchPercent(value); if (percent) { this.layout.setMarginPercent(edge, percent.value); } else { this.layout.setMargin(edge, value); } } setBorder(edge, value) { if (matchPercent(value)) { throw new Error('Node: You cannot set percentage border widths'); } this.layout.setBorder(edge, value); } getAbsoluteLayout() { const parent = this.parent; const parentLayout = parent && parent.getAbsoluteLayout ? parent.getAbsoluteLayout() : { left: 0, top: 0 }; return { left: this.left + parentLayout.left, top: this.top + parentLayout.top, height: this.height, width: this.width }; } copyStyle(node) { this.layout.copyStyle(node.layout); } calculateLayout() { this.layout.calculateLayout(); this.computed = true; } isEmpty() { return this.children.length === 0; } markDirty() { return this.layout.markDirty(); } onAppendDynamically() {} cleanup() { this.children.forEach(c => c.cleanup()); this.layout.unsetMeasureFunc(); Yoga.Node.destroy(this.layout); } get position() { return this.layout.getPositionType() === Yoga.POSITION_TYPE_ABSOLUTE ? 'absolute' : 'relative'; } get top() { return this.layout.getComputedTop() || 0; } get left() { return this.layout.getComputedLeft() || 0; } get right() { return this.layout.getComputedRight() || 0; } get bottom() { return this.layout.getComputedBottom() || 0; } get width() { return this.layout.getComputedWidth(); } get minWidth() { return this.layout.getMinWidth().value; } get maxWidth() { return this.layout.getMaxWidth().value; } get height() { return this.layout.getComputedHeight(); } get minHeight() { return this.layout.getMinHeight().value; } get maxHeight() { return this.layout.getMaxHeight().value; } get paddingTop() { return this.layout.getComputedPadding(Yoga.EDGE_TOP) || 0; } get paddingRight() { return this.layout.getComputedPadding(Yoga.EDGE_RIGHT) || 0; } get paddingBottom() { return this.layout.getComputedPadding(Yoga.EDGE_BOTTOM) || 0; } get paddingLeft() { return this.layout.getComputedPadding(Yoga.EDGE_LEFT) || 0; } get marginTop() { return this.layout.getComputedMargin(Yoga.EDGE_TOP) || 0; } get marginRight() { return this.layout.getComputedMargin(Yoga.EDGE_RIGHT) || 0; } get marginBottom() { return this.layout.getComputedMargin(Yoga.EDGE_BOTTOM) || 0; } get marginLeft() { return this.layout.getComputedMargin(Yoga.EDGE_LEFT) || 0; } get borderTopWidth() { return this.layout.getComputedBorder(Yoga.EDGE_TOP) || 0; } get borderRightWidth() { return this.layout.getComputedBorder(Yoga.EDGE_RIGHT) || 0; } get borderBottomWidth() { return this.layout.getComputedBorder(Yoga.EDGE_BOTTOM) || 0; } get borderLeftWidth() { return this.layout.getComputedBorder(Yoga.EDGE_LEFT) || 0; } get padding() { return { top: this.paddingTop, right: this.paddingRight, bottom: this.paddingBottom, left: this.paddingLeft }; } get margin() { return { top: this.marginTop, right: this.marginRight, bottom: this.marginBottom, left: this.marginLeft }; } set position(value) { this.layout.setPositionType(value === 'absolute' ? Yoga.POSITION_TYPE_ABSOLUTE : Yoga.POSITION_TYPE_RELATIVE); } set top(value) { this.setPosition(Yoga.EDGE_TOP, value); } set left(value) { this.setPosition(Yoga.EDGE_LEFT, value); } set right(value) { this.setPosition(Yoga.EDGE_RIGHT, value); } set bottom(value) { this.setPosition(Yoga.EDGE_BOTTOM, value); } set width(value) { this.setDimension('width', value); } set minWidth(value) { this.setDimension('minWidth', value); } set maxWidth(value) { this.setDimension('maxWidth', value); } set height(value) { this.setDimension('height', value); } set minHeight(value) { this.setDimension('minHeight', value); } set maxHeight(value) { this.setDimension('maxHeight', value); } set paddingTop(value) { this.setPadding(Yoga.EDGE_TOP, value); } set paddingRight(value) { this.setPadding(Yoga.EDGE_RIGHT, value); } set paddingBottom(value) { this.setPadding(Yoga.EDGE_BOTTOM, value); } set paddingLeft(value) { this.setPadding(Yoga.EDGE_LEFT, value); } set marginTop(value) { this.setMargin(Yoga.EDGE_TOP, value); } set marginRight(value) { this.setMargin(Yoga.EDGE_RIGHT, value); } set marginBottom(value) { this.setMargin(Yoga.EDGE_BOTTOM, value); } set marginLeft(value) { this.setMargin(Yoga.EDGE_LEFT, value); } set padding(value) { this.paddingTop = value; this.paddingRight = value; this.paddingBottom = value; this.paddingLeft = value; } set margin(value) { this.marginTop = value; this.marginRight = value; this.marginBottom = value; this.marginLeft = value; } set borderTopWidth(value) { this.setBorder(Yoga.EDGE_TOP, value); } set borderRightWidth(value) { this.setBorder(Yoga.EDGE_RIGHT, value); } set borderBottomWidth(value) { this.setBorder(Yoga.EDGE_BOTTOM, value); } set borderLeftWidth(value) { this.setBorder(Yoga.EDGE_LEFT, value); } } const yogaValue = (prop, value) => { const isAlignType = prop => prop === 'alignItems' || prop === 'alignContent' || prop === 'alignSelf'; switch (value) { case 'auto': if (prop === 'alignSelf') { return Yoga.ALIGN_AUTO; } break; case 'flex': return Yoga.DISPLAY_FLEX; case 'none': return Yoga.DISPLAY_NONE; case 'row': return Yoga.FLEX_DIRECTION_ROW; case 'row-reverse': return Yoga.FLEX_DIRECTION_ROW_REVERSE; case 'column': return Yoga.FLEX_DIRECTION_COLUMN; case 'column-reverse': return Yoga.FLEX_DIRECTION_COLUMN_REVERSE; case 'stretch': return Yoga.ALIGN_STRETCH; case 'baseline': return Yoga.ALIGN_BASELINE; case 'space-around': if (prop === 'justifyContent') { return Yoga.JUSTIFY_SPACE_AROUND; } else if (isAlignType(prop)) { return Yoga.ALIGN_SPACE_AROUND; } break; case 'space-between': if (prop === 'justifyContent') { return Yoga.JUSTIFY_SPACE_BETWEEN; } else if (isAlignType(prop)) { return Yoga.ALIGN_SPACE_BETWEEN; } break; case 'around': return Yoga.JUSTIFY_SPACE_AROUND; case 'between': return Yoga.JUSTIFY_SPACE_BETWEEN; case 'wrap': return Yoga.WRAP_WRAP; case 'wrap-reverse': return Yoga.WRAP_WRAP_REVERSE; case 'nowrap': return Yoga.WRAP_NO_WRAP; case 'flex-start': if (prop === 'justifyContent') { return Yoga.JUSTIFY_FLEX_START; } else if (isAlignType(prop)) { return Yoga.ALIGN_FLEX_START; } break; case 'flex-end': if (prop === 'justifyContent') { return Yoga.JUSTIFY_FLEX_END; } else if (isAlignType(prop)) { return Yoga.ALIGN_FLEX_END; } break; case 'center': if (prop === 'justifyContent') { return Yoga.JUSTIFY_CENTER; } else if (isAlignType(prop)) { return Yoga.ALIGN_CENTER; } break; default: return value; } }; // These are not supported yet const DPI = 72; // 72pt per inch. const parseValue = value => { const match = /^(-?\d*\.?\d+)(in|mm|cm|pt|vh|vw)?$/g.exec(value); if (match) { return { value: parseFloat(match[1], 10), unit: match[2] || 'pt' }; } return { value, unit: undefined }; }; const parseScalar = (value, container) => { const scalar = parseValue(value); switch (scalar.unit) { case 'in': return scalar.value * DPI; case 'mm': return scalar.value * (1 / 25.4) * DPI; case 'cm': return scalar.value * (1 / 2.54) * DPI; case 'vh': if (container.isAutoHeight) { throw new Error('vh unit not supported in auto-height pages. Please specify page height if you want to use vh.'); } return scalar.value * (container.height / 100); case 'vw': return scalar.value * (container.width / 100); default: return scalar.value; } }; const isBorderStyle = (key, value) => key.match(/^border/) && typeof value === 'string'; const matchBorderShorthand = value => value.match(/(\d+(px|in|mm|cm|pt|vw|vh)?)\s(\S+)\s(\S+)/); // Transforms shorthand border values const processBorders = (key, value) => { const match = matchBorderShorthand(value); if (match) { if (key.match(/.Color/)) { return match[4]; } else if (key.match(/.Style/)) { return match[3]; } else if (key.match(/.Width/)) { return match[1]; } else { throw new Error(`StyleSheet: Invalid '${value}' for '${key}'`); } } return value; }; const isBoxModelStyle = (key, value) => key.match(/^(margin)|(padding)/) && typeof value === 'string'; const matchBoxModel = value => value.match(/\d+(px|in|mm|cm|pt|%|vw|vh)?/g); // Transforms shorthand margin and padding values const processBoxModel = (key, value) => { const match = matchBoxModel(value); if (match) { if (key.match(/.Top/)) { return match[0]; } else if (key.match(/.Right/)) { return match[1] || match[0]; } else if (key.match(/.Bottom/)) { return match[2] || match[0]; } else if (key.match(/.Left/)) { return match[3] || match[1] || match[0]; } else { throw new Error(`StyleSheet: Invalid '${value}' for '${key}'`); } } return value; }; // https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Common_weight_name_mapping const FONT_WEIGHTS = { thin: 100, hairline: 100, ultralight: 200, extralight: 200, light: 300, normal: 400, medium: 500, semibold: 600, demibold: 600, bold: 700, ultrabold: 800, extrabold: 800, heavy: 900, black: 900 }; const isFontWeightStyle = key => key.match(/^fontWeight/); const processFontWeight = value => { if (!value) return FONT_WEIGHTS.normal; if (typeof value === 'number') return value; return FONT_WEIGHTS[value.toLowerCase()]; }; const isObjectPositionStyle = (key, value) => key.match(/^objectPosition/) && typeof value === 'string'; const matchObjectPosition = value => value.match(/\d+(px|in|mm|cm|pt|%|vw|vh)?/g); // Transforms shorthand objectPosition values const processObjectPosition = (key, value) => { const match = matchObjectPosition(value); if (match) { if (key.match(/.X/)) { return match[0]; } else if (key.match(/.Y/)) { return match[1]; } else { throw new Error(`StyleSheet: Invalid '${value}' for '${key}'`); } } return value; }; const isTransformOriginStyle = (key, value) => key.match(/^transformOrigin/) && typeof value === 'string'; const matchTransformOrigin = value => value.match(/(-?\d+(px|in|mm|cm|pt|%|vw|vh)?)|top|right|bottom|left|center/g); const transformOffsetKeywords = value => { switch (value) { case 'top': case 'left': return '0%'; case 'right': case 'bottom': return '100%'; case 'center': return '50%'; default: return value; } }; // Transforms shorthand transformOrigin values const processTransformOrigin = (key, value) => { const match = matchTransformOrigin(value); if (match) { let result; if (key.match(/.X/)) { result = match[0]; } else if (key.match(/.Y/)) { result = match[1] || match[0]; } else { throw new Error(`StyleSheet: Invalid '${value}' for '${key}'`); } return transformOffsetKeywords(result); } return value; }; const hasOwnProperty = Object.prototype.hasOwnProperty; const styleShorthands = { margin: { marginTop: true, marginRight: true, marginBottom: true, marginLeft: true }, marginHorizontal: { marginLeft: true, marginRight: true }, marginVertical: { marginTop: true, marginBottom: true }, padding: { paddingTop: true, paddingRight: true, paddingBottom: true, paddingLeft: true }, paddingHorizontal: { paddingLeft: true, paddingRight: true }, paddingVertical: { paddingTop: true, paddingBottom: true }, border: { borderTopColor: true, borderTopStyle: true, borderTopWidth: true, borderRightColor: true, borderRightStyle: true, borderRightWidth: true, borderBottomColor: true, borderBottomStyle: true, borderBottomWidth: true, borderLeftColor: true, borderLeftStyle: true, borderLeftWidth: true }, borderTop: { borderTopColor: true, borderTopStyle: true, borderTopWidth: true }, borderRight: { borderRightColor: true, borderRightStyle: true, borderRightWidth: true }, borderBottom: { borderBottomColor: true, borderBottomStyle: true, borderBottomWidth: true }, borderLeft: { borderLeftColor: true, borderLeftStyle: true, borderLeftWidth: true }, borderColor: { borderTopColor: true, borderRightColor: true, borderBottomColor: true, borderLeftColor: true }, borderRadius: { borderTopLeftRadius: true, borderTopRightRadius: true, borderBottomRightRadius: true, borderBottomLeftRadius: true }, borderStyle: { borderTopStyle: true, borderRightStyle: true, borderBottomStyle: true, borderLeftStyle: true }, borderWidth: { borderTopWidth: true, borderRightWidth: true, borderBottomWidth: true, borderLeftWidth: true }, objectPosition: { objectPositionX: true, objectPositionY: true }, transformOrigin: { transformOriginX: true, transformOriginY: true } }; // Expand the shorthand properties to isolate every declaration from the others. const expandStyles = style => { if (!style) return style; const propsArray = Object.keys(style); const resolvedStyle = {}; for (let i = 0; i < propsArray.length; i++) { const key = propsArray[i]; const value = style[key]; switch (key) { case 'display': case 'flex': case 'flexDirection': case 'flexWrap': case 'flexFlow': case 'flexGrow': case 'flexShrink': case 'flexBasis': case 'justifyContent': case 'alignSelf': case 'alignItems': case 'alignContent': case 'order': resolvedStyle[key] = yogaValue(key, value); break; case 'textAlignVertical': resolvedStyle.verticalAlign = value === 'center' ? 'middle' : value; break; case 'margin': case 'marginHorizontal': case 'marginVertical': case 'padding': case 'paddingHorizontal': case 'paddingVertical': case 'border': case 'borderTop': case 'borderRight': case 'borderBottom': case 'borderLeft': case 'borderColor': case 'borderRadius': case 'borderStyle': case 'borderWidth': case 'objectPosition': case 'transformOrigin': { const expandedProps = styleShorthands[key]; for (const propName in expandedProps) { if (hasOwnProperty.call(expandedProps, propName)) { resolvedStyle[propName] = value; } } } break; default: resolvedStyle[key] = value; break; } } return resolvedStyle; }; const transformStyles = (style, container) => { const expandedStyles = expandStyles(style); const propsArray = Object.keys(expandedStyles); const resolvedStyle = {}; for (let i = 0; i < propsArray.length; i++) { const key = propsArray[i]; const value = expandedStyles[key]; let resolved; if (isBorderStyle(key, value)) { resolved = processBorders(key, value); } else if (isBoxModelStyle(key, value)) { resolved = processBoxModel(key, value); } else if (isObjectPositionStyle(key, value)) { resolved = processObjectPosition(key, value); } else if (isTransformOriginStyle(key, value)) { resolved = processTransformOrigin(key, value); } else if (isFontWeightStyle(key)) { resolved = processFontWeight(value); } else { resolved = value; } resolvedStyle[key] = parseScalar(resolved, container); } return resolvedStyle; }; const create = styles => styles; const flatten = input => { if (!Array.isArray(input)) { input = [input]; } const result = input.reduce((acc, style) => { if (style) { const s = Array.isArray(style) ? flatten(style) : style; Object.keys(s).forEach(key => { if (s[key] !== null && s[key] !== undefined) { acc[key] = s[key]; } }); } return acc; }, {}); return result; }; const resolveMediaQueries = (input, container) => { const result = Object.keys(input).reduce((acc, key) => { if (/@media/.test(key)) { return { ...acc, ...matchMedia({ [key]: input[key] }, container) }; } return { ...acc, [key]: input[key] }; }, {}); return result; }; const resolve = (styles, container) => { if (!styles) return null; styles = flatten(styles); styles = resolveMediaQueries(styles, container); styles = transformStyles(styles, container); return styles; }; const absoluteFillObject = { position: 'absolute', top: 0, left: 0, bottom: 0, right: 0 }; var StyleSheet = { hairlineWidth: 1, create, resolve, flatten, absoluteFillObject }; const Debug = { debug() { const layout = this.getAbsoluteLayout(); const padding = this.padding; const margin = this.margin; this.root.instance.save(); this.debugContent(layout, margin, padding); this.debugPadding(layout, margin, padding); this.debugMargin(layout, margin); this.debugText(layout, margin); this.debugOrigin(); this.root.instance.restore(); }, debugOrigin() { if (this.style.transform) { const origin = this.origin; this.root.instance.circle(origin[0], origin[1], 3).fill('red').circle(origin[0], origin[1], 5).stroke('red'); } }, debugText(layout, margin) { const roundedWidth = Math.round(this.width + margin.left + margin.right); const roundedHeight = Math.round(this.height + margin.top + margin.bottom); this.root.instance.fontSize(4).opacity(1).fillColor('black').text(`${roundedWidth} x ${roundedHeight}`, layout.left - margin.left, Math.max(layout.top - margin.top - 4, 1)); }, debugContent(layout, margin, padding) { this.root.instance.fillColor('#a1c6e7').opacity(0.5).rect(layout.left + padding.left, layout.top + padding.top, layout.width - padding.left - padding.right, layout.height - padding.top - padding.bottom).fill(); }, debugPadding(layout, margin, padding) { this.root.instance.fillColor('#c4deb9').opacity(0.5); // Padding top this.root.instance.rect(layout.left + padding.left, layout.top, layout.width - padding.right - padding.left, padding.top).fill(); // Padding left this.root.instance.rect(layout.left, layout.top, padding.left, layout.height).fill(); // Padding right this.root.instance.rect(layout.left + layout.width - padding.right, layout.top, padding.right, layout.height).fill(); // Padding bottom this.root.instance.rect(layout.left + padding.left, layout.top + layout.height - padding.bottom, layout.width - padding.right - padding.left, padding.bottom).fill(); }, debugMargin(layout, margin) { this.root.instance.fillColor('#f8cca1').opacity(0.5); // Margin top this.root.instance.rect(layout.left, layout.top - margin.top, layout.width, margin.top).fill(); // Margin left this.root.instance.rect(layout.left - margin.left, layout.top - margin.top, margin.left, layout.height + margin.top + margin.bottom).fill(); // Margin right this.root.instance.rect(layout.left + layout.width, layout.top - margin.top, margin.right, layout.height + margin.top + margin.bottom).fill(); // Margin bottom this.root.instance.rect(layout.left, layout.top + layout.height, layout.width, margin.bottom).fill(); } }; // Ref: https://www.w3.org/TR/css-backgrounds-3/#borders // This constant is used to approximate a symmetrical arc using a cubic Bezier curve. const KAPPA = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0); function drawBorders() { const { instance } = this.root; const layout = this.getAbsoluteLayout(); const { borderTopWidth, borderLeftWidth, borderRightWidth, borderBottomWidth } = this; const { opacity, borderTopLeftRadius = 0, borderTopRightRadius = 0, borderBottomLeftRadius = 0, borderBottomRightRadius = 0, borderTopColor = 'black', borderTopStyle = 'solid', borderLeftColor = 'black', borderLeftStyle = 'solid', borderRightColor = 'black', borderRightStyle = 'solid', borderBottomColor = 'black', borderBottomStyle = 'solid' } = this.style; const style = { borderTopColor, borderTopWidth, borderTopStyle, borderLeftColor, borderLeftWidth, borderLeftStyle, borderRightColor, borderRightWidth, borderRightStyle, borderBottomColor, borderBottomWidth, borderBottomStyle, borderTopLeftRadius, borderTopRightRadius, borderBottomLeftRadius, borderBottomRightRadius }; const { width, height } = layout; const rtr = Math.min(borderTopRightRadius, 0.5 * width, 0.5 * height); const rtl = Math.min(borderTopLeftRadius, 0.5 * width, 0.5 * height); const rbr = Math.min(borderBottomRightRadius, 0.5 * width, 0.5 * height); const rbl = Math.min(borderBottomLeftRadius, 0.5 * width, 0.5 * height); instance.save(); instance.strokeOpacity(opacity); if (borderTopWidth) { instance.save(); clipBorderTop(instance, layout, style, rtr, rtl); fillBorderTop(instance, layout, style, rtr, rtl); instance.restore(); } if (borderRightWidth) { instance.save(); clipBorderRight(instance, layout, style, rtr, rbr); fillBorderRight(instance, layout, style, rtr, rbr); instance.restore(); } if (borderBottomWidth) { instance.save(); clipBorderBottom(instance, layout, style, rbl, rbr); fillBorderBottom(instance, layout, style, rbl, rbr); instance.restore(); } if (borderLeftWidth) { instance.save(); clipBorderLeft(instance, layout, style, rbl, rtl); fillBorderLeft(instance, layout, style, rbl, rtl); instance.restore(); } instance.restore(); } const clipBorderTop = (ctx, layout, style, rtr, rtl) => { const { top, left, width, height } = layout; const { borderTopWidth, borderRightWidth, borderLeftWidth } = style; // Clip outer top border edge ctx.moveTo(left + rtl, top); ctx.lineTo(left + width - rtr, top); // Ellipse coefficients outer top right cap const c0 = rtr * (1.0 - KAPPA); // Clip outer top right cap ctx.bezierCurveTo(left + width - c0, top, left + width, top + c0, left + width, top + rtr); // Move down in case the margin exceedes the radius const topRightYCoord = top + Math.max(borderTopWidth, rtr); ctx.lineTo(left + width, topRightYCoord); // Clip inner top right cap ctx.lineTo(left + width - borderRightWidth, topRightYCoord); // Ellipse coefficients inner top right cap const innerTopRightRadiusX = Math.max(rtr - borderRightWidth, 0); const innerTopRightRadiusY = Math.max(rtr - borderTopWidth, 0); const c1 = innerTopRightRadiusX * (1.0 - KAPPA); const c2 = innerTopRightRadiusY * (1.0 - KAPPA); // Clip inner top right cap ctx.bezierCurveTo(left + width - borderRightWidth, top + borderTopWidth + c2, left + width - borderRightWidth - c1, top + borderTopWidth, left + width - borderRightWidth - innerTopRightRadiusX, top + borderTopWidth); // Clip inner top border edge ctx.lineTo(left + Math.max(rtl, borderLeftWidth), top + borderTopWidth); // Ellipse coefficients inner top left cap const innerTopLeftRadiusX = Math.max(rtl - borderLeftWidth, 0); const innerTopLeftRadiusY = Math.max(rtl - borderTopWidth, 0); const c3 = innerTopLeftRadiusX * (1.0 - KAPPA); const c4 = innerTopLeftRadiusY * (1.0 - KAPPA); const topLeftYCoord = top + Math.max(borderTopWidth, rtl); // Clip inner top left cap ctx.bezierCurveTo(left + borderLeftWidth + c3, top + borderTopWidth, left + borderLeftWidth, top + borderTopWidth + c4, left + borderLeftWidth, topLeftYCoord); ctx.lineTo(left, topLeftYCoord); // Move down in case the margin exceedes the radius ctx.lineTo(left, top + rtl); // Ellipse coefficients outer top left cap const c5 = rtl * (1.0 - KAPPA); // Clip outer top left cap ctx.bezierCurveTo(left, top + c5, left + c5, top, left + rtl, top); ctx.closePath(); ctx.clip(); // Clip border top cap joins if (borderRightWidth) { const trSlope = -borderTopWidth / borderRightWidth; ctx.moveTo(left + width / 2, trSlope * (-width / 2) + top); ctx.lineTo(left + width, top); ctx.lineTo(left, top); ctx.lineTo(left, top + height); ctx.closePath(); ctx.clip(); } if (borderLeftWidth) { const trSlope = -borderTopWidth / borderLeftWidth; ctx.moveTo(left + width / 2, trSlope * (-width / 2) + top); ctx.lineTo(left, top); ctx.lineTo(left + width, top); ctx.lineTo(left + width, top + height); ctx.closePath(); ctx.clip(); } }; const fillBorderTop = (ctx, layout, style, rtr, rtl) => { const { top, left, width } = layout; const { borderTopColor, borderTopWidth, borderTopStyle, borderRightWidth, borderLeftWidth } = style; const c0 = rtl * (1.0 - KAPPA); const c1 = rtr * (1.0 - KAPPA); ctx.moveTo(left, top + Math.max(rtl, borderTopWidth)); ctx.bezierCurveTo(left, top + c0, left + c0, top, left + rtl, top); ctx.lineTo(left + width - rtr, top); ctx.bezierCurveTo(left + width - c1, top, left + width, top + c1, left + width, top + rtr); ctx.strokeColor(borderTopColor); ctx.lineWidth(Math.max(borderRightWidth, borderTopWidth, borderLeftWidth) * 2); if (borderTopStyle === 'dashed') { ctx.dash(borderTopWidth * 2, { space: borderTopWidth * 1.2 }); } else if (borderTopStyle === 'dotted') { ctx.dash(borderTopWidth, { space: borderTopWidth * 1.2 }); } ctx.stroke(); ctx.undash(); }; const clipBorderRight = (ctx, layout, style, rtr, rbr) => { const { top, left, width, height } = layout; const { borderTopWidth, borderRightWidth, borderBottomWidth } = style; // Clip outer right border edge ctx.moveTo(left + width, top + rtr); ctx.lineTo(left + width, top + height - rbr); // Ellipse coefficients outer bottom right cap const c0 = rbr * (1.0 - KAPPA); // Clip outer top right cap ctx.bezierCurveTo(left + width, top + height - c0, left + width - c0, top + height, left + width - rbr, top + height); // Move left in case the margin exceedes the radius const topBottomXCoord = left + width - Math.max(borderRightWidth, rbr); ctx.lineTo(topBottomXCoord, top + height); // Clip inner bottom right cap ctx.lineTo(topBottomXCoord, top + height - borderBottomWidth); // Ellipse coefficients inner bottom right cap const innerBottomRightRadiusX = Math.max(rbr - borderRightWidth, 0); const innerBottomRightRadiusY = Math.max(rbr - borderBottomWidth, 0); const c1 = innerBottomRightRadiusX * (1.0 - KAPPA); const c2 = innerBottomRightRadiusY * (1.0 - KAPPA); // Clip inner top right cap ctx.bezierCurveTo(left + width - borderRightWidth - c1, top + height - borderBottomWidth, left + width - borderRightWidth, top + height - borderBottomWidth - c2, left + width - borderRightWidth, top + height - Math.max(rbr, borderBottomWidth)); // Clip inner right border edge ctx.lineTo(left + width - borderRightWidth, top + Math.max(rtr, borderTopWidth)); // Ellipse coefficients inner top right cap const innerTopRightRadiusX = Math.max(rtr - borderRightWidth, 0); const innerTopRightRadiusY = Math.max(rtr - borderTopWidth, 0); const c3 = innerTopRightRadiusX * (1.0 - KAPPA); const c4 = innerTopRightRadiusY * (1.0 - KAPPA); const topRightXCoord = left + width - Math.max(rtr, borderRightWidth); // Clip inner top left cap ctx.bezierCurveTo(left + width - borderRightWidth, top + borderTopWidth + c4, left + width - borderRightWidth - c3, top + borderTopWidth, topRightXCoord, top + borderTopWidth); ctx.lineTo(topRightXCoord, top); // Move right in case the margin exceedes the radius ctx.lineTo(left + width - rtr, top); // Ellipse coefficients outer top right cap const c5 = rtr * (1.0 - KAPPA); // Clip outer top right cap ctx.bezierCurveTo(left + width - c5, top, left + width, top + c5, left + width, top + rtr); ctx.closePath(); ctx.clip(); // Clip border right cap joins if (borderTopWidth) { const trSlope = -borderTopWidth / borderRightWidth; ctx.moveTo(left + width / 2, trSlope * (-width / 2) + top); ctx.lineTo(left + width, top); ctx.lineTo(left + width, top + height); ctx.lineTo(left, top + height); ctx.closePath(); ctx.clip(); } if (borderBottomWidth) { const brSlope = borderBottomWidth / borderRightWidth; ctx.moveTo(left + width / 2, brSlope * (-width / 2) + top + height); ctx.lineTo(left + width, top + height); ctx.lineTo(left + width, top); ctx.lineTo(left, top); ctx.closePath(); ctx.clip(); } }; const fillBorderRight = (ctx, layout, style, rtr, rbr) => { const { top, left, width, height } = layout; const { borderRightColor, borderRightStyle, borderRightWidth, borderTopWidth, borderBottomWidth } = style; const c0 = rbr * (1.0 - KAPPA); const c1 = rtr * (1.0 - KAPPA); ctx.moveTo(left + width - rtr, top); ctx.bezierCurveTo(left + width - c1, top, left + width, top + c1, left + width, top + rtr); ctx.lineTo(left + width, top + height - rbr); ctx.bezierCurveTo(left + width, top + height - c0, left + width - c0, top + height, left + width - rbr, top + height); ctx.strokeColor(borderRightColor); ctx.lineWidth(Math.max(borderRightWidth, borderTopWidth, borderBottomWidth) * 2); if (borderRightStyle === 'dashed') { ctx.dash(borderRightWidth * 2, { space: borderRightWidth * 1.2 }); } else if (borderRightStyle === 'dotted') { ctx.dash(borderRightWidth, { space: borderRightWidth * 1.2 }); } ctx.stroke(); ctx.undash(); }; const clipBorderBottom = (ctx, layout, style, rbl, rbr) => { const { top, left, width, height } = layout; const { borderBottomWidth, borderRightWidth, borderLeftWidth } = style; // Clip outer top border edge ctx.moveTo(left + width - rbr, top + height); ctx.lineTo(left + rbl, top + height); // Ellipse coefficients outer top right cap const c0 = rbl * (1.0 - KAPPA); // Clip outer top right cap ctx.bezierCurveTo(left + c0, top + height, left, top + height - c0, left, top + height - rbl); // Move up in case the margin exceedes the radius const bottomLeftYCoord = top + height - Math.max(borderBottomWidth, rbl); ctx.lineTo(left, bottomLeftYCoord); // Clip inner bottom left cap ctx.lineTo(left + borderLeftWidth, bottomLeftYCoord); // Ellipse coefficients inner top right cap const innerBottomLeftRadiusX = Math.max(rbl - borderLeftWidth, 0); const innerBottomLeftRadiusY = Math.max(rbl - borderBottomWidth, 0); const c1 = innerBottomLeftRadiusX * (1.0 - KAPPA); const c2 = innerBottomLeftRadiusY * (1.0 - KAPPA); // Clip inner bottom left cap ctx.bezierCurveTo(left + borderLeftWidth, top + height - borderBottomWidth - c2, left + borderLeftWidth + c1, top + height - borderBottomWidth, left + borderLeftWidth + innerBottomLeftRadiusX, top + height - borderBottomWidth); // Clip inner bottom border edge ctx.lineTo(left + width - Math.max(rbr, borderRightWidth), top + height - borderBottomWidth); // Ellipse coefficients inner top left cap const innerBottomRightRadiusX = Math.max(rbr - borderRightWidth, 0); const innerBottomRightRadiusY = Math.max(rbr - borderBottomWidth, 0); const c3 = innerBottomRightRadiusX * (1.0 - KAPPA); const c4 = innerBottomRightRadiusY * (1.0 - KAPPA); const bottomRightYCoord = top + height - Math.max(borderBottomWidth, rbr); // Clip inner top left cap ctx.bezierCurveTo(left + width - borderRightWidth - c3, top + height - borderBottomWidth, left + width - borderRightWidth, top + height - borderBottomWidth - c4, left + width - borderRightWidth, bottomRightYCoord); ctx.lineTo(left + width, bottomRightYCoord); // Move down in case the margin exceedes the radius ctx.lineTo(left + width, top + height - rbr); // Ellipse coefficients outer top left cap const c5 = rbr * (1.0 - KAPPA); // Clip outer top left cap ctx.bezierCurveTo(left + width, top + height - c5, left + width - c5, top + height, left + width - rbr, top + height); ctx.closePath(); ctx.clip(); // Clip border bottom cap joins if (borderRightWidth) { const brSlope = borderBottomWidth / borderRightWidth; ctx.moveTo(left + width / 2, brSlope * (-width / 2) + top + height); ctx.lineTo(left + width, top + height); ctx.lineTo(left, top + height); ctx.lineTo(left, top); ctx.closePath(); ctx.clip(); } if (borderLeftWidth) { const trSlope = -borderBottomWidth / borderLeftWidth; ctx.moveTo(left + width / 2, trSlope * (width / 2) + top + height); ctx.lineTo(left, top + height); ctx.lineTo(left + width, top + height); ctx.lineTo(left + width, top); ctx.closePath(); ctx.clip(); } }; const fillBorderBottom = (ctx, layout, style, rbl, rbr) => { const { top, left, width, height } = layout; const { borderBottomColor, borderBottomStyle, borderBottomWidth, borderRightWidth, borderLeftWidth } = style; const c0 = rbl * (1.0 - KAPPA); const c1 = rbr * (1.0 - KAPPA); ctx.moveTo(left + width, top + height - rbr); ctx.bezierCurveTo(left + width, top + height - c1, left + width - c1, top + height, left + width - rbr, top + height); ctx.lineTo(left + rbl, top + height); ctx.bezierCurveTo(left + c0, top + height, left, top + height - c0, left, top + height - rbl); ctx.strokeColor(borderBottomColor); ctx.lineWidth(Math.max(borderBottomWidth, borderRightWidth, borderLeftWidth) * 2); if (borderBottomStyle === 'dashed') { ctx.dash(borderBottomWidth * 2, { space: borderBottomWidth * 1.2 }); } else if (borderBottomStyle === 'dotted') { ctx.dash(borderBottomWidth, { space: borderBottomWidth * 1.2 }); } ctx.stroke(); ctx.undash(); }; const clipBorderLeft = (ctx, layout, style, rbl, rtl) => { const { top, left, width, height } = layout; const { borderTopWidth, borderLeftWidth, borderBottomWidth } = style; // Clip outer left border edge ctx.moveTo(left, top + height - rbl); ctx.lineTo(left, top + rtl); // Ellipse coefficients outer top left cap const c0 = rtl * (1.0 - KAPPA); // Clip outer top left cap ctx.bezierCurveTo(left, top + c0, left + c0, top, left + rtl, top); // Move right in case the margin exceedes the radius const topLeftCoordX = left + Math.max(borderLeftWidth, rtl); ctx.lineTo(topLeftCoordX, top); // Clip inner top left cap ctx.lineTo(topLeftCoordX, top + borderTopWidth); // Ellipse coefficients inner top left cap const innerTopLeftRadiusX = Math.max(rtl - borderLeftWidth, 0); const innerTopLeftRadiusY = Math.max(rtl - borderTopWidth, 0); const c1 = innerTopLeftRadiusX * (1.0 - KAPPA); const c2 = innerTopLeftRadiusY * (1.0 - KAPPA); // Clip inner top right cap ctx.bezierCurveTo(left + borderLeftWidth + c1, top + borderTopWidth, left + borderLeftWidth, top + borderTopWidth + c2, left + borderLeftWidth, top + Math.max(rtl, borderTopWidth)); // Clip inner left border edge ctx.lineTo(left + borderLeftWidth, top + height - Math.max(rbl, borderBottomWidth)); // Ellipse coefficients inner bottom left cap const innerBottomLeftRadiusX = Math.max(rbl - borderLeftWidth, 0); const innerBottomLeftRadiusY = Math.max(rbl - borderBottomWidth, 0); const c3 = innerBottomLeftRadiusX * (1.0 - KAPPA); const c4 = innerBottomLeftRadiusY * (1.0 - KAPPA); const bottomLeftXCoord = left + Math.max(rbl, borderLeftWidth); // Clip inner top left cap ctx.bezierCurveTo(left + borderLeftWidth, top + height - borderBottomWidth - c4, left + borderLeftWidth + c3, top + height - borderBottomWidth, bottomLeftXCoord, top + height - borderBottomWidth); ctx.lineTo(bottomLeftXCoord, top + height); // Move left in case the margin exceedes the radius ctx.lineTo(left + rbl, top + height); // Ellipse coefficients outer top right cap const c5 = rbl * (1.0 - KAPPA); // Clip outer top right cap ctx.bezierCurveTo(left + c5, top + height, left, top + height - c5, left, top + height - rbl); ctx.closePath(); ctx.clip(); // Clip border right cap joins if (borderBottomWidth) { const trSlope = -borderBottomWidth / borderLeftWidth; ctx.moveTo(left + width / 2, trSlope * (width / 2) + top + height); ctx.lineTo(left, top + height); ctx.lineTo(left, top); ctx.lineTo(left + width, top); ctx.closePath(); ctx.clip(); } if (borderBottomWidth) { const trSlope = -borderTopWidth / borderLeftWidth; ctx.moveTo(left + width / 2, trSlope * (-width / 2) + top); ctx.lineTo(left, top); ctx.lineTo(left, top + height); ctx.lineTo(left + width, top + height); ctx.closePath(); ctx.clip(); } }; const fillBorderLeft = (ctx, layout, style, rbl, rtl) => { const { top, left, height } = layout; const { borderLeftColor, borderLeftStyle, borderLeftWidth, borderTopWidth, borderBottomWidth } = style; const c0 = rbl * (1.0 - KAPPA); const c1 = rtl * (1.0 - KAPPA); ctx.moveTo(left + rbl, top + height); ctx.bezierCurveTo(left + c0, top + height, left, top + height - c0, left, top + height - rbl); ctx.lineTo(left, top + rtl); ctx.bezierCurveTo(left, top + c1, left + c1, top, left + rtl, top); ctx.strokeColor(borderLeftColor); ctx.lineWidth(Math.max(borderLeftWidth, borderTopWidth, borderBottomWidth) * 2); if (borderLeftStyle === 'dashed') { ctx.dash(borderLeftWidth * 2, { space: borderLeftWidth * 1.2 }); } else if (borderLeftStyle === 'dotted') { ctx.dash(borderLeftWidth, { space: borderLeftWidth * 1.2 }); } ctx.stroke(); ctx.undash(); }; var Borders = { drawBorders }; // This constant is used to approximate a symmetrical arc using a cubic // Bezier curve. const KAPPA$1 = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0); const Clipping = { clip() { const { top, left, width, height } = this.getAbsoluteLayout(); const { borderTopLeftRadius = 0, borderTopRightRadius = 0, borderBottomRightRadius = 0, borderBottomLeftRadius = 0 } = this.style; // Border top const rtr = Math.min(borderTopRightRadius, 0.5 * width, 0.5 * height); const ctr = rtr * (1.0 - KAPPA$1); this.root.instance.moveTo(left + rtr, top); this.root.instance.lineTo(left + width - rtr, top); this.root.instance.bezierCurveTo(left + width - ctr, top, left + width, top + ctr, left + width, top + rtr); // Border right const rbr = Math.min(borderBottomRightRadius, 0.5 * width, 0.5 * height); const cbr = rbr * (1.0 - KAPPA$1); this.root.instance.lineTo(left + width, top + height - rbr); this.root.instance.bezierCurveTo(left + width, top + height - cbr, left + width - cbr, top + height, left + width - rbr, top + height); // Border bottom const rbl = Math.min(borderBottomLeftRadius, 0.5 * width, 0.5 * height); const cbl = rbl * (1.0 - KAPPA$1); this.root.instance.lineTo(left + rbl, top + height); this.root.instance.bezierCurveTo(left + cbl, top + height, left, top + height - cbl, left, top + height - rbl); // Border left const rtl = Math.min(borderTopLeftRadius, 0.5 * width, 0.5 * height); const ctl = rtl * (1.0 - KAPPA$1); this.root.instance.lineTo(left, top + rtl); this.root.instance.bezierCurveTo(left, top + ctl, left + ctl, top, left + rtl, top); this.root.instance.closePath(); this.root.instance.clip(); } }; const getRotation = transform => { const match = /rotate\((-?\d+.?\d+)(.+)\)/g.exec(transform); if (match && match[1] && match[2]) { const value = match[1]; return match[2] === 'rad' ? value * 180 / Math.PI : value; } return 0; }; const getTranslateX = transform => { const matchX = /translateX\((-?\d+\.?d*)\)/g.exec(transform); const matchGeneric = /translate\((-?\d+\.?d*).*,\s*(-?\d+\.?d*).*\)/g.exec(transform); if (matchX && matchX[1]) return matchX[1]; if (matchGeneric && matchGeneric[1]) return matchGeneric[1]; return 0; }; const getTranslateY = transform => { const matchY = /translateY\((-?\d+\.?\d*)\)/g.exec(transform); const matchGeneric = /translate\((-?\d+\.?\d*).*,\s*(-?\d+\.?\d*).*\)/g.exec(transform); if (matchY && matchY[1]) return matchY[1]; if (matchGeneric && matchGeneric[2]) return matchGeneric[2]; return 0; }; const getScaleX = transform => { const matchX = /scaleX\((-?\d+\.?\d*)\)/g.exec(transform); const matchGeneric = /scale\((-?\d+\.?\d*).*,\s*(-?\d+\.?\d*).*\)/g.exec(transform); if (matchX && matchX[1]) return matchX[1]; if (matchGeneric && matchGeneric[1]) return matchGeneric[1]; return 1; }; const getScaleY = transform => { const matchY = /scaleY\((-?\d+\.?\d*)\)/g.exec(transform); const matchGeneric = /scale\((-?\d+\.?\d*).*,\s*(-?\d+\.?\d*).*\)/g.exec(transform); if (matchY && matchY[1]) return matchY[1]; if (matchGeneric && matchGeneric[2]) return matchGeneric[2]; return 1; }; const getMatrix = transform => { const match = /matrix\(([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)\)/g.exec(transform); if (match) return match.slice(1, 7); return null; }; const applySingleTransformation = (element, transform, origin) => { if (/rotate/g.test(transform)) { element.root.instance.rotate(getRotation(transform), { origin }); } else if (/scaleX/g.test(transform)) { element.root.instance.scale(getScaleX(transform), 1, { origin }); } else if (/scaleY/g.test(transform)) { element.root.instance.scale(1, getScaleY(transform), { origin }); } else if (/scale/g.test(transform)) { element.root.instance.scale(getScaleX(transform), getScaleY(transform), { origin }); } else if (/translateX/g.test(transform)) { element.root.instance.translate(getTranslateX(transform), 1, { origin }); } else if (/translateY/g.test(transform)) { element.root.instance.translate(1, getTranslateY(transform), { origin }); } else if (/translate/g.test(transform)) { element.root.instance.translate(getTranslateX(transform), getTranslateY(transform), { origin }); } else if (/matrix/g.test(transform)) { element.root.instance.transform(...getMatrix(transform)); } }; const Transformations = { applyTransformations() { let match; const re = /[a-zA-Z]+\([^)]+\)/g; const origin = this.origin; const transform = this.style && this.style.transform || ''; while ((match = re.exec(transform)) != null) { applySingleTransformation(this, match[0], origin); } } }; function printWarning(format, ...args) { let argIndex = 0; const message = 'Warning: ' + format.replace(/%s/g, () => args[argIndex++]); if (typeof console !== 'undefined') { console.error(message); } try { throw new Error(message); } catch (x) {} } const __DEV__ = process.env.NODE_ENV !== 'production'; const warning = __DEV__ ? (condition, format, ...args) => { if (format === undefined) { throw new Error('`warning(condition, format, ...args)` requires a warning ' + 'message argument'); } if (!condition) { printWarning(format, ...args); } } : () => {}; const merge = (a, b) => { return isNil(b) ? a : b; }; const deepMerge = objs => objs.reduce((acc, obj) => { return mergeDeepWith(merge, acc, obj); }, {}); const isFunction = compose(equals('Function'), type); const inheritedProperties = ['color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'letterSpacing', 'opacity', 'textDecoration', 'lineHeight', 'textAlign', 'visibility', 'wordSpacing']; class Base extends Node { constructor(root, props) { super(); this.root = root; this.style = {}; this.props = deepMerge([this.constructor.defaultProps, Base.defaultProps, props]); warning(!this.props.styles, '"styles" prop passed instead of "style" prop'); } get page() { return this.parent.page; } get wrap() { return this.props.wrap; } get break() { return this.props.break; } get fixed() { return this.props.fixed; } get minPresenceAhead() { return this.props.minPresenceAhead; } get absolute() { return this.props.style.position === 'absolute'; } get origin() { const { transformOriginX, transformOriginY } = this.style; const { left, top, width, height } = this.getAbsoluteLayout(); const percentX = matchPercent(transformOriginX); const percentY = matchPercent(transformOriginY); const offsetX = percentX ? width * percentX.percent : transformOriginX; const offsetY = percentY ? height * percentY.percent : transformOriginY; return [left + offsetX, top + offsetY]; } set break(value) { this.props.break = value; } appendChild(child) { super.appendChild(child); this.root.markDirty(); } appendChildBefore(child,