pdfkit/lib/gradient.js
2018-11-29 08:14:45 -08:00

250 lines
6.1 KiB
JavaScript

import PDFObject from './object';
const {
number
} = PDFObject;
class PDFGradient {
constructor(doc) {
this.doc = doc;
this.stops = [];
this.embedded = false;
this.transform = [1, 0, 0, 1, 0, 0];
}
stop(pos, color, opacity) {
if (opacity == null) { opacity = 1; }
color = this.doc._normalizeColor(color);
if (this.stops.length === 0) {
if (color.length === 3) {
this._colorSpace = 'DeviceRGB';
} else if (color.length === 4) {
this._colorSpace = 'DeviceCMYK';
} else if (color.length === 1) {
this._colorSpace = 'DeviceGray';
} else {
throw new Error('Unknown color space');
}
} else if (((this._colorSpace === 'DeviceRGB') && (color.length !== 3)) ||
((this._colorSpace === 'DeviceCMYK') && (color.length !== 4)) ||
((this._colorSpace === 'DeviceGray') && (color.length !== 1))) {
throw new Error('All gradient stops must use the same color space');
}
opacity = Math.max(0, Math.min(1, opacity));
this.stops.push([pos, color, opacity]);
return this;
}
setTransform(m11, m12, m21, m22, dx, dy) {
this.transform = [m11, m12, m21, m22, dx, dy];
return this;
}
embed(m) {
let asc, i;
let end, fn;
if (this.stops.length === 0) { return; }
this.embedded = true;
this.matrix = m;
// if the last stop comes before 100%, add a copy at 100%
const last = this.stops[this.stops.length - 1];
if (last[0] < 1) {
this.stops.push([1, last[1], last[2]]);
}
const bounds = [];
const encode = [];
const stops = [];
for (i = 0, end = this.stops.length - 1, asc = 0 <= end; asc ? i < end : i > end; asc ? i++ : i--) {
encode.push(0, 1);
if ((i + 2) !== this.stops.length) {
bounds.push(this.stops[i + 1][0]);
}
fn = this.doc.ref({
FunctionType: 2,
Domain: [0, 1],
C0: this.stops[i + 0][1],
C1: this.stops[i + 1][1],
N: 1
});
stops.push(fn);
fn.end();
}
// if there are only two stops, we don't need a stitching function
if (stops.length === 1) {
fn = stops[0];
} else {
fn = this.doc.ref({
FunctionType: 3, // stitching function
Domain: [0, 1],
Functions: stops,
Bounds: bounds,
Encode: encode
});
fn.end();
}
this.id = `Sh${++this.doc._gradCount}`;
const shader = this.shader(fn);
shader.end();
const pattern = this.doc.ref({
Type: 'Pattern',
PatternType: 2,
Shading: shader,
Matrix: (this.matrix.map((v) => number(v)))
});
pattern.end();
if (this.stops.some(stop => stop[2] < 1)) {
let grad = this.opacityGradient();
grad._colorSpace = 'DeviceGray';
for (let stop of this.stops) {
grad.stop(stop[0], [stop[2]]);
}
grad = grad.embed(this.matrix);
const pageBBox = [0, 0, this.doc.page.width, this.doc.page.height];
const form = this.doc.ref({
Type: 'XObject',
Subtype: 'Form',
FormType: 1,
BBox: pageBBox,
Group: {
Type: 'Group',
S: 'Transparency',
CS: 'DeviceGray'
},
Resources: {
ProcSet: ['PDF', 'Text', 'ImageB', 'ImageC', 'ImageI'],
Pattern: {
Sh1: grad
}
}
});
form.write("/Pattern cs /Sh1 scn");
form.end(`${pageBBox.join(" ")} re f`);
const gstate = this.doc.ref({
Type: 'ExtGState',
SMask: {
Type: 'Mask',
S: 'Luminosity',
G: form
}
});
gstate.end();
const opacityPattern = this.doc.ref({
Type: 'Pattern',
PatternType: 1,
PaintType: 1,
TilingType: 2,
BBox: pageBBox,
XStep: pageBBox[2],
YStep: pageBBox[3],
Resources: {
ProcSet: ['PDF', 'Text', 'ImageB', 'ImageC', 'ImageI'],
Pattern: {
Sh1: pattern
},
ExtGState: {
Gs1: gstate
}
}
});
opacityPattern.write("/Gs1 gs /Pattern cs /Sh1 scn");
opacityPattern.end(`${pageBBox.join(" ")} re f`);
this.doc.page.patterns[this.id] = opacityPattern;
} else {
this.doc.page.patterns[this.id] = pattern;
}
return pattern;
}
apply(op) {
// apply gradient transform to existing document ctm
const [m0, m1, m2, m3, m4, m5] = this.doc._ctm;
const [m11, m12, m21, m22, dx, dy] = this.transform;
const m = [(m0 * m11) + (m2 * m12),
(m1 * m11) + (m3 * m12),
(m0 * m21) + (m2 * m22),
(m1 * m21) + (m3 * m22),
(m0 * dx) + (m2 * dy) + m4,
(m1 * dx) + (m3 * dy) + m5];
if (!this.embedded || (m.join(" ") !== this.matrix.join(" "))) { this.embed(m); }
return this.doc.addContent(`/${this.id} ${op}`);
}
}
class PDFLinearGradient extends PDFGradient {
constructor(doc, x1, y1, x2, y2) {
super(doc);
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
shader(fn) {
return this.doc.ref({
ShadingType: 2,
ColorSpace: this._colorSpace,
Coords: [this.x1, this.y1, this.x2, this.y2],
Function: fn,
Extend: [true, true]});
}
opacityGradient() {
return new PDFLinearGradient(this.doc, this.x1, this.y1, this.x2, this.y2);
}
}
class PDFRadialGradient extends PDFGradient {
constructor(doc, x1, y1, r1, x2, y2, r2) {
super(doc);
this.doc = doc;
this.x1 = x1;
this.y1 = y1;
this.r1 = r1;
this.x2 = x2;
this.y2 = y2;
this.r2 = r2;
}
shader(fn) {
return this.doc.ref({
ShadingType: 3,
ColorSpace: this._colorSpace,
Coords: [this.x1, this.y1, this.r1, this.x2, this.y2, this.r2],
Function: fn,
Extend: [true, true]});
}
opacityGradient() {
return new PDFRadialGradient(this.doc, this.x1, this.y1, this.r1, this.x2, this.y2, this.r2);
}
}
export default {PDFGradient, PDFLinearGradient, PDFRadialGradient};