pdfmake 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -845,11 +845,13 @@ class LayoutBuilder {
845
845
  // We store a reference of the ending cell in the first cell of the rowspan
846
846
  cell._endingCell = rowSpanRightEndingCell;
847
847
  cell._endingCell._startingRowSpanY = cell._startingRowSpanY;
848
+ cell._endingCell._startingRowSpanPage = cell._startingRowSpanPage;
848
849
  }
849
850
  if (rowSpanLeftEndingCell) {
850
851
  // We store a reference of the left ending cell in the first cell of the rowspan
851
852
  cell._leftEndingCell = rowSpanLeftEndingCell;
852
853
  cell._leftEndingCell._startingRowSpanY = cell._startingRowSpanY;
854
+ cell._leftEndingCell._startingRowSpanPage = cell._startingRowSpanPage;
853
855
  }
854
856
 
855
857
  // If we are after a cell that started a rowspan
@@ -884,7 +886,13 @@ class LayoutBuilder {
884
886
  if (dontBreakRows) {
885
887
  // Calculate how many points we have to discount to Y when dontBreakRows and rowSpan are combined
886
888
  const ctxBeforeRowSpanLastRow = this.writer.contextStack[this.writer.contextStack.length - 1];
887
- discountY = ctxBeforeRowSpanLastRow.y - cell._startingRowSpanY;
889
+ const startsOnCurrentPage = typeof cell._startingRowSpanPage === 'number' && cell._startingRowSpanPage === ctxBeforeRowSpanLastRow.page;
890
+ if (startsOnCurrentPage && typeof cell._startingRowSpanY === 'number') {
891
+ discountY = ctxBeforeRowSpanLastRow.y - cell._startingRowSpanY;
892
+ }
893
+
894
+ // Do not increase Y by applying a negative discount.
895
+ discountY = Math.max(0, discountY);
888
896
  }
889
897
  let originalXOffset = 0;
890
898
  // If context was saved from an unbreakable block and we are not in an unbreakable block anymore
@@ -1046,6 +1054,7 @@ class LayoutBuilder {
1046
1054
  tableNode.table.body[i].forEach(cell => {
1047
1055
  if (cell.rowSpan && cell.rowSpan > 1) {
1048
1056
  cell._startingRowSpanY = this.writer.context().y;
1057
+ cell._startingRowSpanPage = this.writer.context().page;
1049
1058
  }
1050
1059
  });
1051
1060
  }
package/js/PDFDocument.js CHANGED
@@ -3,6 +3,7 @@
3
3
  exports.__esModule = true;
4
4
  exports.default = void 0;
5
5
  var _pdfkit = _interopRequireDefault(require("pdfkit"));
6
+ var _variableType = require("./helpers/variableType");
6
7
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
7
8
  const typeName = (bold, italics) => {
8
9
  let type = 'normal';
@@ -16,7 +17,7 @@ const typeName = (bold, italics) => {
16
17
  return type;
17
18
  };
18
19
  class PDFDocument extends _pdfkit.default {
19
- constructor(fonts = {}, images = {}, patterns = {}, attachments = {}, options = {}, virtualfs = null) {
20
+ constructor(fonts = {}, images = {}, patterns = {}, attachments = {}, options = {}, virtualfs = null, localAccessPolicy = undefined) {
20
21
  super(options);
21
22
  this.fonts = {};
22
23
  this.fontCache = {};
@@ -41,6 +42,7 @@ class PDFDocument extends _pdfkit.default {
41
42
  this.images = images;
42
43
  this.attachments = attachments;
43
44
  this.virtualfs = virtualfs;
45
+ this.localAccessPolicy = localAccessPolicy;
44
46
  }
45
47
  getFontType(bold, italics) {
46
48
  return typeName(bold, italics);
@@ -65,6 +67,8 @@ class PDFDocument extends _pdfkit.default {
65
67
  }
66
68
  if (this.virtualfs && this.virtualfs.existsSync(def[0])) {
67
69
  def[0] = this.virtualfs.readFileSync(def[0]);
70
+ } else {
71
+ this.validateLocalFile(def[0]);
68
72
  }
69
73
  this.fontCache[familyName][type] = this.font(...def)._font;
70
74
  }
@@ -89,8 +93,10 @@ class PDFDocument extends _pdfkit.default {
89
93
  return this._imageRegistry[src];
90
94
  }
91
95
  let image;
96
+ let imageSrc = realImageSrc(src);
97
+ this.validateLocalFile(imageSrc);
92
98
  try {
93
- image = this.openImage(realImageSrc(src));
99
+ image = this.openImage(imageSrc);
94
100
  if (!image) {
95
101
  throw new Error('No image');
96
102
  }
@@ -131,8 +137,19 @@ class PDFDocument extends _pdfkit.default {
131
137
  if (this.virtualfs && this.virtualfs.existsSync(attachment.src)) {
132
138
  return this.virtualfs.readFileSync(attachment.src);
133
139
  }
140
+ this.validateLocalFile(attachment.src);
134
141
  return attachment;
135
142
  }
143
+ resolveColor(color, defaultColor) {
144
+ color = color || defaultColor;
145
+ if (typeof this._normalizeColor === 'function') {
146
+ if ((0, _variableType.isString)(color) && this._normalizeColor(color) === null) {
147
+ // color is not valid
148
+ return defaultColor;
149
+ }
150
+ }
151
+ return color;
152
+ }
136
153
  setOpenActionAsPrint() {
137
154
  let printActionRef = this.ref({
138
155
  Type: 'Action',
@@ -142,5 +159,23 @@ class PDFDocument extends _pdfkit.default {
142
159
  this._root.data.OpenAction = printActionRef;
143
160
  printActionRef.end();
144
161
  }
162
+ file(src, options = {}) {
163
+ this.validateLocalFile(src);
164
+ return super.file(src, options);
165
+ }
166
+ validateLocalFile(path) {
167
+ if (typeof this.localAccessPolicy === 'undefined') {
168
+ return;
169
+ }
170
+ if (!(0, _variableType.isString)(path)) {
171
+ return;
172
+ }
173
+ if (/^data:/.test(path)) {
174
+ return;
175
+ }
176
+ if (this.localAccessPolicy(path) !== true) {
177
+ throw new Error(`Access to local file denied by resource access policy: ${path}`);
178
+ }
179
+ }
145
180
  }
146
181
  var _default = exports.default = PDFDocument;
package/js/Printer.js CHANGED
@@ -16,11 +16,13 @@ class PdfPrinter {
16
16
  * @param {object} fontDescriptors font definition dictionary
17
17
  * @param {object} virtualfs
18
18
  * @param {object} urlResolver
19
+ * @param {(path: string) => boolean} localAccessPolicy
19
20
  */
20
- constructor(fontDescriptors, virtualfs, urlResolver) {
21
+ constructor(fontDescriptors, virtualfs, urlResolver, localAccessPolicy) {
21
22
  this.fontDescriptors = fontDescriptors;
22
23
  this.virtualfs = virtualfs;
23
24
  this.urlResolver = urlResolver;
25
+ this.localAccessPolicy = localAccessPolicy;
24
26
  }
25
27
 
26
28
  /**
@@ -66,7 +68,7 @@ class PdfPrinter {
66
68
  info: createMetadata(docDefinition),
67
69
  font: null
68
70
  };
69
- this.pdfKitDoc = new _PDFDocument.default(this.fontDescriptors, docDefinition.images, docDefinition.patterns, docDefinition.attachments, pdfOptions, this.virtualfs);
71
+ this.pdfKitDoc = new _PDFDocument.default(this.fontDescriptors, docDefinition.images, docDefinition.patterns, docDefinition.attachments, pdfOptions, this.virtualfs, this.localAccessPolicy);
70
72
  embedFiles(docDefinition, this.pdfKitDoc);
71
73
  const builder = new _LayoutBuilder.default(pageSize, (0, _PageSize.normalizePageMargin)(docDefinition.pageMargins), new _SVGMeasure.default());
72
74
  builder.registerTableLayouts(_tableLayouts.tableLayouts);
package/js/Renderer.js CHANGED
@@ -171,7 +171,7 @@ class Renderer {
171
171
  }
172
172
  let opacity = (0, _variableType.isNumber)(inline.opacity) ? inline.opacity : 1;
173
173
  this.pdfDocument.opacity(opacity);
174
- this.pdfDocument.fill(inline.color || 'black');
174
+ this.pdfDocument.fill(this.pdfDocument.resolveColor(inline.color, 'black'));
175
175
  this.pdfDocument._font = inline.font;
176
176
  this.pdfDocument.fontSize(inline.fontSize);
177
177
  let shiftedY = offsetText(y + shiftToBaseline, inline);
@@ -264,14 +264,14 @@ class Renderer {
264
264
  let fillOpacity = (0, _variableType.isNumber)(vector.fillOpacity) ? vector.fillOpacity : 1;
265
265
  let strokeOpacity = (0, _variableType.isNumber)(vector.strokeOpacity) ? vector.strokeOpacity : 1;
266
266
  if (vector.color && vector.lineColor) {
267
- this.pdfDocument.fillColor(vector.color, fillOpacity);
268
- this.pdfDocument.strokeColor(vector.lineColor, strokeOpacity);
267
+ this.pdfDocument.fillColor(this.pdfDocument.resolveColor(vector.color, 'black'), fillOpacity);
268
+ this.pdfDocument.strokeColor(this.pdfDocument.resolveColor(vector.lineColor, 'black'), strokeOpacity);
269
269
  this.pdfDocument.fillAndStroke();
270
270
  } else if (vector.color) {
271
- this.pdfDocument.fillColor(vector.color, fillOpacity);
271
+ this.pdfDocument.fillColor(this.pdfDocument.resolveColor(vector.color, 'black'), fillOpacity);
272
272
  this.pdfDocument.fill();
273
273
  } else {
274
- this.pdfDocument.strokeColor(vector.lineColor || 'black', strokeOpacity);
274
+ this.pdfDocument.strokeColor(this.pdfDocument.resolveColor(vector.lineColor, 'black'), strokeOpacity);
275
275
  this.pdfDocument.stroke();
276
276
  }
277
277
  }
@@ -411,7 +411,7 @@ class Renderer {
411
411
  }
412
412
  renderWatermark(page) {
413
413
  let watermark = page.watermark;
414
- this.pdfDocument.fill(watermark.color);
414
+ this.pdfDocument.fill(this.pdfDocument.resolveColor(watermark.color, 'black'));
415
415
  this.pdfDocument.opacity(watermark.opacity);
416
416
  this.pdfDocument.save();
417
417
  this.pdfDocument.rotate(watermark.angle, {
@@ -5,9 +5,17 @@ exports.default = void 0;
5
5
  var _columnCalculator = _interopRequireDefault(require("./columnCalculator"));
6
6
  var _variableType = require("./helpers/variableType");
7
7
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
8
+ const PAGE_BREAK_VALUES = new Set(['before', 'beforeOdd', 'beforeEven', 'after', 'afterOdd', 'afterEven']);
9
+ const hasExplicitPageBreak = cell => {
10
+ if (!cell || typeof cell !== 'object') {
11
+ return false;
12
+ }
13
+ return PAGE_BREAK_VALUES.has(cell.pageBreak);
14
+ };
8
15
  class TableProcessor {
9
16
  constructor(tableNode) {
10
17
  this.tableNode = tableNode;
18
+ this._isCurrentRowUnbreakable = false;
11
19
  }
12
20
  beginTable(writer) {
13
21
  const getTableInnerContentWidth = () => {
@@ -146,7 +154,10 @@ class TableProcessor {
146
154
  writer.context().moveDown(this.topLineWidth);
147
155
  }
148
156
  this.rowTopPageY = writer.context().y + this.rowPaddingTop;
149
- if (this.dontBreakRows && rowIndex > 0) {
157
+ const rowCells = this.tableNode.table.body[rowIndex] || [];
158
+ const rowHasPageBreak = rowCells.some(hasExplicitPageBreak);
159
+ this._isCurrentRowUnbreakable = this.dontBreakRows && rowIndex > 0 && !rowHasPageBreak;
160
+ if (this._isCurrentRowUnbreakable) {
150
161
  writer.beginUnbreakableBlock();
151
162
  }
152
163
  this.rowTopY = writer.context().y;
@@ -536,7 +547,8 @@ class TableProcessor {
536
547
  if (this.headerRows && rowIndex === this.headerRows - 1) {
537
548
  this.headerRepeatable = writer.currentBlockToRepeatable();
538
549
  }
539
- if (this.dontBreakRows) {
550
+ const shouldCommitCurrentRowUnbreakable = this.dontBreakRows && (rowIndex === 0 || this._isCurrentRowUnbreakable);
551
+ if (shouldCommitCurrentRowUnbreakable) {
540
552
  const pageChangedCallback = () => {
541
553
  if (rowIndex > 0 && !this.headerRows && this.layout.hLineWhenBroken !== false) {
542
554
  // Draw the top border of the row after a page break
@@ -547,6 +559,7 @@ class TableProcessor {
547
559
  writer.commitUnbreakableBlock();
548
560
  writer.removeListener('pageChanged', pageChangedCallback);
549
561
  }
562
+ this._isCurrentRowUnbreakable = false;
550
563
  if (this.headerRepeatable && (rowIndex === this.rowsWithoutPageBreak - 1 || rowIndex === this.tableNode.table.body.length - 1)) {
551
564
  writer.commitUnbreakableBlock();
552
565
  writer.pushToRepeatables(this.headerRepeatable);
@@ -3,7 +3,7 @@
3
3
  exports.__esModule = true;
4
4
  exports.default = void 0;
5
5
  var _variableType = require("./helpers/variableType");
6
- const groupDecorations = line => {
6
+ const groupDecorations = (line, pdfDocument) => {
7
7
  let groups = [];
8
8
  let currentGroup = null;
9
9
  for (let i = 0, l = line.inlines.length; i < l; i++) {
@@ -16,7 +16,7 @@ const groupDecorations = line => {
16
16
  if (!Array.isArray(decoration)) {
17
17
  decoration = [decoration];
18
18
  }
19
- let color = inline.decorationColor || inline.color || 'black';
19
+ let color = pdfDocument.resolveColor(pdfDocument.resolveColor(inline.decorationColor, inline.color), 'black');
20
20
  let style = inline.decorationStyle || 'solid';
21
21
  let thickness = (0, _variableType.isNumber)(inline.decorationThickness) ? inline.decorationThickness : null;
22
22
  for (let ii = 0, ll = decoration.length; ii < ll; ii++) {
@@ -46,10 +46,10 @@ class TextDecorator {
46
46
  let height = line.getHeight();
47
47
  for (let i = 0, l = line.inlines.length; i < l; i++) {
48
48
  let inline = line.inlines[i];
49
- if (!inline.background) {
49
+ let color = this.pdfDocument.resolveColor(inline.background, undefined);
50
+ if (!color) {
50
51
  continue;
51
52
  }
52
- let color = inline.background;
53
53
  let patternColor = this.pdfDocument.providePattern(inline.background);
54
54
  if (patternColor !== null) {
55
55
  color = patternColor;
@@ -59,7 +59,7 @@ class TextDecorator {
59
59
  }
60
60
  }
61
61
  drawDecorations(line, x, y) {
62
- let groups = groupDecorations(line);
62
+ let groups = groupDecorations(line, this.pdfDocument);
63
63
  for (let i = 0, l = groups.length; i < l; i++) {
64
64
  this._drawDecoration(groups[i], x, y);
65
65
  }
package/js/URLResolver.js CHANGED
@@ -2,20 +2,52 @@
2
2
 
3
3
  exports.__esModule = true;
4
4
  exports.default = void 0;
5
- async function fetchUrl(url, headers = {}) {
6
- try {
7
- const response = await fetch(url, {
8
- headers
9
- });
10
- if (!response.ok) {
11
- throw new Error(`Failed to fetch (status code: ${response.status}, url: "${url}")`);
5
+ const MAX_REDIRECTS = 30;
6
+
7
+ /**
8
+ * @param {string} url
9
+ * @param {object} headers
10
+ * @param {(url: string) => boolean} urlAccessPolicy
11
+ * @returns {Promise<Response>}
12
+ */
13
+ async function fetchUrl(url, headers = {}, urlAccessPolicy) {
14
+ for (let i = 0; i <= MAX_REDIRECTS; i++) {
15
+ if (typeof urlAccessPolicy !== 'undefined' && urlAccessPolicy(url) !== true) {
16
+ throw new Error(`Access to URL denied by resource access policy: ${url}`);
17
+ }
18
+ try {
19
+ let response = await fetch(url, {
20
+ headers,
21
+ redirect: 'manual'
22
+ });
23
+
24
+ // redirect url
25
+ if (response.status >= 300 && response.status < 400) {
26
+ let location = response.headers.get('location');
27
+ if (!location) {
28
+ throw new Error('Redirect response missing Location header');
29
+ }
30
+ url = new URL(location, url).href;
31
+ continue;
32
+ }
33
+
34
+ // browsers do not support redirect: 'manual'
35
+ if (response.type === 'opaqueredirect') {
36
+ response = await fetch(url, {
37
+ headers
38
+ });
39
+ }
40
+ if (!response.ok) {
41
+ throw new Error(`Failed to fetch (status code: ${response.status})`);
42
+ }
43
+ return response;
44
+ } catch (error) {
45
+ throw new Error(`Network request failed (url: "${url}", error: ${error.message})`, {
46
+ cause: error
47
+ });
12
48
  }
13
- return await response.arrayBuffer();
14
- } catch (error) {
15
- throw new Error(`Network request failed (url: "${url}", error: ${error.message})`, {
16
- cause: error
17
- });
18
49
  }
50
+ throw new Error(`Network request failed (url: "${url}", error: Too many redirects)`);
19
51
  }
20
52
  class URLResolver {
21
53
  constructor(fs) {
@@ -36,10 +68,15 @@ class URLResolver {
36
68
  if (this.fs.existsSync(url)) {
37
69
  return; // url was downloaded earlier
38
70
  }
39
- if (typeof this.urlAccessPolicy !== 'undefined' && this.urlAccessPolicy(url) !== true) {
40
- throw new Error(`Access to URL denied by resource access policy: ${url}`);
71
+ const response = await fetchUrl(url, headers, this.urlAccessPolicy);
72
+
73
+ // validate access policy on redirected url (in browsers, only the final URL is validated)
74
+ if (response.redirected) {
75
+ if (typeof this.urlAccessPolicy !== 'undefined' && this.urlAccessPolicy(response.url) !== true) {
76
+ throw new Error(`Access to URL denied by resource access policy: ${response.url}`);
77
+ }
41
78
  }
42
- const buffer = await fetchUrl(url, headers);
79
+ const buffer = await response.arrayBuffer();
43
80
  this.fs.writeFileSync(url, buffer);
44
81
  }
45
82
  // else cannot be resolved
package/js/base.js CHANGED
@@ -12,6 +12,7 @@ class pdfmake {
12
12
  constructor() {
13
13
  this.virtualfs = _virtualFs.default;
14
14
  this.urlAccessPolicy = undefined;
15
+ this.localAccessPolicy = undefined;
15
16
  }
16
17
 
17
18
  /**
@@ -32,9 +33,12 @@ class pdfmake {
32
33
  if (typeof this.urlAccessPolicy === 'undefined' && isServer) {
33
34
  console.warn('No URL access policy defined. Consider using setUrlAccessPolicy() to restrict external resource downloads.');
34
35
  }
36
+ if (typeof this.localAccessPolicy === 'undefined' && isServer) {
37
+ console.warn('No local access policy defined. Consider using setLocalAccessPolicy() to restrict local file system access.');
38
+ }
35
39
  let urlResolver = new _URLResolver.default(this.virtualfs);
36
40
  urlResolver.setUrlAccessPolicy(this.urlAccessPolicy);
37
- let printer = new _Printer.default(this.fonts, this.virtualfs, urlResolver);
41
+ let printer = new _Printer.default(this.fonts, this.virtualfs, urlResolver, this.localAccessPolicy);
38
42
  const pdfDocumentPromise = printer.createPdfKitDocument(docDefinition, options);
39
43
  return this._transformToDocument(pdfDocumentPromise);
40
44
  }
package/js/index.js CHANGED
@@ -6,6 +6,16 @@ class pdfmake extends pdfmakeBase {
6
6
  constructor() {
7
7
  super();
8
8
  }
9
+
10
+ /**
11
+ * @param {(path: string) => boolean} callback
12
+ */
13
+ setLocalAccessPolicy(callback) {
14
+ if (callback !== undefined && typeof callback !== 'function') {
15
+ throw new Error("Parameter 'callback' has an invalid type. Function or undefined expected.");
16
+ }
17
+ this.localAccessPolicy = callback;
18
+ }
9
19
  _transformToDocument(doc) {
10
20
  return new OutputDocumentServer(doc);
11
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdfmake",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Client/server side PDF printing in pure JavaScript",
5
5
  "main": "js/index.js",
6
6
  "esnext": "src/index.js",
@@ -17,7 +17,7 @@
17
17
  "@babel/cli": "^7.28.6",
18
18
  "@babel/core": "^7.29.0",
19
19
  "@babel/plugin-transform-modules-commonjs": "^7.28.6",
20
- "@babel/preset-env": "^7.29.2",
20
+ "@babel/preset-env": "^7.29.5",
21
21
  "@eslint/js": "^10.0.1",
22
22
  "assert": "^2.1.0",
23
23
  "babel-loader": "^10.1.1",
@@ -25,25 +25,25 @@
25
25
  "browserify-zlib": "^0.2.0",
26
26
  "buffer": "^6.0.3",
27
27
  "core-js": "^3.49.0",
28
- "eslint": "^10.0.3",
29
- "eslint-plugin-jsdoc": "^62.8.0",
28
+ "eslint": "^10.3.0",
29
+ "eslint-plugin-jsdoc": "^62.9.0",
30
30
  "expose-loader": "^5.0.1",
31
31
  "file-saver": "^2.0.5",
32
- "globals": "^17.4.0",
32
+ "globals": "^17.6.0",
33
33
  "mocha": "^11.7.5",
34
34
  "npm-run-all": "^4.1.5",
35
35
  "process": "^0.11.10",
36
36
  "rewire": "^9.0.1",
37
37
  "shx": "^0.4.0",
38
- "sinon": "^21.0.3",
38
+ "sinon": "^22.0.0",
39
39
  "source-map-loader": "^5.0.0",
40
40
  "stream-browserify": "^3.0.0",
41
41
  "string-replace-webpack-plugin": "^0.1.3",
42
42
  "svg-to-pdfkit": "github:alafr/SVG-to-PDFKit#b091ebd4e7b7d2310eb1003511cd5de480f7e0e1",
43
- "terser-webpack-plugin": "^5.4.0",
43
+ "terser-webpack-plugin": "^5.5.0",
44
44
  "transform-loader": "^0.2.4",
45
45
  "util": "^0.12.5",
46
- "webpack": "^5.105.4",
46
+ "webpack": "^5.106.2",
47
47
  "webpack-cli": "^7.0.2"
48
48
  },
49
49
  "engines": {
@@ -942,11 +942,13 @@ class LayoutBuilder {
942
942
  // We store a reference of the ending cell in the first cell of the rowspan
943
943
  cell._endingCell = rowSpanRightEndingCell;
944
944
  cell._endingCell._startingRowSpanY = cell._startingRowSpanY;
945
+ cell._endingCell._startingRowSpanPage = cell._startingRowSpanPage;
945
946
  }
946
947
  if (rowSpanLeftEndingCell) {
947
948
  // We store a reference of the left ending cell in the first cell of the rowspan
948
949
  cell._leftEndingCell = rowSpanLeftEndingCell;
949
950
  cell._leftEndingCell._startingRowSpanY = cell._startingRowSpanY;
951
+ cell._leftEndingCell._startingRowSpanPage = cell._startingRowSpanPage;
950
952
  }
951
953
 
952
954
  // If we are after a cell that started a rowspan
@@ -984,7 +986,17 @@ class LayoutBuilder {
984
986
  if (dontBreakRows) {
985
987
  // Calculate how many points we have to discount to Y when dontBreakRows and rowSpan are combined
986
988
  const ctxBeforeRowSpanLastRow = this.writer.contextStack[this.writer.contextStack.length - 1];
987
- discountY = ctxBeforeRowSpanLastRow.y - cell._startingRowSpanY;
989
+ const startsOnCurrentPage = (
990
+ typeof cell._startingRowSpanPage === 'number' &&
991
+ cell._startingRowSpanPage === ctxBeforeRowSpanLastRow.page
992
+ );
993
+
994
+ if (startsOnCurrentPage && typeof cell._startingRowSpanY === 'number') {
995
+ discountY = ctxBeforeRowSpanLastRow.y - cell._startingRowSpanY;
996
+ }
997
+
998
+ // Do not increase Y by applying a negative discount.
999
+ discountY = Math.max(0, discountY);
988
1000
  }
989
1001
  let originalXOffset = 0;
990
1002
  // If context was saved from an unbreakable block and we are not in an unbreakable block anymore
@@ -1170,6 +1182,7 @@ class LayoutBuilder {
1170
1182
  tableNode.table.body[i].forEach(cell => {
1171
1183
  if (cell.rowSpan && cell.rowSpan > 1) {
1172
1184
  cell._startingRowSpanY = this.writer.context().y;
1185
+ cell._startingRowSpanPage = this.writer.context().page;
1173
1186
  }
1174
1187
  });
1175
1188
  }
@@ -1,4 +1,5 @@
1
1
  import PDFKit from 'pdfkit';
2
+ import { isString } from './helpers/variableType';
2
3
 
3
4
  const typeName = (bold, italics) => {
4
5
  let type = 'normal';
@@ -13,7 +14,7 @@ const typeName = (bold, italics) => {
13
14
  };
14
15
 
15
16
  class PDFDocument extends PDFKit {
16
- constructor(fonts = {}, images = {}, patterns = {}, attachments = {}, options = {}, virtualfs = null) {
17
+ constructor(fonts = {}, images = {}, patterns = {}, attachments = {}, options = {}, virtualfs = null, localAccessPolicy = undefined) {
17
18
  super(options);
18
19
 
19
20
  this.fonts = {};
@@ -39,10 +40,10 @@ class PDFDocument extends PDFKit {
39
40
  }
40
41
  }
41
42
 
42
-
43
43
  this.images = images;
44
44
  this.attachments = attachments;
45
45
  this.virtualfs = virtualfs;
46
+ this.localAccessPolicy = localAccessPolicy;
46
47
  }
47
48
 
48
49
  getFontType(bold, italics) {
@@ -74,6 +75,8 @@ class PDFDocument extends PDFKit {
74
75
 
75
76
  if (this.virtualfs && this.virtualfs.existsSync(def[0])) {
76
77
  def[0] = this.virtualfs.readFileSync(def[0]);
78
+ } else {
79
+ this.validateLocalFile(def[0]);
77
80
  }
78
81
 
79
82
  this.fontCache[familyName][type] = this.font(...def)._font;
@@ -108,8 +111,12 @@ class PDFDocument extends PDFKit {
108
111
 
109
112
  let image;
110
113
 
114
+ let imageSrc = realImageSrc(src);
115
+
116
+ this.validateLocalFile(imageSrc);
117
+
111
118
  try {
112
- image = this.openImage(realImageSrc(src));
119
+ image = this.openImage(imageSrc);
113
120
  if (!image) {
114
121
  throw new Error('No image');
115
122
  }
@@ -157,9 +164,23 @@ class PDFDocument extends PDFKit {
157
164
  return this.virtualfs.readFileSync(attachment.src);
158
165
  }
159
166
 
167
+ this.validateLocalFile(attachment.src);
168
+
160
169
  return attachment;
161
170
  }
162
171
 
172
+ resolveColor(color, defaultColor) {
173
+ color = color || defaultColor;
174
+
175
+ if (typeof this._normalizeColor === 'function') {
176
+ if (isString(color) && this._normalizeColor(color) === null) { // color is not valid
177
+ return defaultColor;
178
+ }
179
+ }
180
+
181
+ return color;
182
+ }
183
+
163
184
  setOpenActionAsPrint() {
164
185
  let printActionRef = this.ref({
165
186
  Type: 'Action',
@@ -169,6 +190,30 @@ class PDFDocument extends PDFKit {
169
190
  this._root.data.OpenAction = printActionRef;
170
191
  printActionRef.end();
171
192
  }
193
+
194
+ file(src, options = {}) {
195
+ this.validateLocalFile(src);
196
+
197
+ return super.file(src, options);
198
+ }
199
+
200
+ validateLocalFile(path) {
201
+ if (typeof this.localAccessPolicy === 'undefined') {
202
+ return;
203
+ }
204
+
205
+ if (!isString(path)) {
206
+ return;
207
+ }
208
+
209
+ if (/^data:/.test(path)) {
210
+ return;
211
+ }
212
+
213
+ if (this.localAccessPolicy(path) !== true) {
214
+ throw new Error(`Access to local file denied by resource access policy: ${path}`);
215
+ }
216
+ }
172
217
  }
173
218
 
174
219
  export default PDFDocument;
package/src/Printer.js CHANGED
@@ -13,11 +13,13 @@ class PdfPrinter {
13
13
  * @param {object} fontDescriptors font definition dictionary
14
14
  * @param {object} virtualfs
15
15
  * @param {object} urlResolver
16
+ * @param {(path: string) => boolean} localAccessPolicy
16
17
  */
17
- constructor(fontDescriptors, virtualfs, urlResolver) {
18
+ constructor(fontDescriptors, virtualfs, urlResolver, localAccessPolicy) {
18
19
  this.fontDescriptors = fontDescriptors;
19
20
  this.virtualfs = virtualfs;
20
21
  this.urlResolver = urlResolver;
22
+ this.localAccessPolicy = localAccessPolicy;
21
23
  }
22
24
 
23
25
  /**
@@ -69,7 +71,7 @@ class PdfPrinter {
69
71
  font: null
70
72
  };
71
73
 
72
- this.pdfKitDoc = new PDFDocument(this.fontDescriptors, docDefinition.images, docDefinition.patterns, docDefinition.attachments, pdfOptions, this.virtualfs);
74
+ this.pdfKitDoc = new PDFDocument(this.fontDescriptors, docDefinition.images, docDefinition.patterns, docDefinition.attachments, pdfOptions, this.virtualfs, this.localAccessPolicy);
73
75
  embedFiles(docDefinition, this.pdfKitDoc);
74
76
 
75
77
  const builder = new LayoutBuilder(pageSize, normalizePageMargin(docDefinition.pageMargins), new SVGMeasure());
package/src/Renderer.js CHANGED
@@ -188,7 +188,7 @@ class Renderer {
188
188
 
189
189
  let opacity = isNumber(inline.opacity) ? inline.opacity : 1;
190
190
  this.pdfDocument.opacity(opacity);
191
- this.pdfDocument.fill(inline.color || 'black');
191
+ this.pdfDocument.fill(this.pdfDocument.resolveColor(inline.color, 'black'));
192
192
 
193
193
  this.pdfDocument._font = inline.font;
194
194
  this.pdfDocument.fontSize(inline.fontSize);
@@ -287,14 +287,14 @@ class Renderer {
287
287
  let strokeOpacity = isNumber(vector.strokeOpacity) ? vector.strokeOpacity : 1;
288
288
 
289
289
  if (vector.color && vector.lineColor) {
290
- this.pdfDocument.fillColor(vector.color, fillOpacity);
291
- this.pdfDocument.strokeColor(vector.lineColor, strokeOpacity);
290
+ this.pdfDocument.fillColor(this.pdfDocument.resolveColor(vector.color, 'black'), fillOpacity);
291
+ this.pdfDocument.strokeColor(this.pdfDocument.resolveColor(vector.lineColor, 'black'), strokeOpacity);
292
292
  this.pdfDocument.fillAndStroke();
293
293
  } else if (vector.color) {
294
- this.pdfDocument.fillColor(vector.color, fillOpacity);
294
+ this.pdfDocument.fillColor(this.pdfDocument.resolveColor(vector.color, 'black'), fillOpacity);
295
295
  this.pdfDocument.fill();
296
296
  } else {
297
- this.pdfDocument.strokeColor(vector.lineColor || 'black', strokeOpacity);
297
+ this.pdfDocument.strokeColor(this.pdfDocument.resolveColor(vector.lineColor, 'black'), strokeOpacity);
298
298
  this.pdfDocument.stroke();
299
299
  }
300
300
  }
@@ -436,7 +436,7 @@ class Renderer {
436
436
  renderWatermark(page) {
437
437
  let watermark = page.watermark;
438
438
 
439
- this.pdfDocument.fill(watermark.color);
439
+ this.pdfDocument.fill(this.pdfDocument.resolveColor(watermark.color, 'black'));
440
440
  this.pdfDocument.opacity(watermark.opacity);
441
441
 
442
442
  this.pdfDocument.save();