mirror of
https://github.com/foliojs/pdfkit.git
synced 2025-12-08 20:15:54 +00:00
320 lines
8.2 KiB
JavaScript
320 lines
8.2 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
|
|
return 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) {
|
|
let phase;
|
|
if (options == null) { options = {}; }
|
|
if (length == null) { return this; }
|
|
if (Array.isArray(length)) {
|
|
length = length.map((v) => number(v)).join(' ');
|
|
phase = options.phase || 0;
|
|
return this.addContent(`[${length}] ${number(phase)} d`);
|
|
} else {
|
|
const space = options.space != null ? options.space : length;
|
|
phase = options.phase || 0;
|
|
return this.addContent(`[${number(length)} ${number(space)}] ${number(phase)} 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, end = numSegs, asc = 0 <= end; asc ? segIdx < end : segIdx > end; asc ? segIdx++ : 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
|
|
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;
|
|
if (options == null) { options = {}; }
|
|
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 (options == null) { options = {}; }
|
|
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);
|
|
}
|
|
};
|