@pdfme/pdf-lib
Version:
Create and modify PDF files with JavaScript
520 lines • 19.9 kB
JavaScript
import { setFillingColor, setStrokingColor } from './colors';
import { beginText, closePath, drawObject, endText, fill, fillAndStroke, lineTo, moveTo, nextLine, popGraphicsState, pushGraphicsState, rotateAndSkewTextRadiansAndTranslate, rotateRadians, scale, setFontAndSize, setLineHeight, setLineWidth, showText, skewRadians, stroke, translate, setLineCap, rotateDegrees, setGraphicsState, setDashPattern, beginMarkedContent, endMarkedContent, clip, endPath, appendBezierCurve, FillRule, fillEvenOdd, concatTransformationMatrix, } from './operators';
import { degrees, toRadians } from './rotations';
import { svgPathToOperators } from './svgPath';
import { asNumber } from './objects';
const clipSpace = ({ topLeft, topRight, bottomRight, bottomLeft }) => [
moveTo(topLeft.x, topLeft.y),
lineTo(topRight.x, topRight.y),
lineTo(bottomRight.x, bottomRight.y),
lineTo(bottomLeft.x, bottomLeft.y),
closePath(),
clip(),
endPath(),
];
const clipSpaces = (spaces) => spaces.flatMap(clipSpace);
export const drawText = (line, options) => [
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
beginText(),
setFillingColor(options.color),
setFontAndSize(options.font, options.size),
rotateAndSkewTextRadiansAndTranslate(toRadians(options.rotate), toRadians(options.xSkew), toRadians(options.ySkew), options.x, options.y),
showText(line),
endText(),
popGraphicsState(),
].filter(Boolean);
export const drawLinesOfText = (lines, options) => {
const operators = [
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
...(options.clipSpaces ? clipSpaces(options.clipSpaces) : []),
options.matrix && concatTransformationMatrix(...options.matrix),
beginText(),
setFillingColor(options.color),
setFontAndSize(options.font, options.size),
setLineHeight(options.lineHeight),
rotateAndSkewTextRadiansAndTranslate(toRadians(options.rotate), toRadians(options.xSkew), toRadians(options.ySkew), options.x, options.y),
].filter(Boolean);
for (let idx = 0, len = lines.length; idx < len; idx++) {
operators.push(showText(lines[idx]), nextLine());
}
operators.push(endText(), popGraphicsState());
return operators;
};
export const drawImage = (name, options) => [
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
...(options.clipSpaces ? clipSpaces(options.clipSpaces) : []),
options.matrix && concatTransformationMatrix(...options.matrix),
translate(options.x, options.y),
rotateRadians(toRadians(options.rotate)),
scale(options.width, options.height),
skewRadians(toRadians(options.xSkew), toRadians(options.ySkew)),
drawObject(name),
popGraphicsState(),
].filter(Boolean);
export const drawPage = (name, options) => [
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
translate(options.x, options.y),
rotateRadians(toRadians(options.rotate)),
scale(options.xScale, options.yScale),
skewRadians(toRadians(options.xSkew), toRadians(options.ySkew)),
drawObject(name),
popGraphicsState(),
].filter(Boolean);
export const drawLine = (options) => [
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
...(options.clipSpaces ? clipSpaces(options.clipSpaces) : []),
options.matrix && concatTransformationMatrix(...options.matrix),
options.color && setStrokingColor(options.color),
setLineWidth(options.thickness),
setDashPattern(options.dashArray ?? [], options.dashPhase ?? 0),
moveTo(options.start.x, options.start.y),
options.lineCap && setLineCap(options.lineCap),
moveTo(options.start.x, options.start.y),
lineTo(options.end.x, options.end.y),
stroke(),
popGraphicsState(),
].filter(Boolean);
export const drawRectangle = (options) => {
let ops = [];
if (!options.radius || asNumber(options.radius) <= 0) {
ops = [
moveTo(0, 0),
lineTo(0, options.height),
lineTo(options.width, options.height),
lineTo(options.width, 0),
closePath(),
];
}
else {
let radius = asNumber(options.radius);
const width = asNumber(options.width);
const height = asNumber(options.height);
if (radius > width / 2.0 || radius > height / 2.0) {
radius = Math.min(width / 2.0, height / 2.0);
}
const offset = KAPPA * radius;
ops = [
moveTo(0, radius),
appendBezierCurve(0, radius - offset, radius - offset, 0, radius, 0),
lineTo(width - radius, 0),
appendBezierCurve(width - radius + offset, 0, width, radius - offset, width, radius),
lineTo(width, height - radius),
appendBezierCurve(width, height - radius + offset, width - radius + offset, height, width - radius, height),
lineTo(radius, height),
appendBezierCurve(radius - offset, height, 0, height - radius + offset, 0, height - radius),
closePath(),
];
}
return [
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
options.color && setFillingColor(options.color),
options.borderColor && setStrokingColor(options.borderColor),
setLineWidth(options.borderWidth),
options.borderLineCap && setLineCap(options.borderLineCap),
setDashPattern(options.borderDashArray ?? [], options.borderDashPhase ?? 0),
...(options.clipSpaces ? clipSpaces(options.clipSpaces) : []),
options.matrix && concatTransformationMatrix(...options.matrix),
translate(options.x, options.y),
rotateRadians(toRadians(options.rotate)),
skewRadians(toRadians(options.xSkew), toRadians(options.ySkew)),
...ops,
// prettier-ignore
options.color && options.borderWidth ? fillAndStroke()
: options.color ? fill()
: options.borderColor ? stroke()
: closePath(),
popGraphicsState(),
].filter(Boolean);
};
const KAPPA = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0);
/** @deprecated */
export const drawEllipsePath = (config) => {
let x = asNumber(config.x);
let y = asNumber(config.y);
const xScale = asNumber(config.xScale);
const yScale = asNumber(config.yScale);
x -= xScale;
y -= yScale;
const ox = xScale * KAPPA;
const oy = yScale * KAPPA;
const xe = x + xScale * 2;
const ye = y + yScale * 2;
const xm = x + xScale;
const ym = y + yScale;
return [
pushGraphicsState(),
moveTo(x, ym),
appendBezierCurve(x, ym - oy, xm - ox, y, xm, y),
appendBezierCurve(xm + ox, y, xe, ym - oy, xe, ym),
appendBezierCurve(xe, ym + oy, xm + ox, ye, xm, ye),
appendBezierCurve(xm - ox, ye, x, ym + oy, x, ym),
popGraphicsState(),
];
};
const drawEllipseCurves = (config) => {
const centerX = asNumber(config.x);
const centerY = asNumber(config.y);
const xScale = asNumber(config.xScale);
const yScale = asNumber(config.yScale);
const x = -xScale;
const y = -yScale;
const ox = xScale * KAPPA;
const oy = yScale * KAPPA;
const xe = x + xScale * 2;
const ye = y + yScale * 2;
const xm = x + xScale;
const ym = y + yScale;
return [
translate(centerX, centerY),
rotateRadians(toRadians(config.rotate)),
moveTo(x, ym),
appendBezierCurve(x, ym - oy, xm - ox, y, xm, y),
appendBezierCurve(xm + ox, y, xe, ym - oy, xe, ym),
appendBezierCurve(xe, ym + oy, xm + ox, ye, xm, ye),
appendBezierCurve(xm - ox, ye, x, ym + oy, x, ym),
];
};
export const drawEllipse = (options) => [
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
options.color && setFillingColor(options.color),
options.borderColor && setStrokingColor(options.borderColor),
...(options.clipSpaces ? clipSpaces(options.clipSpaces) : []),
options.matrix && concatTransformationMatrix(...options.matrix),
setLineWidth(options.borderWidth),
options.borderLineCap && setLineCap(options.borderLineCap),
setDashPattern(options.borderDashArray ?? [], options.borderDashPhase ?? 0),
// The `drawEllipsePath` branch is only here for backwards compatibility.
// See https://github.com/Hopding/pdf-lib/pull/511#issuecomment-667685655.
...(options.rotate === undefined
? drawEllipsePath({
x: options.x,
y: options.y,
xScale: options.xScale,
yScale: options.yScale,
})
: drawEllipseCurves({
x: options.x,
y: options.y,
xScale: options.xScale,
yScale: options.yScale,
rotate: options.rotate ?? degrees(0),
})),
// prettier-ignore
options.color && options.borderWidth ? fillAndStroke()
: options.color ? fill()
: options.borderColor ? stroke()
: closePath(),
popGraphicsState(),
].filter(Boolean);
export const drawSvgPath = (path, options) => [
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
...(options.clipSpaces ? clipSpaces(options.clipSpaces) : []),
options.matrix && concatTransformationMatrix(...options.matrix),
translate(options.x, options.y),
rotateRadians(toRadians(options.rotate ?? degrees(0))),
options.scale && scale(options.scale, options.scale),
options.color && setFillingColor(options.color),
options.borderColor && setStrokingColor(options.borderColor),
options.borderWidth && setLineWidth(options.borderWidth),
options.borderLineCap && setLineCap(options.borderLineCap),
setDashPattern(options.borderDashArray ?? [], options.borderDashPhase ?? 0),
...svgPathToOperators(path),
// prettier-ignore
options.color && options.borderWidth ? fillAndStroke()
: options.color ? options.fillRule === FillRule.EvenOdd ? fillEvenOdd() : fill()
: options.borderColor ? stroke()
: closePath(),
popGraphicsState(),
].filter(Boolean);
export const drawCheckMark = (options) => {
const size = asNumber(options.size);
/*********************** Define Check Mark Points ***************************/
// A check mark is defined by three points in some coordinate space. Here, we
// define these points in a unit coordinate system, where the range of the x
// and y axis are both [-1, 1].
//
// Note that we do not hard code `p1y` in case we wish to change the
// size/shape of the check mark in the future. We want the check mark to
// always form a right angle. This means that the dot product between (p1-p2)
// and (p3-p2) should be zero:
//
// (p1x-p2x) * (p3x-p2x) + (p1y-p2y) * (p3y-p2y) = 0
//
// We can now rejigger this equation to solve for `p1y`:
//
// (p1y-p2y) * (p3y-p2y) = -((p1x-p2x) * (p3x-p2x))
// (p1y-p2y) = -((p1x-p2x) * (p3x-p2x)) / (p3y-p2y)
// p1y = -((p1x-p2x) * (p3x-p2x)) / (p3y-p2y) + p2y
//
// Thanks to my friend Joel Walker (https://github.com/JWalker1995) for
// devising the above equation and unit coordinate system approach!
// (x, y) coords of the check mark's bottommost point
const p2x = -1 + 0.75;
const p2y = -1 + 0.51;
// (x, y) coords of the check mark's topmost point
const p3y = 1 - 0.525;
const p3x = 1 - 0.31;
// (x, y) coords of the check mark's center (vertically) point
const p1x = -1 + 0.325;
const p1y = -((p1x - p2x) * (p3x - p2x)) / (p3y - p2y) + p2y;
/****************************************************************************/
return [
pushGraphicsState(),
options.color && setStrokingColor(options.color),
setLineWidth(options.thickness),
translate(options.x, options.y),
moveTo(p1x * size, p1y * size),
lineTo(p2x * size, p2y * size),
lineTo(p3x * size, p3y * size),
stroke(),
popGraphicsState(),
].filter(Boolean);
};
// prettier-ignore
export const rotateInPlace = (options) => options.rotation === 0 ? [
translate(0, 0),
rotateDegrees(0)
]
: options.rotation === 90 ? [
translate(options.width, 0),
rotateDegrees(90)
]
: options.rotation === 180 ? [
translate(options.width, options.height),
rotateDegrees(180)
]
: options.rotation === 270 ? [
translate(0, options.height),
rotateDegrees(270)
]
: []; // Invalid rotation - noop
export const drawCheckBox = (options) => {
const outline = drawRectangle({
x: options.x,
y: options.y,
width: options.width,
height: options.height,
borderWidth: options.borderWidth,
color: options.color,
borderColor: options.borderColor,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
if (!options.filled)
return outline;
const width = asNumber(options.width);
const height = asNumber(options.height);
const checkMarkSize = Math.min(width, height) / 2;
const checkMark = drawCheckMark({
x: width / 2,
y: height / 2,
size: checkMarkSize,
thickness: options.thickness,
color: options.markColor,
});
return [pushGraphicsState(), ...outline, ...checkMark, popGraphicsState()];
};
export const drawRadioButton = (options) => {
const width = asNumber(options.width);
const height = asNumber(options.height);
const outlineScale = Math.min(width, height) / 2;
const outline = drawEllipse({
x: options.x,
y: options.y,
xScale: outlineScale,
yScale: outlineScale,
color: options.color,
borderColor: options.borderColor,
borderWidth: options.borderWidth,
});
if (!options.filled)
return outline;
const dot = drawEllipse({
x: options.x,
y: options.y,
xScale: outlineScale * 0.45,
yScale: outlineScale * 0.45,
color: options.dotColor,
borderColor: undefined,
borderWidth: 0,
});
return [pushGraphicsState(), ...outline, ...dot, popGraphicsState()];
};
export const drawButton = (options) => {
const x = asNumber(options.x);
const y = asNumber(options.y);
const width = asNumber(options.width);
const height = asNumber(options.height);
const background = drawRectangle({
x,
y,
width,
height,
borderWidth: options.borderWidth,
color: options.color,
borderColor: options.borderColor,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
const lines = drawTextLines(options.textLines, {
color: options.textColor,
font: options.font,
size: options.fontSize,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
return [pushGraphicsState(), ...background, ...lines, popGraphicsState()];
};
export const drawTextLines = (lines, options) => {
const operators = [
beginText(),
setFillingColor(options.color),
setFontAndSize(options.font, options.size),
];
for (let idx = 0, len = lines.length; idx < len; idx++) {
const { encoded, x, y } = lines[idx];
operators.push(rotateAndSkewTextRadiansAndTranslate(toRadians(options.rotate), toRadians(options.xSkew), toRadians(options.ySkew), x, y), showText(encoded));
}
operators.push(endText());
return operators;
};
export const drawTextField = (options) => {
const x = asNumber(options.x);
const y = asNumber(options.y);
const width = asNumber(options.width);
const height = asNumber(options.height);
const borderWidth = asNumber(options.borderWidth);
const padding = asNumber(options.padding);
const clipX = x + borderWidth / 2 + padding;
const clipY = y + borderWidth / 2 + padding;
const clipWidth = width - (borderWidth / 2 + padding) * 2;
const clipHeight = height - (borderWidth / 2 + padding) * 2;
const clippingArea = [
moveTo(clipX, clipY),
lineTo(clipX, clipY + clipHeight),
lineTo(clipX + clipWidth, clipY + clipHeight),
lineTo(clipX + clipWidth, clipY),
closePath(),
clip(),
endPath(),
];
const background = drawRectangle({
x,
y,
width,
height,
borderWidth: options.borderWidth,
color: options.color,
borderColor: options.borderColor,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
const lines = drawTextLines(options.textLines, {
color: options.textColor,
font: options.font,
size: options.fontSize,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
const markedContent = [
beginMarkedContent('Tx'),
pushGraphicsState(),
...lines,
popGraphicsState(),
endMarkedContent(),
];
return [
pushGraphicsState(),
...background,
...clippingArea,
...markedContent,
popGraphicsState(),
];
};
export const drawOptionList = (options) => {
const x = asNumber(options.x);
const y = asNumber(options.y);
const width = asNumber(options.width);
const height = asNumber(options.height);
const lineHeight = asNumber(options.lineHeight);
const borderWidth = asNumber(options.borderWidth);
const padding = asNumber(options.padding);
const clipX = x + borderWidth / 2 + padding;
const clipY = y + borderWidth / 2 + padding;
const clipWidth = width - (borderWidth / 2 + padding) * 2;
const clipHeight = height - (borderWidth / 2 + padding) * 2;
const clippingArea = [
moveTo(clipX, clipY),
lineTo(clipX, clipY + clipHeight),
lineTo(clipX + clipWidth, clipY + clipHeight),
lineTo(clipX + clipWidth, clipY),
closePath(),
clip(),
endPath(),
];
const background = drawRectangle({
x,
y,
width,
height,
borderWidth: options.borderWidth,
color: options.color,
borderColor: options.borderColor,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
const highlights = [];
for (let idx = 0, len = options.selectedLines.length; idx < len; idx++) {
const line = options.textLines[options.selectedLines[idx]];
highlights.push(...drawRectangle({
x: line.x - padding,
y: line.y - (lineHeight - line.height) / 2,
width: width - borderWidth,
height: line.height + (lineHeight - line.height) / 2,
borderWidth: 0,
color: options.selectedColor,
borderColor: undefined,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
}));
}
const lines = drawTextLines(options.textLines, {
color: options.textColor,
font: options.font,
size: options.fontSize,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
const markedContent = [
beginMarkedContent('Tx'),
pushGraphicsState(),
...lines,
popGraphicsState(),
endMarkedContent(),
];
return [
pushGraphicsState(),
...background,
...highlights,
...clippingArea,
...markedContent,
popGraphicsState(),
];
};
//# sourceMappingURL=operations.js.map