mirror of
https://github.com/foliojs/pdfkit.git
synced 2025-12-08 20:15:54 +00:00
358 lines
8.3 KiB
JavaScript
358 lines
8.3 KiB
JavaScript
import SVGPath from '../path';
|
|
import PDFObject from '../object';
|
|
|
|
const { number } = PDFObject;
|
|
|
|
// 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);
|
|
export default {
|
|
initVector() {
|
|
this._ctm = [1, 0, 0, 1, 0, 0]; // current transformation matrix
|
|
this._ctmStack = [];
|
|
},
|
|
|
|
save() {
|
|
this._ctmStack.push(this._ctm.slice());
|
|
// TODO: save/restore colorspace and styles so not setting it unnessesarily all the time?
|
|
return this.addContent('q');
|
|
},
|
|
|
|
restore() {
|
|
this._ctm = this._ctmStack.pop() || [1, 0, 0, 1, 0, 0];
|
|
return this.addContent('Q');
|
|
},
|
|
|
|
closePath() {
|
|
return this.addContent('h');
|
|
},
|
|
|
|
lineWidth(w) {
|
|
return this.addContent(`${number(w)} w`);
|
|
},
|
|
|
|
_CAP_STYLES: {
|
|
BUTT: 0,
|
|
ROUND: 1,
|
|
SQUARE: 2,
|
|
},
|
|
|
|
lineCap(c) {
|
|
if (typeof c === 'string') {
|
|
c = this._CAP_STYLES[c.toUpperCase()];
|
|
}
|
|
return this.addContent(`${c} J`);
|
|
},
|
|
|
|
_JOIN_STYLES: {
|
|
MITER: 0,
|
|
ROUND: 1,
|
|
BEVEL: 2,
|
|
},
|
|
|
|
lineJoin(j) {
|
|
if (typeof j === 'string') {
|
|
j = this._JOIN_STYLES[j.toUpperCase()];
|
|
}
|
|
return this.addContent(`${j} j`);
|
|
},
|
|
|
|
miterLimit(m) {
|
|
return this.addContent(`${number(m)} M`);
|
|
},
|
|
|
|
dash(length, options = {}) {
|
|
const originalLength = length;
|
|
if (!Array.isArray(length)) {
|
|
length = [length, options.space || length];
|
|
}
|
|
|
|
const valid = length.every((x) => Number.isFinite(x) && x > 0);
|
|
if (!valid) {
|
|
throw new Error(
|
|
`dash(${JSON.stringify(originalLength)}, ${JSON.stringify(
|
|
options,
|
|
)}) invalid, lengths must be numeric and greater than zero`,
|
|
);
|
|
}
|
|
|
|
length = length.map(number).join(' ');
|
|
return this.addContent(`[${length}] ${number(options.phase || 0)} d`);
|
|
},
|
|
|
|
undash() {
|
|
return this.addContent('[] 0 d');
|
|
},
|
|
|
|
moveTo(x, y) {
|
|
return this.addContent(`${number(x)} ${number(y)} m`);
|
|
},
|
|
|
|
lineTo(x, y) {
|
|
return this.addContent(`${number(x)} ${number(y)} l`);
|
|
},
|
|
|
|
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
|
|
return this.addContent(
|
|
`${number(cp1x)} ${number(cp1y)} ${number(cp2x)} ${number(cp2y)} ${number(
|
|
x,
|
|
)} ${number(y)} c`,
|
|
);
|
|
},
|
|
|
|
quadraticCurveTo(cpx, cpy, x, y) {
|
|
return this.addContent(
|
|
`${number(cpx)} ${number(cpy)} ${number(x)} ${number(y)} v`,
|
|
);
|
|
},
|
|
|
|
rect(x, y, w, h) {
|
|
return this.addContent(
|
|
`${number(x)} ${number(y)} ${number(w)} ${number(h)} re`,
|
|
);
|
|
},
|
|
|
|
roundedRect(x, y, w, h, r) {
|
|
if (r == null) {
|
|
r = 0;
|
|
}
|
|
r = Math.min(r, 0.5 * w, 0.5 * h);
|
|
|
|
// amount to inset control points from corners (see `ellipse`)
|
|
const c = r * (1.0 - KAPPA);
|
|
|
|
this.moveTo(x + r, y);
|
|
this.lineTo(x + w - r, y);
|
|
this.bezierCurveTo(x + w - c, y, x + w, y + c, x + w, y + r);
|
|
this.lineTo(x + w, y + h - r);
|
|
this.bezierCurveTo(x + w, y + h - c, x + w - c, y + h, x + w - r, y + h);
|
|
this.lineTo(x + r, y + h);
|
|
this.bezierCurveTo(x + c, y + h, x, y + h - c, x, y + h - r);
|
|
this.lineTo(x, y + r);
|
|
this.bezierCurveTo(x, y + c, x + c, y, x + r, y);
|
|
return this.closePath();
|
|
},
|
|
|
|
ellipse(x, y, r1, r2) {
|
|
// based on http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas/2173084#2173084
|
|
if (r2 == null) {
|
|
r2 = r1;
|
|
}
|
|
x -= r1;
|
|
y -= r2;
|
|
const ox = r1 * KAPPA;
|
|
const oy = r2 * KAPPA;
|
|
const xe = x + r1 * 2;
|
|
const ye = y + r2 * 2;
|
|
const xm = x + r1;
|
|
const ym = y + r2;
|
|
|
|
this.moveTo(x, ym);
|
|
this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
|
|
this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
|
|
this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
|
|
this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
|
|
return this.closePath();
|
|
},
|
|
|
|
circle(x, y, radius) {
|
|
return this.ellipse(x, y, radius);
|
|
},
|
|
|
|
arc(x, y, radius, startAngle, endAngle, anticlockwise) {
|
|
if (anticlockwise == null) {
|
|
anticlockwise = false;
|
|
}
|
|
const TWO_PI = 2.0 * Math.PI;
|
|
const HALF_PI = 0.5 * Math.PI;
|
|
|
|
let deltaAng = endAngle - startAngle;
|
|
|
|
if (Math.abs(deltaAng) > TWO_PI) {
|
|
// draw only full circle if more than that is specified
|
|
deltaAng = TWO_PI;
|
|
} else if (deltaAng !== 0 && anticlockwise !== deltaAng < 0) {
|
|
// necessary to flip direction of rendering
|
|
const dir = anticlockwise ? -1 : 1;
|
|
deltaAng = dir * TWO_PI + deltaAng;
|
|
}
|
|
|
|
const numSegs = Math.ceil(Math.abs(deltaAng) / HALF_PI);
|
|
const segAng = deltaAng / numSegs;
|
|
const handleLen = (segAng / HALF_PI) * KAPPA * radius;
|
|
let curAng = startAngle;
|
|
|
|
// component distances between anchor point and control point
|
|
let deltaCx = -Math.sin(curAng) * handleLen;
|
|
let deltaCy = Math.cos(curAng) * handleLen;
|
|
|
|
// anchor point
|
|
let ax = x + Math.cos(curAng) * radius;
|
|
let ay = y + Math.sin(curAng) * radius;
|
|
|
|
// calculate and render segments
|
|
this.moveTo(ax, ay);
|
|
|
|
for (let segIdx = 0; segIdx < numSegs; segIdx++) {
|
|
// starting control point
|
|
const cp1x = ax + deltaCx;
|
|
const cp1y = ay + deltaCy;
|
|
|
|
// step angle
|
|
curAng += segAng;
|
|
|
|
// next anchor point
|
|
ax = x + Math.cos(curAng) * radius;
|
|
ay = y + Math.sin(curAng) * radius;
|
|
|
|
// next control point delta
|
|
deltaCx = -Math.sin(curAng) * handleLen;
|
|
deltaCy = Math.cos(curAng) * handleLen;
|
|
|
|
// ending control point
|
|
const cp2x = ax - deltaCx;
|
|
const cp2y = ay - deltaCy;
|
|
|
|
// render segment
|
|
this.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, ax, ay);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
polygon(...points) {
|
|
this.moveTo(...(points.shift() || []));
|
|
for (let point of points) {
|
|
this.lineTo(...(point || []));
|
|
}
|
|
return this.closePath();
|
|
},
|
|
|
|
path(path) {
|
|
SVGPath.apply(this, path);
|
|
return this;
|
|
},
|
|
|
|
_windingRule(rule) {
|
|
if (/even-?odd/.test(rule)) {
|
|
return '*';
|
|
}
|
|
|
|
return '';
|
|
},
|
|
|
|
fill(color, rule) {
|
|
if (/(even-?odd)|(non-?zero)/.test(color)) {
|
|
rule = color;
|
|
color = null;
|
|
}
|
|
|
|
if (color) {
|
|
this.fillColor(color);
|
|
}
|
|
return this.addContent(`f${this._windingRule(rule)}`);
|
|
},
|
|
|
|
stroke(color) {
|
|
if (color) {
|
|
this.strokeColor(color);
|
|
}
|
|
return this.addContent('S');
|
|
},
|
|
|
|
fillAndStroke(fillColor, strokeColor, rule) {
|
|
if (strokeColor == null) {
|
|
strokeColor = fillColor;
|
|
}
|
|
const isFillRule = /(even-?odd)|(non-?zero)/;
|
|
if (isFillRule.test(fillColor)) {
|
|
rule = fillColor;
|
|
fillColor = null;
|
|
}
|
|
|
|
if (isFillRule.test(strokeColor)) {
|
|
rule = strokeColor;
|
|
strokeColor = fillColor;
|
|
}
|
|
|
|
if (fillColor) {
|
|
this.fillColor(fillColor);
|
|
this.strokeColor(strokeColor);
|
|
}
|
|
|
|
return this.addContent(`B${this._windingRule(rule)}`);
|
|
},
|
|
|
|
clip(rule) {
|
|
return this.addContent(`W${this._windingRule(rule)} n`);
|
|
},
|
|
|
|
transform(m11, m12, m21, m22, dx, dy) {
|
|
// keep track of the current transformation matrix
|
|
if (
|
|
m11 === 1 &&
|
|
m12 === 0 &&
|
|
m21 === 0 &&
|
|
m22 === 1 &&
|
|
dx === 0 &&
|
|
dy === 0
|
|
) {
|
|
// Ignore identity transforms
|
|
return this;
|
|
}
|
|
const m = this._ctm;
|
|
const [m0, m1, m2, m3, m4, m5] = m;
|
|
m[0] = m0 * m11 + m2 * m12;
|
|
m[1] = m1 * m11 + m3 * m12;
|
|
m[2] = m0 * m21 + m2 * m22;
|
|
m[3] = m1 * m21 + m3 * m22;
|
|
m[4] = m0 * dx + m2 * dy + m4;
|
|
m[5] = m1 * dx + m3 * dy + m5;
|
|
|
|
const values = [m11, m12, m21, m22, dx, dy].map((v) => number(v)).join(' ');
|
|
return this.addContent(`${values} cm`);
|
|
},
|
|
|
|
translate(x, y) {
|
|
return this.transform(1, 0, 0, 1, x, y);
|
|
},
|
|
|
|
rotate(angle, options = {}) {
|
|
let y;
|
|
const rad = (angle * Math.PI) / 180;
|
|
const cos = Math.cos(rad);
|
|
const sin = Math.sin(rad);
|
|
let x = (y = 0);
|
|
|
|
if (options.origin != null) {
|
|
[x, y] = options.origin;
|
|
const x1 = x * cos - y * sin;
|
|
const y1 = x * sin + y * cos;
|
|
x -= x1;
|
|
y -= y1;
|
|
}
|
|
|
|
return this.transform(cos, sin, -sin, cos, x, y);
|
|
},
|
|
|
|
scale(xFactor, yFactor, options = {}) {
|
|
let y;
|
|
if (yFactor == null) {
|
|
yFactor = xFactor;
|
|
}
|
|
if (typeof yFactor === 'object') {
|
|
options = yFactor;
|
|
yFactor = xFactor;
|
|
}
|
|
|
|
let x = (y = 0);
|
|
if (options.origin != null) {
|
|
[x, y] = options.origin;
|
|
x -= xFactor * x;
|
|
y -= yFactor * y;
|
|
}
|
|
|
|
return this.transform(xFactor, 0, 0, yFactor, x, y);
|
|
},
|
|
};
|