pdfmake
Version:
Client/server side PDF printing in pure JavaScript
404 lines (337 loc) • 12.3 kB
JavaScript
import TextDecorator from './TextDecorator';
import TextInlines from './TextInlines';
import { isNumber } from './helpers/variableType';
import SVGtoPDF from './3rd-party/svg-to-pdfkit';
const findFont = (fonts, requiredFonts, defaultFont) => {
for (let i = 0; i < requiredFonts.length; i++) {
let requiredFont = requiredFonts[i].toLowerCase();
for (let font in fonts) {
if (font.toLowerCase() === requiredFont) {
return font;
}
}
}
return defaultFont;
};
/**
* Shift the "y" height of the text baseline up or down (superscript or subscript,
* respectively). The exact shift can / should be changed according to standard
* conventions.
*
* @param {number} y
* @param {object} inline
* @returns {number}
*/
const offsetText = (y, inline) => {
let newY = y;
if (inline.sup) {
newY -= inline.fontSize * 0.75;
}
if (inline.sub) {
newY += inline.fontSize * 0.35;
}
return newY;
};
class Renderer {
constructor(pdfDocument, progressCallback) {
this.pdfDocument = pdfDocument;
this.progressCallback = progressCallback;
}
renderPages(pages) {
this.pdfDocument._pdfMakePages = pages; // TODO: Why?
let totalItems = 0;
if (this.progressCallback) {
pages.forEach(page => {
totalItems += page.items.length;
});
}
let renderedItems = 0;
for (let i = 0; i < pages.length; i++) {
this.pdfDocument.addPage({ size: [pages[i].pageSize.width, pages[i].pageSize.height] });
let page = pages[i];
for (let ii = 0, il = page.items.length; ii < il; ii++) {
let item = page.items[ii];
switch (item.type) {
case 'vector':
this.renderVector(item.item);
break;
case 'line':
this.renderLine(item.item, item.item.x, item.item.y);
break;
case 'image':
this.renderImage(item.item);
break;
case 'svg':
this.renderSVG(item.item);
break;
case 'attachment':
this.renderAttachment(item.item);
break;
case 'beginClip':
this.beginClip(item.item);
break;
case 'endClip':
this.endClip();
break;
}
renderedItems++;
if (this.progressCallback) {
this.progressCallback(renderedItems / totalItems);
}
}
if (page.watermark) {
this.renderWatermark(page);
}
}
}
renderLine(line, x, y) {
function preparePageNodeRefLine(_pageNodeRef, inline) {
let newWidth;
let diffWidth;
let textInlines = new TextInlines(null);
if (_pageNodeRef.positions === undefined) {
throw new Error('Page reference id not found');
}
let pageNumber = _pageNodeRef.positions[0].pageNumber.toString();
inline.text = pageNumber;
newWidth = textInlines.widthOfText(inline.text, inline);
diffWidth = inline.width - newWidth;
inline.width = newWidth;
switch (inline.alignment) {
case 'right':
inline.x += diffWidth;
break;
case 'center':
inline.x += diffWidth / 2;
break;
}
}
if (line._pageNodeRef) {
preparePageNodeRefLine(line._pageNodeRef, line.inlines[0]);
}
x = x || 0;
y = y || 0;
let lineHeight = line.getHeight();
let ascenderHeight = line.getAscenderHeight();
let descent = lineHeight - ascenderHeight;
const textDecorator = new TextDecorator(this.pdfDocument);
textDecorator.drawBackground(line, x, y);
//TODO: line.optimizeInlines();
//TOOD: lines without differently styled inlines should be written to pdf as one stream
for (let i = 0, l = line.inlines.length; i < l; i++) {
let inline = line.inlines[i];
let shiftToBaseline = lineHeight - ((inline.font.ascender / 1000) * inline.fontSize) - descent;
if (inline._pageNodeRef) {
preparePageNodeRefLine(inline._pageNodeRef, inline);
}
let options = {
lineBreak: false,
textWidth: inline.width,
characterSpacing: inline.characterSpacing,
wordCount: 1,
link: inline.link
};
if (inline.linkToDestination) {
options.goTo = inline.linkToDestination;
}
if (line.id && i === 0) {
options.destination = line.id;
}
if (inline.fontFeatures) {
options.features = inline.fontFeatures;
}
let opacity = isNumber(inline.opacity) ? inline.opacity : 1;
this.pdfDocument.opacity(opacity);
this.pdfDocument.fill(inline.color || 'black');
this.pdfDocument._font = inline.font;
this.pdfDocument.fontSize(inline.fontSize);
let shiftedY = offsetText(y + shiftToBaseline, inline);
this.pdfDocument.text(inline.text, x + inline.x, shiftedY, options);
if (inline.linkToPage) {
this.pdfDocument.ref({ Type: 'Action', S: 'GoTo', D: [inline.linkToPage, 0, 0] }).end();
this.pdfDocument.annotate(x + inline.x, shiftedY, inline.width, inline.height, { Subtype: 'Link', Dest: [inline.linkToPage - 1, 'XYZ', null, null, null] });
}
}
// Decorations won't draw correctly for superscript
textDecorator.drawDecorations(line, x, y);
}
renderVector(vector) {
//TODO: pdf optimization (there's no need to write all properties everytime)
this.pdfDocument.lineWidth(vector.lineWidth || 1);
if (vector.dash) {
this.pdfDocument.dash(vector.dash.length, { space: vector.dash.space || vector.dash.length, phase: vector.dash.phase || 0 });
} else {
this.pdfDocument.undash();
}
this.pdfDocument.lineJoin(vector.lineJoin || 'miter');
this.pdfDocument.lineCap(vector.lineCap || 'butt');
//TODO: clipping
let gradient = null;
switch (vector.type) {
case 'ellipse':
this.pdfDocument.ellipse(vector.x, vector.y, vector.r1, vector.r2);
if (vector.linearGradient) {
gradient = this.pdfDocument.linearGradient(vector.x - vector.r1, vector.y, vector.x + vector.r1, vector.y);
}
break;
case 'rect':
if (vector.r) {
this.pdfDocument.roundedRect(vector.x, vector.y, vector.w, vector.h, vector.r);
} else {
this.pdfDocument.rect(vector.x, vector.y, vector.w, vector.h);
}
if (vector.linearGradient) {
gradient = this.pdfDocument.linearGradient(vector.x, vector.y, vector.x + vector.w, vector.y);
}
break;
case 'line':
this.pdfDocument.moveTo(vector.x1, vector.y1);
this.pdfDocument.lineTo(vector.x2, vector.y2);
break;
case 'polyline':
if (vector.points.length === 0) {
break;
}
this.pdfDocument.moveTo(vector.points[0].x, vector.points[0].y);
for (let i = 1, l = vector.points.length; i < l; i++) {
this.pdfDocument.lineTo(vector.points[i].x, vector.points[i].y);
}
if (vector.points.length > 1) {
let p1 = vector.points[0];
let pn = vector.points[vector.points.length - 1];
if (vector.closePath || p1.x === pn.x && p1.y === pn.y) {
this.pdfDocument.closePath();
}
}
break;
case 'path':
this.pdfDocument.path(vector.d);
break;
}
if (vector.linearGradient && gradient) {
let step = 1 / (vector.linearGradient.length - 1);
for (let i = 0; i < vector.linearGradient.length; i++) {
gradient.stop(i * step, vector.linearGradient[i]);
}
vector.color = gradient;
}
let patternColor = this.pdfDocument.providePattern(vector.color);
if (patternColor !== null) {
vector.color = patternColor;
}
let fillOpacity = isNumber(vector.fillOpacity) ? vector.fillOpacity : 1;
let strokeOpacity = isNumber(vector.strokeOpacity) ? vector.strokeOpacity : 1;
if (vector.color && vector.lineColor) {
this.pdfDocument.fillColor(vector.color, fillOpacity);
this.pdfDocument.strokeColor(vector.lineColor, strokeOpacity);
this.pdfDocument.fillAndStroke();
} else if (vector.color) {
this.pdfDocument.fillColor(vector.color, fillOpacity);
this.pdfDocument.fill();
} else {
this.pdfDocument.strokeColor(vector.lineColor || 'black', strokeOpacity);
this.pdfDocument.stroke();
}
}
renderImage(image) {
let opacity = isNumber(image.opacity) ? image.opacity : 1;
this.pdfDocument.opacity(opacity);
if (image.cover) {
const align = image.cover.align || 'center';
const valign = image.cover.valign || 'center';
const width = image.cover.width ? image.cover.width : image.width;
const height = image.cover.height ? image.cover.height : image.height;
this.pdfDocument.save();
this.pdfDocument.rect(image.x, image.y, width, height).clip();
this.pdfDocument.image(image.image, image.x, image.y, { cover: [width, height], align: align, valign: valign });
this.pdfDocument.restore();
} else {
this.pdfDocument.image(image.image, image.x, image.y, { width: image._width, height: image._height });
}
if (image.link) {
this.pdfDocument.link(image.x, image.y, image._width, image._height, image.link);
}
if (image.linkToPage) {
this.pdfDocument.ref({ Type: 'Action', S: 'GoTo', D: [image.linkToPage, 0, 0] }).end();
this.pdfDocument.annotate(image.x, image.y, image._width, image._height, { Subtype: 'Link', Dest: [image.linkToPage - 1, 'XYZ', null, null, null] });
}
if (image.linkToDestination) {
this.pdfDocument.goTo(image.x, image.y, image._width, image._height, image.linkToDestination);
}
if (image.linkToFile) {
const attachment = this.pdfDocument.provideAttachment(image.linkToFile);
this.pdfDocument.fileAnnotation(
image.x,
image.y,
image._width,
image._height,
attachment,
// add empty rectangle as file annotation appearance with the same size as the rendered image
{
AP: {
N: {
Type: 'XObject',
Subtype: 'Form',
FormType: 1,
BBox: [image.x, image.y, image._width, image._height]
}
},
}
);
}
}
renderSVG(svg) {
let options = Object.assign({ width: svg._width, height: svg._height, assumePt: true }, svg.options);
options.fontCallback = (family, bold, italic) => {
let fontsFamily = family.split(',').map(f => f.trim().replace(/('|")/g, ''));
let font = findFont(this.pdfDocument.fonts, fontsFamily, svg.font || 'Roboto');
let fontFile = this.pdfDocument.getFontFile(font, bold, italic);
if (fontFile === null) {
let type = this.pdfDocument.getFontType(bold, italic);
throw new Error(`Font '${font}' in style '${type}' is not defined in the font section of the document definition.`);
}
return fontFile;
};
SVGtoPDF(this.pdfDocument, svg.svg, svg.x, svg.y, options);
if (svg.link) {
this.pdfDocument.link(svg.x, svg.y, svg._width, svg._height, svg.link);
}
if (svg.linkToPage) {
this.pdfDocument.ref({ Type: 'Action', S: 'GoTo', D: [svg.linkToPage, 0, 0] }).end();
this.pdfDocument.annotate(svg.x, svg.y, svg._width, svg._height, { Subtype: 'Link', Dest: [svg.linkToPage - 1, 'XYZ', null, null, null] });
}
if (svg.linkToDestination) {
this.pdfDocument.goTo(svg.x, svg.y, svg._width, svg._height, svg.linkToDestination);
}
}
renderAttachment(attachment) {
const file = this.pdfDocument.provideAttachment(attachment.attachment);
const options = {};
if (attachment.icon) {
options.Name = attachment.icon;
}
this.pdfDocument.fileAnnotation(attachment.x, attachment.y, attachment._width, attachment._height, file, options);
}
beginClip(rect) {
this.pdfDocument.save();
this.pdfDocument.addContent(`${rect.x} ${rect.y} ${rect.width} ${rect.height} re`);
this.pdfDocument.clip();
}
endClip() {
this.pdfDocument.restore();
}
renderWatermark(page) {
let watermark = page.watermark;
this.pdfDocument.fill(watermark.color);
this.pdfDocument.opacity(watermark.opacity);
this.pdfDocument.save();
this.pdfDocument.rotate(watermark.angle, { origin: [this.pdfDocument.page.width / 2, this.pdfDocument.page.height / 2] });
let x = this.pdfDocument.page.width / 2 - watermark._size.size.width / 2;
let y = this.pdfDocument.page.height / 2 - watermark._size.size.height / 2;
this.pdfDocument._font = watermark.font;
this.pdfDocument.fontSize(watermark.fontSize);
this.pdfDocument.text(watermark.text, x, y, { lineBreak: false });
this.pdfDocument.restore();
}
}
export default Renderer;