pdfmake
Version:
Client/server side PDF printing in pure JavaScript
716 lines (584 loc) • 20 kB
JavaScript
import TextInlines from './TextInlines';
import StyleContextStack from './StyleContextStack';
import ColumnCalculator from './columnCalculator';
import { defaultTableLayout } from './tableLayouts';
import { isString, isNumber, isObject } from './helpers/variableType';
import { stringifyNode, getNodeId, getNodeMargin } from './helpers/node';
import { pack } from './helpers/tools';
import qrEncoder from './qrEnc.js';
class DocMeasure {
constructor(
pdfDocument,
styleDictionary,
defaultStyle,
svgMeasure,
tableLayouts
) {
this.pdfDocument = pdfDocument;
this.textInlines = new TextInlines(pdfDocument);
this.styleStack = new StyleContextStack(styleDictionary, defaultStyle);
this.svgMeasure = svgMeasure;
this.tableLayouts = tableLayouts;
this.autoImageIndex = 1;
}
/**
* Measures all nodes and sets min/max-width properties required for the second
* layout-pass.
*
* @param {object} docStructure document-definition-object
* @returns {object} document-measurement-object
*/
measureDocument(docStructure) {
return this.measureNode(docStructure);
}
measureNode(node) {
return this.styleStack.auto(node, () => {
// TODO: refactor + rethink whether this is the proper way to handle margins
node._margin = getNodeMargin(node, this.styleStack);
if (node.columns) {
return extendMargins(this.measureColumns(node));
} else if (node.stack) {
return extendMargins(this.measureVerticalContainer(node));
} else if (node.ul) {
return extendMargins(this.measureUnorderedList(node));
} else if (node.ol) {
return extendMargins(this.measureOrderedList(node));
} else if (node.table) {
return extendMargins(this.measureTable(node));
} else if (node.text !== undefined) {
return extendMargins(this.measureLeaf(node));
} else if (node.toc) {
return extendMargins(this.measureToc(node));
} else if (node.image) {
return extendMargins(this.measureImage(node));
} else if (node.svg) {
return extendMargins(this.measureSVG(node));
} else if (node.canvas) {
return extendMargins(this.measureCanvas(node));
} else if (node.qr) {
return extendMargins(this.measureQr(node));
} else if (node.attachment) {
return extendMargins(this.measureAttachment(node));
} else {
throw new Error(`Unrecognized document structure: ${stringifyNode(node)}`);
}
});
function extendMargins(node) {
let margin = node._margin;
if (margin) {
node._minWidth += margin[0] + margin[2];
node._maxWidth += margin[0] + margin[2];
}
return node;
}
}
measureImageWithDimensions(node, dimensions) {
if (node.fit) {
let factor = (dimensions.width / dimensions.height > node.fit[0] / node.fit[1]) ? node.fit[0] / dimensions.width : node.fit[1] / dimensions.height;
node._width = node._minWidth = node._maxWidth = dimensions.width * factor;
node._height = dimensions.height * factor;
} else if (node.cover) {
node._width = node._minWidth = node._maxWidth = node.cover.width;
node._height = node._minHeight = node._maxHeight = node.cover.height;
} else {
node._width = node._minWidth = node._maxWidth = node.width || dimensions.width;
node._height = node.height || (dimensions.height * node._width / dimensions.width);
if (isNumber(node.maxWidth) && node.maxWidth < node._width) {
node._width = node._minWidth = node._maxWidth = node.maxWidth;
node._height = node._width * dimensions.height / dimensions.width;
}
if (isNumber(node.maxHeight) && node.maxHeight < node._height) {
node._height = node.maxHeight;
node._width = node._minWidth = node._maxWidth = node._height * dimensions.width / dimensions.height;
}
if (isNumber(node.minWidth) && node.minWidth > node._width) {
node._width = node._minWidth = node._maxWidth = node.minWidth;
node._height = node._width * dimensions.height / dimensions.width;
}
if (isNumber(node.minHeight) && node.minHeight > node._height) {
node._height = node.minHeight;
node._width = node._minWidth = node._maxWidth = node._height * dimensions.width / dimensions.height;
}
}
node._alignment = this.styleStack.getProperty('alignment');
}
convertIfBase64Image(node) {
if (/^data:image\/(jpeg|jpg|png);base64,/.test(node.image)) { // base64 image
let label = `$$pdfmake$$${this.autoImageIndex++}`;
this.pdfDocument.images[label] = node.image;
node.image = label;
}
}
measureImage(node) {
this.convertIfBase64Image(node);
let image = this.pdfDocument.provideImage(node.image);
let imageSize = { width: image.width, height: image.height };
if (image.constructor.name === 'JPEG') {
// If EXIF orientation calls for it, swap width and height
if (image.orientation > 4) {
imageSize = { width: image.height, height: image.width };
}
}
this.measureImageWithDimensions(node, imageSize);
return node;
}
measureSVG(node) {
let dimensions = this.svgMeasure.measureSVG(node.svg);
this.measureImageWithDimensions(node, dimensions);
node.font = this.styleStack.getProperty('font');
// scale SVG based on final dimension
node.svg = this.svgMeasure.writeDimensions(node.svg, { width: node._width, height: node._height });
return node;
}
measureLeaf(node) {
if (node._textRef && node._textRef._textNodeRef.text) {
node.text = node._textRef._textNodeRef.text;
}
// Make sure style properties of the node itself are considered when building inlines.
// We could also just pass [node] to buildInlines, but that fails for bullet points.
let styleStack = this.styleStack.clone();
styleStack.push(node);
let data = this.textInlines.buildInlines(node.text, styleStack);
node._inlines = data.items;
node._minWidth = data.minWidth;
node._maxWidth = data.maxWidth;
return node;
}
measureToc(node) {
if (node.toc.title) {
node.toc.title = this.measureNode(node.toc.title);
}
if (node.toc._items.length > 0) {
let body = [];
let textStyle = node.toc.textStyle || {};
let numberStyle = node.toc.numberStyle || textStyle;
let textMargin = node.toc.textMargin || [0, 0, 0, 0];
for (let i = 0, l = node.toc._items.length; i < l; i++) {
let item = node.toc._items[i];
let lineStyle = item._textNodeRef.tocStyle || textStyle;
let lineMargin = item._textNodeRef.tocMargin || textMargin;
let lineNumberStyle = item._textNodeRef.tocNumberStyle || numberStyle;
let destination = getNodeId(item._nodeRef);
body.push([
{ text: item._textNodeRef.text, linkToDestination: destination, alignment: 'left', style: lineStyle, margin: lineMargin },
{ text: '00000', linkToDestination: destination, alignment: 'right', _tocItemRef: item._nodeRef, style: lineNumberStyle, margin: [0, lineMargin[1], 0, lineMargin[3]] }
]);
}
node.toc._table = {
table: {
dontBreakRows: true,
widths: ['*', 'auto'],
body: body
},
layout: 'noBorders'
};
node.toc._table = this.measureNode(node.toc._table);
}
return node;
}
measureVerticalContainer(node) {
let items = node.stack;
node._minWidth = 0;
node._maxWidth = 0;
for (let i = 0, l = items.length; i < l; i++) {
items[i] = this.measureNode(items[i]);
node._minWidth = Math.max(node._minWidth, items[i]._minWidth);
node._maxWidth = Math.max(node._maxWidth, items[i]._maxWidth);
}
return node;
}
gapSizeForList() {
return this.textInlines.sizeOfText('9. ', this.styleStack);
}
buildUnorderedMarker(styleStack, gapSize, type) {
function buildDisc(gapSize, color) {
// TODO: ascender-based calculations
let radius = gapSize.fontSize / 6;
return {
canvas: [{
x: radius,
y: (gapSize.height / gapSize.lineHeight) + gapSize.descender - gapSize.fontSize / 3,
r1: radius,
r2: radius,
type: 'ellipse',
color: color
}]
};
}
function buildSquare(gapSize, color) {
// TODO: ascender-based calculations
let size = gapSize.fontSize / 3;
return {
canvas: [{
x: 0,
y: (gapSize.height / gapSize.lineHeight) + gapSize.descender - (gapSize.fontSize / 3) - (size / 2),
h: size,
w: size,
type: 'rect',
color: color
}]
};
}
function buildCircle(gapSize, color) {
// TODO: ascender-based calculations
let radius = gapSize.fontSize / 6;
return {
canvas: [{
x: radius,
y: (gapSize.height / gapSize.lineHeight) + gapSize.descender - gapSize.fontSize / 3,
r1: radius,
r2: radius,
type: 'ellipse',
lineColor: color
}]
};
}
let marker;
let color = styleStack.getProperty('markerColor') || styleStack.getProperty('color') || 'black';
switch (type) {
case 'circle':
marker = buildCircle(gapSize, color);
break;
case 'square':
marker = buildSquare(gapSize, color);
break;
case 'none':
marker = {};
break;
case 'disc':
default:
marker = buildDisc(gapSize, color);
break;
}
marker._minWidth = marker._maxWidth = gapSize.width;
marker._minHeight = marker._maxHeight = gapSize.height;
return marker;
}
buildOrderedMarker(counter, styleStack, type, separator) {
function prepareAlpha(counter) {
function toAlpha(num) {
return (num >= 26 ? toAlpha((num / 26 >> 0) - 1) : '') + 'abcdefghijklmnopqrstuvwxyz'[num % 26 >> 0];
}
if (counter < 1) {
return counter.toString();
}
return toAlpha(counter - 1);
}
function prepareRoman(counter) {
if (counter < 1 || counter > 4999) {
return counter.toString();
}
let num = counter;
let lookup = { M: 1000, CM: 900, D: 500, CD: 400, C: 100, XC: 90, L: 50, XL: 40, X: 10, IX: 9, V: 5, IV: 4, I: 1 };
let roman = '';
for (let i in lookup) {
while (num >= lookup[i]) {
roman += i;
num -= lookup[i];
}
}
return roman;
}
function prepareDecimal(counter) {
return counter.toString();
}
let counterText;
switch (type) {
case 'none':
counterText = null;
break;
case 'upper-alpha':
counterText = prepareAlpha(counter).toUpperCase();
break;
case 'lower-alpha':
counterText = prepareAlpha(counter);
break;
case 'upper-roman':
counterText = prepareRoman(counter);
break;
case 'lower-roman':
counterText = prepareRoman(counter).toLowerCase();
break;
case 'decimal':
default:
counterText = prepareDecimal(counter);
break;
}
if (counterText === null) {
return {};
}
if (separator) {
if (Array.isArray(separator)) {
if (separator[0]) {
counterText = separator[0] + counterText;
}
if (separator[1]) {
counterText += separator[1];
}
counterText += ' ';
} else {
counterText += `${separator} `;
}
}
let textArray = { text: counterText };
let markerColor = styleStack.getProperty('markerColor');
if (markerColor) {
textArray.color = markerColor;
}
return { _inlines: this.textInlines.buildInlines(textArray, styleStack).items };
}
measureUnorderedList(node) {
let style = this.styleStack.clone();
let items = node.ul;
node.type = node.type || 'disc';
node._gapSize = this.gapSizeForList();
node._minWidth = 0;
node._maxWidth = 0;
for (let i = 0, l = items.length; i < l; i++) {
let item = items[i] = this.measureNode(items[i]);
if (!item.ol && !item.ul) {
item.listMarker = this.buildUnorderedMarker(style, node._gapSize, item.listType || node.type);
}
node._minWidth = Math.max(node._minWidth, items[i]._minWidth + node._gapSize.width);
node._maxWidth = Math.max(node._maxWidth, items[i]._maxWidth + node._gapSize.width);
}
return node;
}
measureOrderedList(node) {
let style = this.styleStack.clone();
let items = node.ol;
node.type = node.type || 'decimal';
node.separator = node.separator || '.';
node.reversed = node.reversed || false;
if (!isNumber(node.start)) {
node.start = node.reversed ? items.length : 1;
}
node._gapSize = this.gapSizeForList();
node._minWidth = 0;
node._maxWidth = 0;
let counter = node.start;
for (let i = 0, l = items.length; i < l; i++) {
let item = items[i] = this.measureNode(items[i]);
if (!item.ol && !item.ul) {
let counterValue = isNumber(item.counter) ? item.counter : counter;
item.listMarker = this.buildOrderedMarker(counterValue, style, item.listType || node.type, node.separator);
if (item.listMarker._inlines) {
node._gapSize.width = Math.max(node._gapSize.width, item.listMarker._inlines[0].width);
}
if (node.reversed) {
counter--;
} else {
counter++;
}
}
node._minWidth = Math.max(node._minWidth, items[i]._minWidth);
node._maxWidth = Math.max(node._maxWidth, items[i]._maxWidth);
}
node._minWidth += node._gapSize.width;
node._maxWidth += node._gapSize.width;
for (let i = 0, l = items.length; i < l; i++) {
let item = items[i];
if (!item.ol && !item.ul) {
item.listMarker._minWidth = item.listMarker._maxWidth = node._gapSize.width;
}
}
return node;
}
measureColumns(node) {
let columns = node.columns;
node._gap = this.styleStack.getProperty('columnGap') || 0;
for (let i = 0, l = columns.length; i < l; i++) {
columns[i] = this.measureNode(columns[i]);
}
let measures = ColumnCalculator.measureMinMax(columns);
let numGaps = (columns.length > 0) ? (columns.length - 1) : 0;
node._minWidth = measures.min + node._gap * numGaps;
node._maxWidth = measures.max + node._gap * numGaps;
return node;
}
measureTable(node) {
extendTableWidths(node);
node._layout = getLayout(this.tableLayouts);
node._offsets = getOffsets(node._layout);
let colSpans = [];
let col;
let row;
let cols;
let rows;
for (col = 0, cols = node.table.body[0].length; col < cols; col++) {
let c = node.table.widths[col];
c._minWidth = 0;
c._maxWidth = 0;
for (row = 0, rows = node.table.body.length; row < rows; row++) {
let rowData = node.table.body[row];
let data = rowData[col];
if (data === undefined) {
throw new Error(`Malformed table row, a cell is undefined.\nRow index: ${row}\nColumn index: ${col}\nRow data: ${stringifyNode(rowData)}`);
}
if (data === null) { // transform to object
data = '';
}
if (!data._span) {
data = rowData[col] = this.styleStack.auto(data, measureCb(this, data));
if (data.colSpan && data.colSpan > 1) {
markSpans(rowData, col, data.colSpan);
colSpans.push({ col: col, span: data.colSpan, minWidth: data._minWidth, maxWidth: data._maxWidth });
} else {
c._minWidth = Math.max(c._minWidth, data._minWidth);
c._maxWidth = Math.max(c._maxWidth, data._maxWidth);
}
}
if (data.rowSpan && data.rowSpan > 1) {
markVSpans(node.table, row, col, data.rowSpan);
}
}
}
extendWidthsForColSpans();
let measures = ColumnCalculator.measureMinMax(node.table.widths);
node._minWidth = measures.min + node._offsets.total;
node._maxWidth = measures.max + node._offsets.total;
return node;
function measureCb(_this, data) {
return () => {
if (isObject(data)) {
data.fillColor = _this.styleStack.getProperty('fillColor');
data.fillOpacity = _this.styleStack.getProperty('fillOpacity');
}
return _this.measureNode(data);
};
}
function getLayout(tableLayouts) {
let layout = node.layout;
if (isString(layout)) {
layout = tableLayouts[layout];
}
return pack(defaultTableLayout, layout);
}
function getOffsets(layout) {
let offsets = [];
let totalOffset = 0;
let prevRightPadding = 0;
for (let i = 0, l = node.table.widths.length; i < l; i++) {
let lOffset = prevRightPadding + layout.vLineWidth(i, node) + layout.paddingLeft(i, node);
offsets.push(lOffset);
totalOffset += lOffset;
prevRightPadding = layout.paddingRight(i, node);
}
totalOffset += prevRightPadding + layout.vLineWidth(node.table.widths.length, node);
return {
total: totalOffset,
offsets: offsets
};
}
function extendWidthsForColSpans() {
let q;
let j;
for (let i = 0, l = colSpans.length; i < l; i++) {
let span = colSpans[i];
let currentMinMax = getMinMax(span.col, span.span, node._offsets);
let minDifference = span.minWidth - currentMinMax.minWidth;
let maxDifference = span.maxWidth - currentMinMax.maxWidth;
if (minDifference > 0) {
q = minDifference / span.span;
for (j = 0; j < span.span; j++) {
node.table.widths[span.col + j]._minWidth += q;
}
}
if (maxDifference > 0) {
q = maxDifference / span.span;
for (j = 0; j < span.span; j++) {
node.table.widths[span.col + j]._maxWidth += q;
}
}
}
}
function getMinMax(col, span, offsets) {
let result = { minWidth: 0, maxWidth: 0 };
for (let i = 0; i < span; i++) {
result.minWidth += node.table.widths[col + i]._minWidth + (i ? offsets.offsets[col + i] : 0);
result.maxWidth += node.table.widths[col + i]._maxWidth + (i ? offsets.offsets[col + i] : 0);
}
return result;
}
function markSpans(rowData, col, span) {
for (let i = 1; i < span; i++) {
rowData[col + i] = {
_span: true,
_minWidth: 0,
_maxWidth: 0,
rowSpan: rowData[col].rowSpan
};
}
}
function markVSpans(table, row, col, span) {
for (let i = 1; i < span; i++) {
table.body[row + i][col] = {
_span: true,
_minWidth: 0,
_maxWidth: 0,
fillColor: table.body[row][col].fillColor,
fillOpacity: table.body[row][col].fillOpacity
};
}
}
function extendTableWidths(node) {
if (!node.table.widths) {
node.table.widths = 'auto';
}
if (isString(node.table.widths)) {
node.table.widths = [node.table.widths];
while (node.table.widths.length < node.table.body[0].length) {
node.table.widths.push(node.table.widths[node.table.widths.length - 1]);
}
}
for (let i = 0, l = node.table.widths.length; i < l; i++) {
let w = node.table.widths[i];
if (isNumber(w) || isString(w)) {
node.table.widths[i] = { width: w };
}
}
}
}
measureCanvas(node) {
let w = 0;
let h = 0;
for (let i = 0, l = node.canvas.length; i < l; i++) {
let vector = node.canvas[i];
switch (vector.type) {
case 'ellipse':
w = Math.max(w, vector.x + vector.r1);
h = Math.max(h, vector.y + vector.r2);
break;
case 'rect':
w = Math.max(w, vector.x + vector.w);
h = Math.max(h, vector.y + vector.h);
break;
case 'line':
w = Math.max(w, vector.x1, vector.x2);
h = Math.max(h, vector.y1, vector.y2);
break;
case 'polyline':
for (let i2 = 0, l2 = vector.points.length; i2 < l2; i2++) {
w = Math.max(w, vector.points[i2].x);
h = Math.max(h, vector.points[i2].y);
}
break;
}
}
node._minWidth = node._maxWidth = w;
node._minHeight = node._maxHeight = h;
node._alignment = this.styleStack.getProperty('alignment');
return node;
}
measureQr(node) {
node = qrEncoder.measure(node);
node._alignment = this.styleStack.getProperty('alignment');
return node;
}
measureAttachment(node) {
node._width = node.width || 7;
node._height = node.height || 18;
return node;
}
}
export default DocMeasure;