volleyball-dev-frontend/node_modules/@react-pdf/render/lib/index.js
2025-06-02 16:42:16 +00:00

2076 lines
74 KiB
JavaScript

import * as P from '@react-pdf/primitives';
import { isNil, matchPercent } from '@react-pdf/fns';
import absPath from 'abs-svg-path';
import parsePath from 'parse-svg-path';
import normalizePath from 'normalize-svg-path';
import colorString from 'color-string';
const renderPath = (ctx, node) => {
const d = node.props?.d;
if (d)
ctx.path(node.props.d);
};
const KAPPA$3 = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0);
const renderRect = (ctx, node) => {
const x = node.props?.x || 0;
const y = node.props?.y || 0;
const rx = node.props?.rx || 0;
const ry = node.props?.ry || 0;
const width = node.props?.width || 0;
const height = node.props?.height || 0;
if (!width || !height)
return;
if (rx && ry) {
const krx = rx * KAPPA$3;
const kry = ry * KAPPA$3;
ctx.moveTo(x + rx, y);
ctx.lineTo(x - rx + width, y);
ctx.bezierCurveTo(x - rx + width + krx, y, x + width, y + ry - kry, x + width, y + ry);
ctx.lineTo(x + width, y + height - ry);
ctx.bezierCurveTo(x + width, y + height - ry + kry, x - rx + width + krx, y + height, x - rx + width, y + height);
ctx.lineTo(x + rx, y + height);
ctx.bezierCurveTo(x + rx - krx, y + height, x, y + height - ry + kry, x, y + height - ry);
ctx.lineTo(x, y + ry);
ctx.bezierCurveTo(x, y + ry - kry, x + rx - krx, y, x + rx, y);
}
else {
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
ctx.lineTo(x + width, y + height);
ctx.lineTo(x, y + height);
}
ctx.closePath();
};
const renderLine$1 = (ctx, node) => {
const { x1, x2, y1, y2 } = node.props || {};
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
};
const renderGroup = () => {
// noop
};
const KAPPA$2 = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0);
const drawEllipse = (ctx, rx, ry, cx = 0, cy = 0) => {
const x = cx - rx;
const y = cy - ry;
const ox = rx * KAPPA$2;
const oy = ry * KAPPA$2;
const xe = x + rx * 2;
const ye = y + ry * 2;
const xm = x + rx;
const ym = y + ry;
ctx.moveTo(x, ym);
ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
ctx.closePath();
};
const renderEllipse = (ctx, node) => {
const { cx, cy, rx, ry } = node.props || {};
drawEllipse(ctx, rx, ry, cx, cy);
};
const renderCircle = (ctx, node) => {
const cx = node.props?.cx;
const cy = node.props?.cy;
const r = node.props?.r;
drawEllipse(ctx, r, r, cx, cy);
};
/* eslint-disable no-return-assign */
const number = (n) => {
if (n > -1e21 && n < 1e21) {
return Math.round(n * 1e6) / 1e6;
}
throw new Error(`unsupported number: ${n}`);
};
const _renderGlyphs = (ctx, encoded, positions, x, y) => {
const commands = [];
const scale = ctx._fontSize / 1000;
let i;
let last = 0;
let hadOffset = false;
ctx.save();
// flip coordinate system
ctx.transform(1, 0, 0, -1, 0, ctx.page.height);
y = ctx.page.height - y;
// add current font to page if necessary
if (ctx.page.fonts[ctx._font.id] == null) {
ctx.page.fonts[ctx._font.id] = ctx._font.ref();
}
// begin the text object
ctx.addContent('BT');
// text position
ctx.addContent(`1 0 0 1 ${number(x)} ${number(y)} Tm`);
// font and font size
ctx.addContent(`/${ctx._font.id} ${number(ctx._fontSize)} Tf`);
// Adds a segment of text to the TJ command buffer
const addSegment = (cur) => {
if (last < cur) {
const hex = encoded.slice(last, cur).join('');
const advance = positions[cur - 1].xAdvance - positions[cur - 1].advanceWidth;
commands.push(`<${hex}> ${number(-advance)}`);
}
return (last = cur);
};
// Flushes the current TJ commands to the output stream
const flush = (s) => {
addSegment(s);
if (commands.length > 0) {
ctx.addContent(`[${commands.join(' ')}] TJ`);
return (commands.length = 0);
}
};
for (i = 0; i < positions.length; i += 1) {
// If we have an x or y offset, we have to break out of the current TJ command
// so we can move the text position.
const pos = positions[i];
if (pos.xOffset || pos.yOffset) {
// Flush the current buffer
flush(i);
// Move the text position and flush just the current character
ctx.addContent(`1 0 0 1 ${number(x + pos.xOffset * scale)} ${number(y + pos.yOffset * scale)} Tm`);
flush(i + 1);
hadOffset = true;
}
else {
// If the last character had an offset, reset the text position
if (hadOffset) {
ctx.addContent(`1 0 0 1 ${number(x)} ${number(y)} Tm`);
hadOffset = false;
}
// Group segments that don't have any advance adjustments
if (pos.xAdvance - pos.advanceWidth !== 0) {
addSegment(i + 1);
}
}
x += pos.xAdvance * scale;
}
// Flush any remaining commands
flush(i);
// end the text object
ctx.addContent('ET');
// restore flipped coordinate system
return ctx.restore();
};
const renderGlyphs = (ctx, glyphs, positions, x, y) => {
const scale = 1000 / ctx._fontSize;
const unitsPerEm = ctx._font.font.unitsPerEm || 1000;
const advanceWidthScale = 1000 / unitsPerEm;
// Glyph encoding and positioning
const encodedGlyphs = ctx._font.encodeGlyphs(glyphs);
const encodedPositions = positions.map((pos, i) => ({
xAdvance: pos.xAdvance * scale,
yAdvance: pos.yAdvance * scale,
xOffset: pos.xOffset,
yOffset: pos.yOffset,
advanceWidth: glyphs[i].advanceWidth * advanceWidthScale,
}));
return _renderGlyphs(ctx, encodedGlyphs, encodedPositions, x, y);
};
const renderRun$1 = (ctx, run) => {
if (!run.glyphs)
return;
if (!run.positions)
return;
const runAdvanceWidth = run.xAdvance;
const font = run.attributes.font?.[0];
const { fontSize, color, opacity } = run.attributes;
if (color)
ctx.fillColor(color);
ctx.fillOpacity(opacity);
if (font) {
ctx.font(font.type === 'STANDARD' ? font.fullName : font, fontSize);
}
try {
renderGlyphs(ctx, run.glyphs, run.positions, 0, 0);
}
catch (error) {
console.log(error);
}
ctx.translate(runAdvanceWidth, 0);
};
const renderSpan = (ctx, line, textAnchor, dominantBaseline) => {
ctx.save();
const x = line.box?.x || 0;
const y = line.box?.y || 0;
const font = line.runs[0]?.attributes.font?.[0];
const scale = line.runs[0]?.attributes?.scale || 1;
const width = line.xAdvance;
if (!font)
return;
const ascent = font.ascent * scale;
const xHeight = font.xHeight * scale;
const descent = font.descent * scale;
const capHeight = font.capHeight * scale;
let xTranslate = x;
let yTranslate = y;
switch (textAnchor) {
case 'middle':
xTranslate = x - width / 2;
break;
case 'end':
xTranslate = x - width;
break;
default:
xTranslate = x;
break;
}
switch (dominantBaseline) {
case 'middle':
case 'central':
yTranslate = y + capHeight / 2;
break;
case 'hanging':
yTranslate = y + capHeight;
break;
case 'mathematical':
yTranslate = y + xHeight;
break;
case 'text-after-edge':
yTranslate = y + descent;
break;
case 'text-before-edge':
yTranslate = y + ascent;
break;
default:
yTranslate = y;
break;
}
ctx.translate(xTranslate, yTranslate);
line.runs.forEach((run) => renderRun$1(ctx, run));
ctx.restore();
};
const renderSvgText = (ctx, node) => {
const children = node.children;
children.forEach((span) => renderSpan(ctx, span.lines[0], span.props.textAnchor, span.props.dominantBaseline));
};
const pairs = (values) => {
const result = [];
for (let i = 0; i < values.length; i += 2) {
result.push([values[i], values[i + 1]]);
}
return result;
};
/**
* Parse svg-like points into number arrays
*
* @param points string ex. "20,30 50,60"
* @returns points array ex. [[20, 30], [50, 60]]
*/
const parsePoints = (points) => {
let values = (points || '')
.trim()
.replace(/,/g, ' ')
.replace(/(\d)-(\d)/g, '$1 -$2')
.split(/\s+/);
if (values.length % 2 !== 0) {
values = values.slice(0, -1);
}
const mappedValues = values.map(parseFloat);
return pairs(mappedValues);
};
const drawPolyline = (ctx, points) => {
if (points.length > 0) {
ctx.moveTo(points[0][0], points[0][1]);
points.slice(1).forEach((p) => ctx.lineTo(p[0], p[1]));
}
};
const renderPolyline = (ctx, node) => {
const points = parsePoints(node.props.points || '');
drawPolyline(ctx, points);
};
const renderPolygon = (ctx, node) => {
const points = parsePoints(node.props.points || '');
drawPolyline(ctx, points);
ctx.closePath();
};
const renderImage$1 = (ctx, node) => {
if (!node.box)
return;
if (!node.image?.data)
return;
const { x = 0, y = 0 } = node.props;
const { width, height, opacity } = node.style;
const paddingTop = node.box.paddingLeft || 0;
const paddingLeft = node.box.paddingLeft || 0;
if (width === 0 || height === 0) {
console.warn(`Image with src '${node.props.href}' skipped due to invalid dimensions`);
return;
}
if (typeof width === 'string' || typeof height === 'string') {
console.warn(`Image with src '${node.props.href}' skipped due to percentage width or height`);
return;
}
ctx.save();
ctx
.fillOpacity(opacity || 1)
.image(node.image.data, x + paddingLeft, y + paddingTop, {
width,
height,
});
ctx.restore();
};
// 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 clipNode = (ctx, node) => {
if (!node.box)
return;
if (!node.style)
return;
const { top, left, width, height } = node.box;
const { borderTopLeftRadius = 0, borderTopRightRadius = 0, borderBottomRightRadius = 0, borderBottomLeftRadius = 0, } = node.style;
// Border top
// @ts-expect-error this is always a number due to resolve border radius step
const rtr = Math.min(borderTopRightRadius, 0.5 * width, 0.5 * height);
const ctr = rtr * (1.0 - KAPPA$1);
ctx.moveTo(left + rtr, top);
ctx.lineTo(left + width - rtr, top);
ctx.bezierCurveTo(left + width - ctr, top, left + width, top + ctr, left + width, top + rtr);
// Border right
// @ts-expect-error this is always a number due to resolve border radius step
const rbr = Math.min(borderBottomRightRadius, 0.5 * width, 0.5 * height);
const cbr = rbr * (1.0 - KAPPA$1);
ctx.lineTo(left + width, top + height - rbr);
ctx.bezierCurveTo(left + width, top + height - cbr, left + width - cbr, top + height, left + width - rbr, top + height);
// Border bottom
// @ts-expect-error this is always a number due to resolve border radius step
const rbl = Math.min(borderBottomLeftRadius, 0.5 * width, 0.5 * height);
const cbl = rbl * (1.0 - KAPPA$1);
ctx.lineTo(left + rbl, top + height);
ctx.bezierCurveTo(left + cbl, top + height, left, top + height - cbl, left, top + height - rbl);
// Border left
// @ts-expect-error this is always a number due to resolve border radius step
const rtl = Math.min(borderTopLeftRadius, 0.5 * width, 0.5 * height);
const ctl = rtl * (1.0 - KAPPA$1);
ctx.lineTo(left, top + rtl);
ctx.bezierCurveTo(left, top + ctl, left + ctl, top, left + rtl, top);
ctx.closePath();
ctx.clip();
};
const applySingleTransformation = (ctx, transform, origin) => {
const { operation, value } = transform;
switch (operation) {
case 'scale': {
const [scaleX, scaleY] = value;
ctx.scale(scaleX, scaleY, { origin });
break;
}
case 'rotate': {
const [angle] = value;
ctx.rotate(angle, { origin });
break;
}
case 'translate': {
const [x, y = 0] = value;
ctx.translate(x, y, { origin });
break;
}
case 'skew': {
const [xAngle = 0, yAngle = 0] = value;
const radx = (xAngle * Math.PI) / 180;
const rady = (yAngle * Math.PI) / 180;
const tanx = Math.tan(radx);
const tany = Math.tan(rady);
let x = 0;
let y = 0;
if (origin != null) {
[x, y] = Array.from(origin);
const x1 = x + tanx * y;
const y1 = y + tany * x;
x -= x1;
y -= y1;
}
ctx.transform(1, tany, tanx, 1, x, y);
break;
}
case 'matrix': {
ctx.transform(...value);
break;
}
default: {
console.error(`Transform operation: '${operation}' doesn't supported`);
}
}
};
const applyTransformations = (ctx, node) => {
if (!node.origin)
return;
const { props, style } = node;
const origin = [node.origin.left, node.origin.top];
const propsTransform = 'transform' in props ? props.transform : undefined;
const operations = style?.transform || propsTransform || [];
operations.forEach((operation) => {
applySingleTransformation(ctx, operation, origin);
});
};
// From https://github.com/dy/svg-path-bounds/blob/master/index.js
const getPathBoundingBox = (node) => {
const path = normalizePath(absPath(parsePath(node.props?.d || '')));
if (!path.length)
return [0, 0, 0, 0];
const bounds = [Infinity, Infinity, -Infinity, -Infinity];
for (let i = 0, l = path.length; i < l; i += 1) {
const points = path[i].slice(1);
for (let j = 0; j < points.length; j += 2) {
if (points[j + 0] < bounds[0])
bounds[0] = points[j + 0];
if (points[j + 1] < bounds[1])
bounds[1] = points[j + 1];
if (points[j + 0] > bounds[2])
bounds[2] = points[j + 0];
if (points[j + 1] > bounds[3])
bounds[3] = points[j + 1];
}
}
return bounds;
};
const getCircleBoundingBox = (node) => {
const r = node.props?.r || 0;
const cx = node.props?.cx || 0;
const cy = node.props?.cy || 0;
return [cx - r, cy - r, cx + r, cy + r];
};
const getEllipseBoundingBox = (node) => {
const cx = node.props?.cx || 0;
const cy = node.props?.cy || 0;
const rx = node.props?.rx || 0;
const ry = node.props?.ry || 0;
return [cx - rx, cy - ry, cx + rx, cy + ry];
};
const getLineBoundingBox = (node) => {
const x1 = node.props?.x1 || 0;
const y1 = node.props?.y1 || 0;
const x2 = node.props?.x2 || 0;
const y2 = node.props?.y2 || 0;
return [
Math.min(x1, x2),
Math.min(y1, y2),
Math.max(x1, x2),
Math.max(y1, y2),
];
};
const getRectBoundingBox = (node) => {
const x = node.props?.x || 0;
const y = node.props?.y || 0;
const width = node.props?.width || 0;
const height = node.props?.height || 0;
return [x, y, x + width, y + height];
};
const max = (values) => Math.max(-Infinity, ...values);
const min = (values) => Math.min(Infinity, ...values);
const getPolylineBoundingBox = (node) => {
const points = parsePoints(node.props?.points);
const xValues = points.map((p) => p[0]);
const yValues = points.map((p) => p[1]);
return [min(xValues), min(yValues), max(xValues), max(yValues)];
};
const boundingBoxFns = {
[P.Rect]: getRectBoundingBox,
[P.Line]: getLineBoundingBox,
[P.Path]: getPathBoundingBox,
[P.Circle]: getCircleBoundingBox,
[P.Ellipse]: getEllipseBoundingBox,
[P.Polygon]: getPolylineBoundingBox,
[P.Polyline]: getPolylineBoundingBox,
};
const getBoundingBox = (node) => {
const boundingBoxFn = boundingBoxFns[node.type];
return boundingBoxFn ? boundingBoxFn(node) : [0, 0, 0, 0];
};
const setStrokeWidth = (ctx, node) => {
if (!node.props)
return;
if (!('strokeWidth' in node.props))
return;
const lineWidth = node.props.strokeWidth;
if (lineWidth)
ctx.lineWidth(lineWidth);
};
const setStrokeColor = (ctx, node) => {
if (!node.props)
return;
if (!('stroke' in node.props))
return;
const strokeColor = node.props.stroke;
if (strokeColor)
ctx.strokeColor(strokeColor);
};
const setOpacity = (ctx, node) => {
if (!node.props)
return;
if (!('opacity' in node.props))
return;
const opacity = node.props.opacity;
if (!isNil(opacity))
ctx.opacity(opacity);
};
const setFillOpacity = (ctx, node) => {
if (!node.props)
return;
if (!('fillOpacity' in node.props))
return;
const fillOpacity = node.props.fillOpacity || null;
if (!isNil(fillOpacity))
ctx.fillOpacity(fillOpacity);
};
const setStrokeOpacity = (ctx, node) => {
if (!node.props)
return;
if (!('strokeOpacity' in node.props))
return;
const strokeOpacity = node.props?.strokeOpacity;
if (!isNil(strokeOpacity))
ctx.strokeOpacity(strokeOpacity);
};
const setLineJoin = (ctx, node) => {
if (!node.props)
return;
if (!('strokeLinejoin' in node.props))
return;
const lineJoin = node.props.strokeLinejoin;
if (lineJoin)
ctx.lineJoin(lineJoin);
};
const setLineCap = (ctx, node) => {
if (!node.props)
return;
if (!('strokeLinecap' in node.props))
return;
const lineCap = node.props?.strokeLinecap;
if (lineCap)
ctx.lineCap(lineCap);
};
const setLineDash = (ctx, node) => {
if (!node.props)
return;
if (!('strokeDasharray' in node.props))
return;
const value = node.props?.strokeDasharray || null;
// @ts-expect-error check this works as expected
if (value)
ctx.dash(value.split(/[\s,]+/).map(Number));
};
const hasLinearGradientFill = (node) => {
if (!node.props)
return false;
if (!('fill' in node.props))
return false;
if (typeof node.props.fill === 'string')
return false;
return node.props.fill?.type === P.LinearGradient;
};
const hasRadialGradientFill = (node) => {
if (!node.props)
return false;
if (!('fill' in node.props))
return false;
if (typeof node.props.fill === 'string')
return false;
return node.props.fill?.type === P.RadialGradient;
};
function multiplyMatrices(m1, m2) {
const a = m1[0] * m2[0] + m1[2] * m2[1];
const b = m1[1] * m2[0] + m1[3] * m2[1];
const c = m1[0] * m2[2] + m1[2] * m2[3];
const d = m1[1] * m2[2] + m1[3] * m2[3];
const e = m1[0] * m2[4] + m1[2] * m2[5] + m1[4];
const f = m1[1] * m2[4] + m1[3] * m2[5] + m1[5];
return [a, b, c, d, e, f];
}
const transformGradient = (grad, transforms, bbox, units) => {
const matrices = transforms.map((transform) => {
switch (transform.operation) {
case 'scale': {
const value = transform.value;
return [value[0], 0, 0, value[1], 0, 0];
}
case 'translate': {
const value = transform.value;
let x = value[0] || 0;
let y = value[1] || 0;
if (units === 'objectBoundingBox') {
x = (bbox[2] - bbox[0]) * x;
y = (bbox[3] - bbox[1]) * y;
}
return [1, 0, 0, 1, x, y];
}
case 'rotate': {
const value = transform.value;
const cos = Math.cos(value[0]);
const sin = Math.sin(value[0]);
return [cos, sin, -sin, cos, 0, 0];
}
case 'skew': {
const value = transform.value;
return [1, Math.tan(value[0]), Math.tan(value[1]), 1, 0, 0];
}
case 'matrix': {
const value = transform.value;
let x = value[4] || 0;
let y = value[5] || 0;
if (units === 'objectBoundingBox') {
x = (bbox[2] - bbox[0]) * x;
y = (bbox[3] - bbox[1]) * y;
}
return [value[0], value[1], value[2], value[3], x, y];
}
default:
return [1, 0, 0, 1, 0, 0];
}
});
const matrix = matrices.reduce(multiplyMatrices, [1, 0, 0, 1, 0, 0]);
grad.setTransform(...matrix);
};
// Math simplified from https://github.com/devongovett/svgkit/blob/master/src/elements/SVGGradient.js#L104
const setLinearGradientFill = (ctx, node) => {
if (!node.props)
return;
if (!('fill' in node.props))
return;
const bbox = getBoundingBox(node);
const gradient = node.props?.fill;
if (!gradient)
return;
const units = gradient.props.gradientUnits || 'objectBoundingBox';
const transforms = gradient.props.gradientTransform || [];
let x1 = gradient.props.x1 || 0;
let y1 = gradient.props.y1 || 0;
let x2 = gradient.props.x2 || 1;
let y2 = gradient.props.y2 || 0;
if (units === 'objectBoundingBox') {
const m0 = bbox[2] - bbox[0];
const m3 = bbox[3] - bbox[1];
const m4 = bbox[0];
const m5 = bbox[1];
x1 = m0 * x1 + m4;
y1 = m3 * y1 + m5;
x2 = m0 * x2 + m4;
y2 = m3 * y2 + m5;
}
const grad = ctx.linearGradient(x1, y1, x2, y2);
transformGradient(grad, transforms, bbox, units);
gradient.children?.forEach((stop) => {
grad.stop(stop.props.offset, stop.props.stopColor, stop.props.stopOpacity);
});
ctx.fill(grad);
};
// Math simplified from https://github.com/devongovett/svgkit/blob/master/src/elements/SVGGradient.js#L155
const setRadialGradientFill = (ctx, node) => {
if (!node.props)
return;
if (!('fill' in node.props))
return;
const bbox = getBoundingBox(node);
const gradient = node.props?.fill;
if (!gradient)
return;
const units = gradient.props.gradientUnits || 'objectBoundingBox';
const transforms = gradient.props.gradientTransform || [];
let r = gradient.props.r || 0.5;
let cx = gradient.props.cx || 0.5;
let cy = gradient.props.cy || 0.5;
let fx = gradient.props.fx || cx;
let fy = gradient.props.fy || cy;
if (units === 'objectBoundingBox') {
const m0 = bbox[2] - bbox[0];
const m3 = bbox[3] - bbox[1];
const m4 = bbox[0];
const m5 = bbox[1];
r = r * m0;
cx = m0 * cx + m4;
cy = m3 * cy + m5;
fx = m0 * fx + m4;
fy = m3 * fy + m5;
}
const grad = ctx.radialGradient(cx, cy, 0, fx, fy, r);
transformGradient(grad, transforms, bbox, units);
gradient.children?.forEach((stop) => {
grad.stop(stop.props.offset, stop.props.stopColor, stop.props.stopOpacity);
});
ctx.fill(grad);
};
const setFillColor = (ctx, node) => {
if (!node.props)
return;
if (!('fill' in node.props))
return;
const fillColor = node.props?.fill;
if (fillColor)
ctx.fillColor(fillColor);
};
const setFill = (ctx, node) => {
if (hasLinearGradientFill(node))
return setLinearGradientFill(ctx, node);
if (hasRadialGradientFill(node))
return setRadialGradientFill(ctx, node);
return setFillColor(ctx, node);
};
const draw = (ctx, node) => {
const props = node.props || {};
if ('fill' in props && 'stroke' in props && props.fill && props.stroke) {
ctx.fillAndStroke(props.fillRule);
}
else if ('fill' in props && props.fill) {
ctx.fill(props.fillRule);
}
else if ('stroke' in props && props.stroke) {
ctx.stroke();
}
else {
ctx.save();
ctx.opacity(0);
ctx.fill(null);
ctx.restore();
}
};
const noop = () => { };
const renderFns$1 = {
[P.Tspan]: noop,
[P.TextInstance]: noop,
[P.Path]: renderPath,
[P.Rect]: renderRect,
[P.Line]: renderLine$1,
[P.G]: renderGroup,
[P.Text]: renderSvgText,
[P.Circle]: renderCircle,
[P.Image]: renderImage$1,
[P.Ellipse]: renderEllipse,
[P.Polygon]: renderPolygon,
[P.Polyline]: renderPolyline,
};
const renderNode$1 = (ctx, node) => {
const renderFn = renderFns$1[node.type];
if (renderFn) {
renderFn(ctx, node);
}
else {
console.warn(`SVG node of type ${node.type} is not currently supported`);
}
};
const drawNode = (ctx, node) => {
setLineCap(ctx, node);
setLineDash(ctx, node);
setLineJoin(ctx, node);
setStrokeWidth(ctx, node);
setStrokeColor(ctx, node);
setFill(ctx, node);
setStrokeOpacity(ctx, node);
setFillOpacity(ctx, node);
setOpacity(ctx, node);
applyTransformations(ctx, node);
renderNode$1(ctx, node);
draw(ctx, node);
};
const clipPath = (ctx, node) => {
if (!node.props)
return;
if (!('clipPath' in node.props))
return;
const value = node.props.clipPath;
if (value) {
const children = value.children || [];
children.forEach((child) => renderNode$1(ctx, child));
ctx.clip();
}
};
const drawChildren = (ctx, node) => {
const children = node.children || [];
children.forEach((child) => {
ctx.save();
clipPath(ctx, child);
drawNode(ctx, child);
drawChildren(ctx, child);
ctx.restore();
});
};
const resolveAspectRatio = (ctx, node) => {
if (!node.box)
return;
const { width, height } = node.box;
const { viewBox, preserveAspectRatio } = node.props;
const { meetOrSlice = 'meet', align = 'xMidYMid' } = preserveAspectRatio || {};
if (viewBox == null || width == null || height == null)
return;
const x = viewBox?.minX || 0;
const y = viewBox?.minY || 0;
const logicalWidth = viewBox?.maxX || width;
const logicalHeight = viewBox?.maxY || height;
const logicalRatio = logicalWidth / logicalHeight;
const physicalRatio = width / height;
const scaleX = width / logicalWidth;
const scaleY = height / logicalHeight;
if (align === 'none') {
ctx.scale(scaleX, scaleY);
ctx.translate(-x, -y);
return;
}
if ((logicalRatio < physicalRatio && meetOrSlice === 'meet') ||
(logicalRatio >= physicalRatio && meetOrSlice === 'slice')) {
ctx.scale(scaleY, scaleY);
switch (align) {
case 'xMinYMin':
case 'xMinYMid':
case 'xMinYMax':
ctx.translate(-x, -y);
break;
case 'xMidYMin':
case 'xMidYMid':
case 'xMidYMax':
ctx.translate(-x - (logicalWidth - (width * logicalHeight) / height) / 2, -y);
break;
default:
ctx.translate(-x - (logicalWidth - (width * logicalHeight) / height), -y);
}
}
else {
ctx.scale(scaleX, scaleX);
switch (align) {
case 'xMinYMin':
case 'xMidYMin':
case 'xMaxYMin':
ctx.translate(-x, -y);
break;
case 'xMinYMid':
case 'xMidYMid':
case 'xMaxYMid':
ctx.translate(-x, -y - (logicalHeight - (height * logicalWidth) / width) / 2);
break;
default:
ctx.translate(-x, -y - (logicalHeight - (height * logicalWidth) / width));
}
}
};
const moveToOrigin = (ctx, node) => {
if (!node.box)
return;
const { top, left } = node.box;
const paddingLeft = node.box.paddingLeft || 0;
const paddingTop = node.box.paddingTop || 0;
ctx.translate(left + paddingLeft, top + paddingTop);
};
const renderSvg = (ctx, node) => {
ctx.save();
clipNode(ctx, node);
moveToOrigin(ctx, node);
resolveAspectRatio(ctx, node);
drawChildren(ctx, node);
ctx.restore();
};
const black = { value: '#000', opacity: 1 };
// TODO: parse to number[] in layout to avoid this step
const parseColor = (hex) => {
if (!hex)
return black;
const parsed = colorString.get(hex);
if (!parsed)
return black;
const value = colorString.to.hex(parsed.value.slice(0, 3));
const opacity = parsed.value[3];
return { value, opacity };
};
const DEST_REGEXP = /^#.+/;
const isSrcId$1 = (src) => src.match(DEST_REGEXP);
const renderAttachment = (ctx, attachment) => {
const { xOffset = 0, yOffset = 0, width, height, image } = attachment;
ctx.translate(-width + xOffset, -height + yOffset);
ctx.image(image, 0, 0, {
fit: [width, height],
align: 'center',
valign: 'bottom',
});
};
const renderAttachments = (ctx, run) => {
if (!run.glyphs)
return;
if (!run.positions)
return;
const font = run.attributes.font?.[0];
if (!font)
return;
ctx.save();
const space = font.glyphForCodePoint(0x20);
const objectReplacement = font.glyphForCodePoint(0xfffc);
let attachmentAdvance = 0;
for (let i = 0; i < run.glyphs.length; i += 1) {
const position = run.positions[i];
const glyph = run.glyphs[i];
attachmentAdvance += position.xAdvance || 0;
if (glyph.id === objectReplacement.id && run.attributes.attachment) {
ctx.translate(attachmentAdvance, position.yOffset || 0);
renderAttachment(ctx, run.attributes.attachment);
run.glyphs[i] = space;
attachmentAdvance = 0;
}
}
ctx.restore();
};
const renderRun = (ctx, run) => {
if (!run.glyphs)
return;
if (!run.positions)
return;
const font = run.attributes.font?.[0];
if (!font)
return;
const { fontSize, link } = run.attributes;
const color = parseColor(run.attributes.color);
const opacity = isNil(run.attributes.opacity)
? color.opacity
: run.attributes.opacity;
const { height = 0, descent = 0, xAdvance = 0 } = run;
ctx.fillColor(color.value);
ctx.fillOpacity(opacity);
if (link) {
if (isSrcId$1(link)) {
ctx.goTo(0, -height - descent, xAdvance, height, link.slice(1));
}
else {
ctx.link(0, -height - descent, xAdvance, height, link);
}
}
renderAttachments(ctx, run);
ctx.font(font.type === 'STANDARD' ? font.fullName : font, fontSize);
try {
renderGlyphs(ctx, run.glyphs, run.positions, 0, 0);
}
catch (error) {
console.log(error);
}
ctx.translate(xAdvance, 0);
};
const renderBackground$1 = (ctx, rect, backgroundColor) => {
const color = parseColor(backgroundColor);
ctx.save();
ctx.fillOpacity(color.opacity);
ctx.rect(rect.x, rect.y, rect.width, rect.height);
ctx.fill(color.value);
ctx.restore();
};
const renderDecorationLine = (ctx, decorationLine) => {
ctx.save();
ctx.lineWidth(decorationLine.rect.height);
ctx.strokeOpacity(decorationLine.opacity);
if (/dashed/.test(decorationLine.style)) {
ctx.dash(3 * decorationLine.rect.height, {});
}
else if (/dotted/.test(decorationLine.style)) {
ctx.dash(decorationLine.rect.height, {});
}
if (/wavy/.test(decorationLine.style)) {
const dist = Math.max(2, decorationLine.rect.height);
let step = 1.1 * dist;
const stepCount = Math.floor(decorationLine.rect.width / (2 * step));
// Adjust step to fill entire width
const remainingWidth = decorationLine.rect.width - stepCount * 2 * step;
const adjustment = remainingWidth / stepCount / 2;
step += adjustment;
const cp1y = decorationLine.rect.y + dist;
const cp2y = decorationLine.rect.y - dist;
let { x } = decorationLine.rect;
ctx.moveTo(decorationLine.rect.x, decorationLine.rect.y);
for (let i = 0; i < stepCount; i += 1) {
ctx.bezierCurveTo(x + step, cp1y, x + step, cp2y, x + 2 * step, decorationLine.rect.y);
x += 2 * step;
}
}
else {
ctx.moveTo(decorationLine.rect.x, decorationLine.rect.y);
ctx.lineTo(decorationLine.rect.x + decorationLine.rect.width, decorationLine.rect.y);
if (/double/.test(decorationLine.style)) {
ctx.moveTo(decorationLine.rect.x, decorationLine.rect.y + decorationLine.rect.height * 2);
ctx.lineTo(decorationLine.rect.x + decorationLine.rect.width, decorationLine.rect.y + decorationLine.rect.height * 2);
}
}
ctx.stroke(decorationLine.color);
ctx.restore();
};
const renderLine = (ctx, line) => {
if (!line.box)
return;
const lineAscent = line.ascent || 0;
ctx.save();
ctx.translate(line.box.x, line.box.y + lineAscent);
for (let i = 0; i < line.runs.length; i += 1) {
const run = line.runs[i];
const isLastRun = i === line.runs.length - 1;
if (run.attributes.backgroundColor) {
const xAdvance = run.xAdvance ?? 0;
const overflowRight = isLastRun ? line.overflowRight ?? 0 : 0;
const backgroundRect = {
x: 0,
y: -lineAscent,
height: line.box.height,
width: xAdvance - overflowRight,
};
renderBackground$1(ctx, backgroundRect, run.attributes.backgroundColor);
}
renderRun(ctx, run);
}
ctx.restore();
ctx.save();
ctx.translate(line.box.x, line.box.y);
if (line.decorationLines) {
for (let i = 0; i < line.decorationLines.length; i += 1) {
const decorationLine = line.decorationLines[i];
renderDecorationLine(ctx, decorationLine);
}
}
ctx.restore();
};
const renderBlock = (ctx, block) => {
block.forEach((line) => {
renderLine(ctx, line);
});
};
const renderText = (ctx, node) => {
if (!node.box)
return;
if (!node.lines)
return;
const { top, left } = node.box;
const blocks = [node.lines];
const paddingTop = node.box?.paddingTop || 0;
const paddingLeft = node.box?.paddingLeft || 0;
const initialY = node.lines[0] ? node.lines[0].box.y : 0;
const offsetX = node.alignOffset || 0;
ctx.save();
ctx.translate(left + paddingLeft - offsetX, top + paddingTop - initialY);
blocks.forEach((block) => {
renderBlock(ctx, block);
});
ctx.restore();
};
const renderPage = (ctx, node) => {
if (!node.box)
return;
const { width, height } = node.box;
const dpi = node.props?.dpi || 72;
const userUnit = dpi / 72;
ctx.addPage({ size: [width, height], margin: 0, userUnit });
};
const renderNote = (ctx, node) => {
if (!node.box)
return;
const { top, left } = node.box;
const value = node?.children?.[0].value || '';
const color = node.style?.backgroundColor;
ctx.note(left, top, 0, 0, value, { color });
};
const embedImage = (ctx, node) => {
const src = node.image.data;
let image;
if (typeof src === 'string') {
image = ctx._imageRegistry[src];
}
if (!image) {
image = ctx.openImage(src);
}
if (!image.obj) {
image.embed(ctx);
}
return image;
};
const isNumeric = (n) => {
return !Number.isNaN(parseFloat(n)) && Number.isFinite(n);
};
const applyContainObjectFit = (cw, ch, iw, ih, px, py) => {
const cr = cw / ch;
const ir = iw / ih;
const pxp = matchPercent(px ?? null);
const pyp = matchPercent(py ?? null);
const pxv = pxp ? pxp.percent : 0.5;
const pyv = pyp ? pyp.percent : 0.5;
if (cr > ir) {
const height = ch;
const width = height * ir;
const yOffset = isNumeric(py) ? py : 0;
const xOffset = isNumeric(px) ? px : (cw - width) * pxv;
return { width, height, xOffset, yOffset };
}
const width = cw;
const height = width / ir;
const xOffset = isNumeric(px) ? px : 0;
const yOffset = isNumeric(py) ? py : (ch - height) * pyv;
return { width, height, yOffset, xOffset };
};
const applyNoneObjectFit = (cw, ch, iw, ih, px, py) => {
const width = iw;
const height = ih;
const pxp = matchPercent(px ?? null);
const pyp = matchPercent(py ?? null);
const pxv = pxp ? pxp.percent : 0.5;
const pyv = pyp ? pyp.percent : 0.5;
const xOffset = isNumeric(px) ? px : (cw - width) * pxv;
const yOffset = isNumeric(py) ? py : (ch - height) * pyv;
return { width, height, xOffset, yOffset };
};
const applyCoverObjectFit = (cw, ch, iw, ih, px, py) => {
const ir = iw / ih;
const cr = cw / ch;
const pxp = matchPercent(px ?? null);
const pyp = matchPercent(py ?? null);
const pxv = pxp ? pxp.percent : 0.5;
const pyv = pyp ? pyp.percent : 0.5;
if (cr > ir) {
const width = cw;
const height = width / ir;
const xOffset = isNumeric(px) ? px : 0;
const yOffset = isNumeric(py) ? py : (ch - height) * pyv;
return { width, height, yOffset, xOffset };
}
const height = ch;
const width = height * ir;
const xOffset = isNumeric(px) ? px : (cw - width) * pxv;
const yOffset = isNumeric(py) ? py : 0;
return { width, height, xOffset, yOffset };
};
const applyScaleDownObjectFit = (cw, ch, iw, ih, px, py) => {
const containDimension = applyContainObjectFit(cw, ch, iw, ih, px, py);
const noneDimension = applyNoneObjectFit(cw, ch, iw, ih, px, py);
return containDimension.width < noneDimension.width
? containDimension
: noneDimension;
};
const applyFillObjectFit = (cw, ch, px, py) => {
return {
width: cw,
height: ch,
xOffset: matchPercent(px ?? null) ? 0 : px || 0,
yOffset: matchPercent(py ?? null) ? 0 : py || 0,
};
};
const resolveObjectFit = (type = 'fill', cw, ch, iw, ih, px, py) => {
switch (type) {
case 'contain':
return applyContainObjectFit(cw, ch, iw, ih, px, py);
case 'cover':
return applyCoverObjectFit(cw, ch, iw, ih, px, py);
case 'none':
return applyNoneObjectFit(cw, ch, iw, ih, px, py);
case 'scale-down':
return applyScaleDownObjectFit(cw, ch, iw, ih, px, py);
default:
return applyFillObjectFit(cw, ch, px, py);
}
};
const drawImage = (ctx, node, options) => {
if (!node.box)
return;
if (!node.image)
return;
const { left, top } = node.box;
const opacity = node.style?.opacity;
const objectFit = node.style?.objectFit;
const objectPositionX = node.style?.objectPositionX;
const objectPositionY = node.style?.objectPositionY;
const paddingTop = node.box.paddingTop || 0;
const paddingRight = node.box.paddingRight || 0;
const paddingBottom = node.box.paddingBottom || 0;
const paddingLeft = node.box.paddingLeft || 0;
const imageCache = options.imageCache || new Map();
const { width, height, xOffset, yOffset } = resolveObjectFit(objectFit, node.box.width - paddingLeft - paddingRight, node.box.height - paddingTop - paddingBottom, node.image.width, node.image.height, objectPositionX, objectPositionY);
if (node.image.data) {
if (width !== 0 && height !== 0) {
const cacheKey = node.image.key;
const image = imageCache.get(cacheKey) || embedImage(ctx, node);
if (cacheKey)
imageCache.set(cacheKey, image);
const imageOpacity = isNil(opacity) ? 1 : opacity;
ctx
.fillOpacity(imageOpacity)
.image(image, left + paddingLeft + xOffset, top + paddingTop + yOffset, {
width,
height,
});
}
else {
console.warn(`Image with src '${JSON.stringify(node.props.src || node.props.source)}' skipped due to invalid dimensions`);
}
}
};
const renderImage = (ctx, node, options) => {
ctx.save();
clipNode(ctx, node);
drawImage(ctx, node, options);
ctx.restore();
};
const CONTENT_COLOR = '#a1c6e7';
const PADDING_COLOR = '#c4deb9';
const MARGIN_COLOR = '#f8cca1';
// TODO: Draw debug boxes using clipping to enhance quality
const debugContent = (ctx, node) => {
if (!node.box)
return;
const { left, top, width, height, paddingLeft = 0, paddingTop = 0, paddingRight = 0, paddingBottom = 0, borderLeftWidth = 0, borderTopWidth = 0, borderRightWidth = 0, borderBottomWidth = 0, } = node.box;
ctx
.fillColor(CONTENT_COLOR)
.opacity(0.5)
.rect(left + paddingLeft + borderLeftWidth, top + paddingTop + borderTopWidth, width - paddingLeft - paddingRight - borderRightWidth - borderLeftWidth, height - paddingTop - paddingBottom - borderTopWidth - borderBottomWidth)
.fill();
};
const debugPadding = (ctx, node) => {
if (!node.box)
return;
const { left, top, width, height, paddingLeft = 0, paddingTop = 0, paddingRight = 0, paddingBottom = 0, borderLeftWidth = 0, borderTopWidth = 0, borderRightWidth = 0, borderBottomWidth = 0, } = node.box;
ctx.fillColor(PADDING_COLOR).opacity(0.5);
// Padding top
ctx
.rect(left + paddingLeft + borderLeftWidth, top + borderTopWidth, width - paddingRight - paddingLeft - borderLeftWidth - borderRightWidth, paddingTop)
.fill();
// Padding left
ctx
.rect(left + borderLeftWidth, top + borderTopWidth, paddingLeft, height - borderTopWidth - borderBottomWidth)
.fill();
// Padding right
ctx
.rect(left + width - paddingRight - borderRightWidth, top + borderTopWidth, paddingRight, height - borderTopWidth - borderBottomWidth)
.fill();
// Padding bottom
ctx
.rect(left + paddingLeft + borderLeftWidth, top + height - paddingBottom - borderBottomWidth, width - paddingRight - paddingLeft - borderLeftWidth - borderRightWidth, paddingBottom)
.fill();
};
const debugMargin = (ctx, node) => {
if (!node.box)
return;
const { left, top, width, height } = node.box;
const { marginLeft = 0, marginTop = 0, marginRight = 0, marginBottom = 0, } = node.box;
ctx.fillColor(MARGIN_COLOR).opacity(0.5);
// Margin top
ctx.rect(left, top - marginTop, width, marginTop).fill();
// Margin left
ctx
.rect(left - marginLeft, top - marginTop, marginLeft, height + marginTop + marginBottom)
.fill();
// Margin right
ctx
.rect(left + width, top - marginTop, marginRight, height + marginTop + marginBottom)
.fill();
// Margin bottom
ctx.rect(left, top + height, width, marginBottom).fill();
};
const debugText = (ctx, node) => {
if (!node.box)
return;
const { left, top, width, height } = node.box;
const { marginLeft = 0, marginTop = 0, marginRight = 0, marginBottom = 0, } = node.box;
const roundedWidth = Math.round(width + marginLeft + marginRight);
const roundedHeight = Math.round(height + marginTop + marginBottom);
ctx
.fontSize(6)
.opacity(1)
.fillColor('black')
.text(`${roundedWidth} x ${roundedHeight}`, left - marginLeft, Math.max(top - marginTop - 4, 1), { width: Infinity });
};
const debugOrigin = (ctx, node) => {
if (node.origin) {
ctx
.circle(node.origin.left, node.origin.top, 3)
.fill('red')
.circle(node.origin.left, node.origin.top, 5)
.stroke('red');
}
};
const renderDebug = (ctx, node) => {
if (!node.props)
return;
if (!('debug' in node.props) || !node.props.debug)
return;
ctx.save();
debugContent(ctx, node);
debugPadding(ctx, node);
debugMargin(ctx, node);
debugText(ctx, node);
debugOrigin(ctx, node);
ctx.restore();
};
const availableMethods = [
'dash',
'clip',
'save',
'path',
'fill',
'font',
'text',
'rect',
'scale',
'moveTo',
'lineTo',
'stroke',
'rotate',
'circle',
'lineCap',
'opacity',
'ellipse',
'polygon',
'restore',
'lineJoin',
'fontSize',
'fillColor',
'lineWidth',
'translate',
'miterLimit',
'strokeColor',
'fillOpacity',
'roundedRect',
'fillAndStroke',
'strokeOpacity',
'bezierCurveTo',
'quadraticCurveTo',
'linearGradient',
'radialGradient',
];
const painter = (ctx) => {
const p = availableMethods.reduce((acc, prop) => ({
...acc,
[prop]: (...args) => {
// @ts-expect-error ctx[prop] is a function
ctx[prop](...args);
return p;
},
}), {});
return p;
};
const renderCanvas = (ctx, node) => {
if (!node.box)
return;
const { top, left, width, height } = node.box;
const paddingTop = node.box.paddingTop || 0;
const paddingLeft = node.box.paddingLeft || 0;
const paddingRight = node.box.paddingRight || 0;
const paddingBottom = node.box.paddingBottom || 0;
const availableWidth = width - paddingLeft - paddingRight;
const availableHeight = height - paddingTop - paddingBottom;
if (!availableWidth || !availableHeight) {
console.warn('Canvas element has null width or height. Please provide valid values via the `style` prop in order to correctly render it.');
}
ctx.save().translate(left + paddingLeft, top + paddingTop);
if (node.props.paint) {
node.props.paint(painter(ctx), availableWidth, availableHeight);
}
ctx.restore();
};
// 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);
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();
};
const shouldRenderBorders = (node) => node.box &&
(node.box.borderTopWidth ||
node.box.borderRightWidth ||
node.box.borderBottomWidth ||
node.box.borderLeftWidth);
const renderBorders = (ctx, node) => {
if (!node.box)
return;
if (!shouldRenderBorders(node))
return;
const { width, height, borderTopWidth = 0, borderLeftWidth = 0, borderRightWidth = 0, borderBottomWidth = 0, } = node.box;
const { opacity = 1, borderTopColor = 'black', borderTopStyle = 'solid', borderLeftColor = 'black', borderLeftStyle = 'solid', borderRightColor = 'black', borderRightStyle = 'solid', borderBottomColor = 'black', borderBottomStyle = 'solid', } = node.style;
// @ts-expect-error this is always a number due to resolve border radius step
const borderTopLeftRadius = node.style.borderTopLeftRadius || 0;
// @ts-expect-error this is always a number due to resolve border radius step
const borderTopRightRadius = node.style.borderTopRightRadius || 0;
// @ts-expect-error this is always a number due to resolve border radius step
const borderBottomLeftRadius = node.style.borderBottomLeftRadius || 0;
// @ts-expect-error this is always a number due to resolve border radius step
const borderBottomRightRadius = node.style.borderBottomRightRadius || 0;
const style = {
borderTopColor,
borderTopWidth,
borderTopStyle,
borderLeftColor,
borderLeftWidth,
borderLeftStyle,
borderRightColor,
borderRightWidth,
borderRightStyle,
borderBottomColor,
borderBottomWidth,
borderBottomStyle};
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);
ctx.save();
ctx.strokeOpacity(opacity);
if (borderTopWidth) {
ctx.save();
clipBorderTop(ctx, node.box, style, rtr, rtl);
fillBorderTop(ctx, node.box, style, rtr, rtl);
ctx.restore();
}
if (borderRightWidth) {
ctx.save();
clipBorderRight(ctx, node.box, style, rtr, rbr);
fillBorderRight(ctx, node.box, style, rtr, rbr);
ctx.restore();
}
if (borderBottomWidth) {
ctx.save();
clipBorderBottom(ctx, node.box, style, rbl, rbr);
fillBorderBottom(ctx, node.box, style, rbl, rbr);
ctx.restore();
}
if (borderLeftWidth) {
ctx.save();
clipBorderLeft(ctx, node.box, style, rbl, rtl);
fillBorderLeft(ctx, node.box, style, rbl, rtl);
ctx.restore();
}
ctx.restore();
};
const drawBackground = (ctx, node) => {
if (!node.box)
return;
const { top, left, width, height } = node.box;
const color = parseColor(node.style.backgroundColor);
const nodeOpacity = isNil(node.style?.opacity) ? 1 : node.style.opacity;
const opacity = Math.min(color.opacity, nodeOpacity);
ctx
.fillOpacity(opacity)
.fillColor(color.value)
.rect(left, top, width, height)
.fill();
};
const renderBackground = (ctx, node) => {
const hasBackground = !!node.box && !!node.style?.backgroundColor;
if (hasBackground) {
ctx.save();
clipNode(ctx, node);
drawBackground(ctx, node);
ctx.restore();
}
};
const isString = (value) => typeof value === 'string';
const isSrcId = (value) => /^#.+/.test(value);
const renderLink = (ctx, node, src) => {
if (!src || !node.box)
return;
const isId = isSrcId(src);
const method = isId ? 'goTo' : 'link';
const value = isId ? src.slice(1) : src;
const { top, left, width, height } = node.box;
ctx[method](left, top, width, height, value);
};
const setLink = (ctx, node) => {
const props = node.props || {};
if ('src' in props && isString(props.src))
return renderLink(ctx, node, props.src);
if ('href' in props && isString(props.href))
return renderLink(ctx, node, props.href);
};
const setDestination = (ctx, node) => {
if (!node.box)
return;
if (!node.props)
return;
if ('id' in node.props) {
ctx.addNamedDestination(node.props.id, 'XYZ', null, node.box.top, null);
}
};
const clean = (options) => {
const opt = { ...options };
// We need to ensure the elements are no present if not true
Object.entries(opt).forEach((pair) => {
if (!pair[1]) {
delete opt[pair[0]];
}
});
return opt;
};
const parseCommonFormOptions = (node) => {
// Common Options
return {
required: node.props?.required || false,
noExport: node.props?.noExport || false,
readOnly: node.props?.readOnly || false,
value: node.props?.value || undefined,
defaultValue: node.props?.defaultValue || undefined,
};
};
const parseTextInputOptions = (node, fieldSet) => {
return clean({
...parseCommonFormOptions(node),
parent: fieldSet || undefined,
align: node.props?.align || 'left',
multiline: node.props?.multiline || undefined,
password: node.props?.password || false,
noSpell: node.props?.noSpell || false,
format: node.props?.format || undefined,
fontSize: node.props?.fontSize || undefined,
MaxLen: node.props?.maxLength || undefined,
});
};
const parseSelectAndListFieldOptions = (node) => {
return clean({
...parseCommonFormOptions(node),
sort: node.props?.sort || false,
edit: node.props?.edit || false,
multiSelect: node.props?.multiSelect || false,
noSpell: node.props?.noSpell || false,
select: node.props?.select || [''],
});
};
const getAppearance = (ctx, codepoint, width, height) => {
const appearance = ctx.ref({
Type: 'XObject',
Subtype: 'Form',
BBox: [0, 0, width, height],
Resources: {
ProcSet: ['PDF', 'Text', 'ImageB', 'ImageC', 'ImageI'],
Font: {
ZaDi: ctx._acroform.fonts.ZaDi,
},
},
});
appearance.initDeflate();
appearance.write(`/Tx BMC\nq\n/ZaDi ${height * 0.8} Tf\nBT\n${width * 0.45} ${height / 4} Td (${codepoint}) Tj\nET\nQ\nEMC`);
appearance.end(null);
return appearance;
};
const parseCheckboxOptions = (ctx, node, fieldSet) => {
const { width, height } = node.box || {};
const onOption = node.props?.onState || 'Yes';
const offOption = node.props?.offState || 'Off';
const xMark = node.props?.xMark || false;
if (!Object.prototype.hasOwnProperty.call(ctx._acroform.fonts, 'ZaDi')) {
const ref = ctx.ref({
Type: 'Font',
Subtype: 'Type1',
BaseFont: 'ZapfDingbats',
});
ctx._acroform.fonts.ZaDi = ref;
ref.end(null);
}
const normalAppearance = {
[onOption]: getAppearance(ctx, xMark ? '8' : '4', width, height),
[offOption]: getAppearance(ctx, xMark ? ' ' : '8', width, height),
};
return clean({
...parseCommonFormOptions(node),
backgroundColor: node.props?.backgroundColor || undefined,
borderColor: node.props?.borderColor || undefined,
parent: fieldSet || undefined,
value: `/${node.props?.checked === true ? onOption : offOption}`,
defaultValue: `/${node.props?.checked === true ? onOption : offOption}`,
AS: node.props?.checked === true ? onOption : offOption,
AP: { N: normalAppearance, D: normalAppearance },
});
};
const renderTextInput = (ctx, node, options) => {
if (!node.box)
return;
const { top, left, width, height } = node.box;
// Element's name
const name = node.props?.name || '';
const fieldSetOptions = options.fieldSets?.at(0);
if (!ctx._root.data.AcroForm) {
ctx.initForm();
}
ctx.formText(name, left, top, width, height, parseTextInputOptions(node, fieldSetOptions));
};
const renderSelect = (ctx, node) => {
if (!node.box)
return;
const { top, left, width, height } = node.box;
// Element's name
const name = node.props?.name || '';
if (!ctx._root.data.AcroForm) {
ctx.initForm();
}
ctx.formCombo(name, left, top, width, height, parseSelectAndListFieldOptions(node));
};
const renderFieldSet = (ctx, node, options) => {
const name = node.props?.name || '';
if (!ctx._root.data.AcroForm) {
ctx.initForm();
}
const formField = ctx.formField(name);
const option = options;
if (!option.fieldSets) {
option.fieldSets = [formField];
}
else {
option.fieldSets.push(formField);
}
};
const cleanUpFieldSet = (_ctx, _node, options) => {
options.fieldSets.pop();
};
const renderList = (ctx, node) => {
if (!node.box)
return;
const { top, left, width, height } = node.box || {};
// Element's name
const name = ('name' in node.props ? node.props.name || '' : '');
if (!ctx._root.data.AcroForm) {
ctx.initForm();
}
ctx.formList(name, left, top, width, height, parseSelectAndListFieldOptions(node));
};
const renderCheckbox = (ctx, node, options) => {
if (!node.box)
return;
const { top, left, width, height } = node.box;
// Element's name
const name = node.props?.name || '';
const fieldSetOptions = options.fieldSets?.at(0);
if (!ctx._root.data.AcroForm) {
ctx.initForm();
}
ctx.formCheckbox(name, left, top, width, height, parseCheckboxOptions(ctx, node, fieldSetOptions));
};
const isRecursiveNode = (node) => node.type !== P.Text && node.type !== P.Svg;
const renderChildren = (ctx, node, options) => {
ctx.save();
if (node.box) {
ctx.translate(node.box.left, node.box.top);
}
const children = node.children || [];
const renderChild = (child) => renderNode(ctx, child, options);
children.forEach(renderChild);
ctx.restore();
};
const renderFns = {
[P.Text]: renderText,
[P.Note]: renderNote,
[P.Image]: renderImage,
[P.FieldSet]: renderFieldSet,
[P.TextInput]: renderTextInput,
[P.Select]: renderSelect,
[P.Checkbox]: renderCheckbox,
[P.List]: renderList,
[P.Canvas]: renderCanvas,
[P.Svg]: renderSvg,
[P.Link]: setLink,
};
const cleanUpFns = {
[P.FieldSet]: cleanUpFieldSet,
};
const renderNode = (ctx, node, options) => {
const overflowHidden = node.style?.overflow === 'hidden';
const shouldRenderChildren = isRecursiveNode(node);
if (node.type === P.Page)
renderPage(ctx, node);
ctx.save();
if (overflowHidden)
clipNode(ctx, node);
applyTransformations(ctx, node);
renderBackground(ctx, node);
renderBorders(ctx, node);
const renderFn = renderFns[node.type];
if (renderFn)
renderFn(ctx, node, options);
if (shouldRenderChildren)
renderChildren(ctx, node, options);
const cleanUpFn = cleanUpFns[node.type];
if (cleanUpFn)
cleanUpFn(ctx, node, options);
setDestination(ctx, node);
renderDebug(ctx, node);
ctx.restore();
};
const addNodeBookmark = (ctx, node, pageNumber, registry) => {
if (!node.box)
return;
if (!node.props)
return;
if ('bookmark' in node.props && node.props.bookmark) {
const bookmark = node.props.bookmark;
const { title, parent, expanded, zoom, fit } = bookmark;
const outline = registry[parent] || ctx.outline;
const top = bookmark.top || node.box.top;
const left = bookmark.left || node.box.left;
const instance = outline.addItem(title, {
pageNumber,
expanded,
top,
left,
zoom,
fit,
});
registry[bookmark.ref] = instance;
}
if (!node.children)
return;
node.children.forEach((child) => addNodeBookmark(ctx, child, pageNumber, registry));
};
const addBookmarks = (ctx, root) => {
const registry = {};
const pages = root.children || [];
pages.forEach((page, i) => {
addNodeBookmark(ctx, page, i, registry);
});
};
const render = (ctx, doc) => {
const pages = doc.children || [];
const options = { imageCache: new Map(), fieldSets: [] };
pages.forEach((page) => renderNode(ctx, page, options));
addBookmarks(ctx, doc);
ctx.end();
return ctx;
};
export { render as default };