pdfmake 0.3.4 → 0.3.6

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.
@@ -420,7 +420,11 @@ class LayoutBuilder {
420
420
  if (availableHeight - margin[1] < 0) {
421
421
  // Consume the whole available space
422
422
  this.writer.context().moveDown(availableHeight);
423
- this.writer.moveToNextPage(node.pageOrientation);
423
+ if (this.writer.context().inSnakingColumns() && !this.writer.context().isInNestedNonSnakingGroup()) {
424
+ this.snakingAwarePageBreak(node.pageOrientation);
425
+ } else {
426
+ this.writer.moveToNextPage(node.pageOrientation);
427
+ }
424
428
  /**
425
429
  * TODO - Something to consider:
426
430
  * Right now the node starts at the top of next page (after header)
@@ -442,7 +446,11 @@ class LayoutBuilder {
442
446
  // Necessary for nodes inside tables
443
447
  if (availableHeight - margin[3] < 0) {
444
448
  this.writer.context().moveDown(availableHeight);
445
- this.writer.moveToNextPage(node.pageOrientation);
449
+ if (this.writer.context().inSnakingColumns() && !this.writer.context().isInNestedNonSnakingGroup()) {
450
+ this.snakingAwarePageBreak(node.pageOrientation);
451
+ } else {
452
+ this.writer.moveToNextPage(node.pageOrientation);
453
+ }
446
454
  /**
447
455
  * TODO - Something to consider:
448
456
  * Right now next node starts at the top of next page (after header)
@@ -553,6 +561,37 @@ class LayoutBuilder {
553
561
  }
554
562
  }
555
563
 
564
+ /**
565
+ * Helper for page breaks that respects snaking column context.
566
+ * When in snaking columns, first tries moving to next column.
567
+ * If no columns available, moves to next page and resets x to left margin.
568
+ * @param {string} pageOrientation - Optional page orientation for the new page
569
+ */
570
+ snakingAwarePageBreak(pageOrientation) {
571
+ let ctx = this.writer.context();
572
+ let snakingSnapshot = ctx.getSnakingSnapshot();
573
+ if (!snakingSnapshot) {
574
+ return;
575
+ }
576
+
577
+ // Try flowing to next column first
578
+ if (this.writer.canMoveToNextColumn()) {
579
+ this.writer.moveToNextColumn();
580
+ return;
581
+ }
582
+
583
+ // No more columns available, move to new page
584
+ this.writer.moveToNextPage(pageOrientation);
585
+
586
+ // Reset snaking column state for the new page
587
+ // Save lastColumnWidth before reset — if we're inside a nested
588
+ // column group (e.g. product/price row), the reset would overwrite
589
+ // it with the snaking column width, corrupting inner column layout.
590
+ let savedLastColumnWidth = ctx.lastColumnWidth;
591
+ ctx.resetSnakingColumnsForNewPage();
592
+ ctx.lastColumnWidth = savedLastColumnWidth;
593
+ }
594
+
556
595
  // vertical container
557
596
  processVerticalContainer(node) {
558
597
  node.stack.forEach(item => {
@@ -648,7 +687,8 @@ class LayoutBuilder {
648
687
  marginX: columnNode._margin ? [columnNode._margin[0], columnNode._margin[2]] : [0, 0],
649
688
  cells: columns,
650
689
  widths: columns,
651
- gaps
690
+ gaps,
691
+ snakingColumns: columnNode.snakingColumns
652
692
  });
653
693
  addAll(columnNode.positions, result.positions);
654
694
  this.nestedLevel--;
@@ -838,7 +878,7 @@ class LayoutBuilder {
838
878
  return null;
839
879
  }
840
880
 
841
- processRow({ marginX = [0, 0], dontBreakRows = false, rowsWithoutPageBreak = 0, cells, widths, gaps, tableNode, tableBody, rowIndex, height }) {
881
+ processRow({ marginX = [0, 0], dontBreakRows = false, rowsWithoutPageBreak = 0, cells, widths, gaps, tableNode, tableBody, rowIndex, height, snakingColumns = false }) {
842
882
  const isUnbreakableRow = dontBreakRows || rowIndex <= rowsWithoutPageBreak - 1;
843
883
  let pageBreaks = [];
844
884
  let pageBreaksByRowSpan = [];
@@ -855,8 +895,19 @@ class LayoutBuilder {
855
895
  // Use the marginX if we are in a top level table/column (not nested)
856
896
  const marginXParent = this.nestedLevel === 1 ? marginX : null;
857
897
  const _bottomByPage = tableNode ? tableNode._bottomByPage : null;
858
- this.writer.context().beginColumnGroup(marginXParent, _bottomByPage);
859
-
898
+ // Pass column gap and widths to context snapshot for snaking columns
899
+ // to advance correctly and reset to first-column width on new pages.
900
+ const columnGapForGroup = (gaps && gaps.length > 1) ? gaps[1] : 0;
901
+ const columnWidthsForContext = widths.map(w => w._calcWidth);
902
+ this.writer.context().beginColumnGroup(marginXParent, _bottomByPage,
903
+ snakingColumns, columnGapForGroup, columnWidthsForContext);
904
+
905
+ // IMPORTANT: We iterate ALL columns even when snakingColumns is enabled.
906
+ // This is intentional — beginColumn() must be called for each column to set up
907
+ // proper geometry (widths, offsets) and rowspan/colspan tracking. The
908
+ // completeColumnGroup() call at the end depends on this bookkeeping to compute
909
+ // heights correctly. Content processing is skipped for columns > 0 via
910
+ // skipForSnaking below, but the column structure must still be established.
860
911
  for (let i = 0, l = cells.length; i < l; i++) {
861
912
  let cell = cells[i];
862
913
  let cellIndexBegin = i;
@@ -913,7 +964,13 @@ class LayoutBuilder {
913
964
  // We pass the endingSpanCell reference to store the context just after processing rowspan cell
914
965
  this.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell);
915
966
 
916
- if (!cell._span) {
967
+ // When snaking, only process content from the first column (i === 0).
968
+ // Content overflows into subsequent columns via moveToNextColumn().
969
+ // We skip content processing here but NOT the beginColumn() call above —
970
+ // the column geometry setup is still needed for proper layout bookkeeping.
971
+ const skipForSnaking = snakingColumns && i > 0;
972
+
973
+ if (!cell._span && !skipForSnaking) {
917
974
  this.processNode(cell, true);
918
975
  this.writer.context().updateBottomByPage();
919
976
 
@@ -968,7 +1025,11 @@ class LayoutBuilder {
968
1025
  // If content did not break page, check if we should break by height
969
1026
  if (willBreakByHeight && !isUnbreakableRow && pageBreaks.length === 0) {
970
1027
  this.writer.context().moveDown(this.writer.context().availableHeight);
971
- this.writer.moveToNextPage();
1028
+ if (snakingColumns) {
1029
+ this.snakingAwarePageBreak();
1030
+ } else {
1031
+ this.writer.moveToNextPage();
1032
+ }
972
1033
  }
973
1034
 
974
1035
  const bottomByPage = this.writer.context().completeColumnGroup(height, endingSpanCell);
@@ -989,7 +1050,7 @@ class LayoutBuilder {
989
1050
  itemBegin.cell = cell;
990
1051
  itemBegin.bottomY = this.writer.context().y;
991
1052
  itemBegin.isCellContentMultiPage = !itemBegin.cell.positions.every(item => item.pageNumber === itemBegin.cell.positions[0].pageNumber);
992
- itemBegin.getViewHeight = function() {
1053
+ itemBegin.getViewHeight = function () {
993
1054
  if (this.cell._willBreak) {
994
1055
  return this.cell._bottomY - this.cell._rowTopPageY;
995
1056
  }
@@ -1009,7 +1070,7 @@ class LayoutBuilder {
1009
1070
 
1010
1071
  return this.viewHeight;
1011
1072
  };
1012
- itemBegin.getNodeHeight = function() {
1073
+ itemBegin.getNodeHeight = function () {
1013
1074
  return this.nodeHeight;
1014
1075
  };
1015
1076
 
@@ -1077,7 +1138,32 @@ class LayoutBuilder {
1077
1138
  processor.beginTable(this.writer);
1078
1139
 
1079
1140
  let rowHeights = tableNode.table.heights;
1141
+ let lastRowHeight = 0;
1142
+
1080
1143
  for (let i = 0, l = tableNode.table.body.length; i < l; i++) {
1144
+ // Between table rows: check if we should move to the next snaking column.
1145
+ // This must happen AFTER the previous row's endRow (borders drawn) and
1146
+ // BEFORE this row's beginRow. At this point, the table row column group
1147
+ // has been completed, so canMoveToNextColumn() works correctly.
1148
+ if (i > 0 && this.writer.context().inSnakingColumns()) {
1149
+ // Estimate minimum space for next row: use last row's height as heuristic,
1150
+ // or fall back to a minimum of padding + line height + border
1151
+ let minRowHeight = lastRowHeight > 0 ? lastRowHeight : (
1152
+ processor.rowPaddingTop + 14 + processor.rowPaddingBottom +
1153
+ processor.bottomLineWidth + processor.topLineWidth
1154
+ );
1155
+ if (this.writer.context().availableHeight < minRowHeight) {
1156
+ this.snakingAwarePageBreak();
1157
+
1158
+ // Skip border when headerRows present (header repeat includes it)
1159
+ if (processor.layout.hLineWhenBroken !== false && !processor.headerRows) {
1160
+ processor.drawHorizontalLine(i, this.writer);
1161
+ }
1162
+ }
1163
+ }
1164
+
1165
+ let rowYBefore = this.writer.context().y;
1166
+
1081
1167
  // if dontBreakRows and row starts a rowspan
1082
1168
  // we store the 'y' of the beginning of each rowSpan
1083
1169
  if (processor.dontBreakRows) {
@@ -1130,6 +1216,12 @@ class LayoutBuilder {
1130
1216
  }
1131
1217
 
1132
1218
  processor.endRow(i, this.writer, result.pageBreaks);
1219
+
1220
+ // Track the height of the completed row for the next row's estimate
1221
+ let rowYAfter = this.writer.context().y;
1222
+ if (this.writer.context().page === pageBeforeProcessing) {
1223
+ lastRowHeight = rowYAfter - rowYBefore;
1224
+ }
1133
1225
  }
1134
1226
 
1135
1227
  processor.endTable(this.writer);
@@ -1155,6 +1247,27 @@ class LayoutBuilder {
1155
1247
  }
1156
1248
  }
1157
1249
 
1250
+ if (node.outline) {
1251
+ line._outline = {
1252
+ id: node.id,
1253
+ parentId: node.outlineParentId,
1254
+ text: node.outlineText || node.text,
1255
+ expanded: node.outlineExpanded || false
1256
+ };
1257
+ } else if (Array.isArray(node.text)) {
1258
+ for (let i = 0, l = node.text.length; i < l; i++) {
1259
+ let item = node.text[i];
1260
+ if (item.outline) {
1261
+ line._outline = {
1262
+ id: item.id,
1263
+ parentId: item.outlineParentId,
1264
+ text: item.outlineText || item.text,
1265
+ expanded: item.outlineExpanded || false
1266
+ };
1267
+ }
1268
+ }
1269
+ }
1270
+
1158
1271
  if (node._tocItemRef) {
1159
1272
  line._pageNodeRef = node._tocItemRef;
1160
1273
  }
@@ -1176,6 +1289,28 @@ class LayoutBuilder {
1176
1289
  }
1177
1290
 
1178
1291
  while (line && (maxHeight === -1 || currentHeight < maxHeight)) {
1292
+ // Check if line fits vertically in current context
1293
+ if (line.getHeight() > this.writer.context().availableHeight && this.writer.context().y > this.writer.context().pageMargins.top) {
1294
+ // Line doesn't fit, forced move to next page/column
1295
+ // Only do snaking-specific break if we're in snaking columns AND NOT inside
1296
+ // a nested non-snaking group (like a table row). Table cells should use
1297
+ // standard page breaks — column breaks happen between table rows instead.
1298
+ if (this.writer.context().inSnakingColumns() && !this.writer.context().isInNestedNonSnakingGroup()) {
1299
+ this.snakingAwarePageBreak(node.pageOrientation);
1300
+
1301
+ // Always reflow text after a snaking break (column or page).
1302
+ // This ensures text adapts to the new column width, whether it's narrower or wider.
1303
+ if (line.inlines && line.inlines.length > 0) {
1304
+ node._inlines.unshift(...line.inlines);
1305
+ }
1306
+ // Rebuild line with new width
1307
+ line = this.buildNextLine(node);
1308
+ continue;
1309
+ } else {
1310
+ this.writer.moveToNextPage(node.pageOrientation);
1311
+ }
1312
+ }
1313
+
1179
1314
  let positions = this.writer.addLine(line);
1180
1315
  node.positions.push(positions);
1181
1316
  line = this.buildNextLine(node);
@@ -1241,7 +1376,6 @@ class LayoutBuilder {
1241
1376
  (line.hasEnoughSpaceForInline(textNode._inlines[0], textNode._inlines.slice(1)) || isForceContinue)) {
1242
1377
  let isHardWrap = false;
1243
1378
  let inline = textNode._inlines.shift();
1244
- isForceContinue = false;
1245
1379
 
1246
1380
  if (!inline.noWrap && inline.text.length > 1 && inline.width > line.getAvailableWidth()) {
1247
1381
  let maxChars = findMaxFitLength(inline.text, line.getAvailableWidth(), (txt) =>
@@ -114,7 +114,7 @@ class PDFDocument extends PDFKit {
114
114
  throw new Error('No image');
115
115
  }
116
116
  } catch (error) {
117
- throw new Error(`Invalid image: ${error.toString()}\nImages dictionary should contain dataURL entries (or local file paths in node.js)`);
117
+ throw new Error(`Invalid image: ${error.toString()}\nImages dictionary should contain dataURL entries (or local file paths in node.js)`, { cause: error });
118
118
  }
119
119
 
120
120
  image.embed(this);
@@ -173,11 +173,130 @@ class PageElementWriter extends ElementWriter {
173
173
  this.repeatables.pop();
174
174
  }
175
175
 
176
+ /**
177
+ * Move to the next column in a column group (snaking columns).
178
+ * Handles repeatables and emits columnChanged event.
179
+ */
180
+ moveToNextColumn() {
181
+ let nextColumn = this.context().moveToNextColumn();
182
+
183
+ // Handle repeatables (like table headers) for the new column
184
+ this.repeatables.forEach(function (rep) {
185
+ // In snaking columns, we WANT headers to repeat.
186
+ // However, in Standard Page Breaks, headers are drawn using useBlockXOffset=true (original absolute X).
187
+ // This works for page breaks because margins are consistent.
188
+ // In Snaking Columns, the X position changes for each column.
189
+ // If we use true, the header is drawn at the *original* X position (Col 1), overlapping/invisible.
190
+ // We MUST use false to force drawing relative to the CURRENT context X (new column start).
191
+ this.addFragment(rep, false);
192
+ }, this);
193
+
194
+ this.emit('columnChanged', {
195
+ prevY: nextColumn.prevY,
196
+ y: this.context().y
197
+ });
198
+ }
199
+
200
+ /**
201
+ * Check if currently in a column group that can move to next column.
202
+ * Only returns true if snakingColumns is enabled for the column group.
203
+ * @returns {boolean}
204
+ */
205
+ canMoveToNextColumn() {
206
+ let ctx = this.context();
207
+ let snakingSnapshot = ctx.getSnakingSnapshot();
208
+
209
+ if (snakingSnapshot) {
210
+ // Check if we're inside a nested (non-snaking) column group.
211
+ // If so, don't allow a snaking column move — it would corrupt
212
+ // the inner row's layout (e.g. product name in col 1, price in col 2).
213
+ // The inner row should complete via normal page break instead.
214
+ for (let i = ctx.snapshots.length - 1; i >= 0; i--) {
215
+ let snap = ctx.snapshots[i];
216
+ if (snap.snakingColumns) {
217
+ break; // Reached the snaking snapshot, no inner groups found
218
+ }
219
+ if (!snap.overflowed) {
220
+ return false; // Found a non-snaking, non-overflowed inner group
221
+ }
222
+ }
223
+
224
+ let overflowCount = 0;
225
+ for (let i = ctx.snapshots.length - 1; i >= 0; i--) {
226
+ if (ctx.snapshots[i].overflowed) {
227
+ overflowCount++;
228
+ } else {
229
+ break;
230
+ }
231
+ }
232
+
233
+ if (snakingSnapshot.columnWidths &&
234
+ overflowCount >= snakingSnapshot.columnWidths.length - 1) {
235
+ return false;
236
+ }
237
+
238
+ let currentColumnWidth = ctx.availableWidth || ctx.lastColumnWidth || 0;
239
+ let nextColumnWidth = snakingSnapshot.columnWidths ?
240
+ snakingSnapshot.columnWidths[overflowCount + 1] : currentColumnWidth;
241
+ let nextX = ctx.x + currentColumnWidth + (snakingSnapshot.gap || 0);
242
+ let page = ctx.getCurrentPage();
243
+ let pageWidth = page.pageSize.width;
244
+ let rightMargin = page.pageMargins ? page.pageMargins.right : 0;
245
+ let parentRightMargin = ctx.marginXTopParent ? ctx.marginXTopParent[1] : 0;
246
+ let rightBoundary = pageWidth - rightMargin - parentRightMargin;
247
+
248
+ return (nextX + nextColumnWidth) <= (rightBoundary + 1);
249
+ }
250
+ return false;
251
+ }
252
+
176
253
  _fitOnPage(addFct) {
177
254
  let position = addFct();
178
255
  if (!position) {
179
- this.moveToNextPage();
180
- position = addFct();
256
+ if (this.canMoveToNextColumn()) {
257
+ this.moveToNextColumn();
258
+ position = addFct();
259
+ }
260
+
261
+ if (!position) {
262
+ let ctx = this.context();
263
+ let snakingSnapshot = ctx.getSnakingSnapshot();
264
+
265
+ if (snakingSnapshot) {
266
+ if (ctx.isInNestedNonSnakingGroup()) {
267
+ // Inside a table cell within snaking columns — use standard page break.
268
+ // Don't reset snaking state; the table handles its own breaks.
269
+ // Column breaks happen between rows in processTable instead.
270
+ this.moveToNextPage();
271
+ } else {
272
+ this.moveToNextPage();
273
+
274
+ // Save lastColumnWidth before reset — if we're inside a nested
275
+ // column group (e.g. product/price row), the reset would overwrite
276
+ // it with the snaking column width, corrupting inner column layout.
277
+ let savedLastColumnWidth = ctx.lastColumnWidth;
278
+ ctx.resetSnakingColumnsForNewPage();
279
+ ctx.lastColumnWidth = savedLastColumnWidth;
280
+ }
281
+
282
+ position = addFct();
283
+ } else {
284
+ while (ctx.snapshots.length > 0 && ctx.snapshots[ctx.snapshots.length - 1].overflowed) {
285
+ let popped = ctx.snapshots.pop();
286
+ let prevSnapshot = ctx.snapshots[ctx.snapshots.length - 1];
287
+ if (prevSnapshot) {
288
+ ctx.x = prevSnapshot.x;
289
+ ctx.y = prevSnapshot.y;
290
+ ctx.availableHeight = prevSnapshot.availableHeight;
291
+ ctx.availableWidth = popped.availableWidth;
292
+ ctx.lastColumnWidth = prevSnapshot.lastColumnWidth;
293
+ }
294
+ }
295
+
296
+ this.moveToNextPage();
297
+ position = addFct();
298
+ }
299
+ }
181
300
  }
182
301
  return position;
183
302
  }
package/src/Printer.js CHANGED
@@ -7,21 +7,6 @@ import Renderer from './Renderer';
7
7
  import { isNumber, isValue } from './helpers/variableType';
8
8
  import { convertToDynamicContent } from './helpers/tools';
9
9
 
10
- /**
11
- * Printer which turns document definition into a pdf
12
- *
13
- * @example
14
- * var fontDescriptors = {
15
- * Roboto: {
16
- * normal: 'fonts/Roboto-Regular.ttf',
17
- * bold: 'fonts/Roboto-Medium.ttf',
18
- * italics: 'fonts/Roboto-Italic.ttf',
19
- * bolditalics: 'fonts/Roboto-MediumItalic.ttf'
20
- * }
21
- * };
22
- *
23
- * var printer = new PdfPrinter(fontDescriptors);
24
- */
25
10
  class PdfPrinter {
26
11
 
27
12
  /**
@@ -29,7 +14,7 @@ class PdfPrinter {
29
14
  * @param {object} virtualfs
30
15
  * @param {object} urlResolver
31
16
  */
32
- constructor(fontDescriptors, virtualfs = null, urlResolver = null) {
17
+ constructor(fontDescriptors, virtualfs, urlResolver) {
33
18
  this.fontDescriptors = fontDescriptors;
34
19
  this.virtualfs = virtualfs;
35
20
  this.urlResolver = urlResolver;
@@ -127,10 +112,6 @@ class PdfPrinter {
127
112
  return { url: url, headers: {} };
128
113
  };
129
114
 
130
- if (this.urlResolver === null) {
131
- return;
132
- }
133
-
134
115
  for (let font in this.fontDescriptors) {
135
116
  if (this.fontDescriptors.hasOwnProperty(font)) {
136
117
  if (this.fontDescriptors[font].normal) {
package/src/Renderer.js CHANGED
@@ -41,6 +41,7 @@ class Renderer {
41
41
  constructor(pdfDocument, progressCallback) {
42
42
  this.pdfDocument = pdfDocument;
43
43
  this.progressCallback = progressCallback;
44
+ this.outlineMap = [];
44
45
  }
45
46
 
46
47
  renderPages(pages) {
@@ -128,6 +129,18 @@ class Renderer {
128
129
  }
129
130
  }
130
131
 
132
+ if (line._outline) {
133
+ let parentOutline = this.pdfDocument.outline;
134
+ if (line._outline.parentId && this.outlineMap[line._outline.parentId]) {
135
+ parentOutline = this.outlineMap[line._outline.parentId];
136
+ }
137
+
138
+ let outline = parentOutline.addItem(line._outline.text, { expanded: line._outline.expanded });
139
+ if (line._outline.id) {
140
+ this.outlineMap[line._outline.id] = outline;
141
+ }
142
+ }
143
+
131
144
  if (line._pageNodeRef) {
132
145
  preparePageNodeRefLine(line._pageNodeRef, line.inlines[0]);
133
146
  }
package/src/SVGMeasure.js CHANGED
@@ -26,8 +26,8 @@ const parseSVG = (svgString) => {
26
26
 
27
27
  try {
28
28
  doc = new XmlDocument(svgString);
29
- } catch (err) {
30
- throw new Error('Invalid svg document (' + err + ')');
29
+ } catch (error) {
30
+ throw new Error('Invalid svg document (' + error + ')', { cause: error });
31
31
  }
32
32
 
33
33
  if (doc.name !== "svg") {
@@ -20,7 +20,7 @@ class TableProcessor {
20
20
  const prepareRowSpanData = () => {
21
21
  let rsd = [];
22
22
  let x = 0;
23
- let lastWidth = 0;
23
+ let lastWidth;
24
24
 
25
25
  rsd.push({ left: 0, rowSpan: 0 });
26
26
 
@@ -274,7 +274,6 @@ class TableProcessor {
274
274
  lineColor: borderColor
275
275
  }, false, isNumber(overrideY), null, forcePage);
276
276
  currentLine = null;
277
- borderColor = null;
278
277
  cellAbove = null;
279
278
  currentCell = null;
280
279
  rowCellAbove = null;
@@ -356,9 +355,6 @@ class TableProcessor {
356
355
  dash: dash,
357
356
  lineColor: borderColor
358
357
  }, false, true);
359
- cellBefore = null;
360
- currentCell = null;
361
- borderColor = null;
362
358
  }
363
359
 
364
360
  endTable(writer) {
@@ -6,7 +6,7 @@ async function fetchUrl(url, headers = {}) {
6
6
  }
7
7
  return await response.arrayBuffer();
8
8
  } catch (error) {
9
- throw new Error(`Network request failed (url: "${url}", error: ${error.message})`);
9
+ throw new Error(`Network request failed (url: "${url}", error: ${error.message})`, { cause: error });
10
10
  }
11
11
  }
12
12
 
@@ -14,6 +14,14 @@ class URLResolver {
14
14
  constructor(fs) {
15
15
  this.fs = fs;
16
16
  this.resolving = {};
17
+ this.urlAccessPolicy = undefined;
18
+ }
19
+
20
+ /**
21
+ * @param {(url: string) => boolean} callback
22
+ */
23
+ setUrlAccessPolicy(callback) {
24
+ this.urlAccessPolicy = callback;
17
25
  }
18
26
 
19
27
  resolve(url, headers = {}) {
@@ -22,6 +30,11 @@ class URLResolver {
22
30
  if (this.fs.existsSync(url)) {
23
31
  return; // url was downloaded earlier
24
32
  }
33
+
34
+ if ((typeof this.urlAccessPolicy !== 'undefined') && (this.urlAccessPolicy(url) !== true)) {
35
+ throw new Error(`Access to URL denied by resource access policy: ${url}`);
36
+ }
37
+
25
38
  const buffer = await fetchUrl(url, headers);
26
39
  this.fs.writeFileSync(url, buffer);
27
40
  }
package/src/base.js CHANGED
@@ -2,12 +2,13 @@ import Printer from './Printer';
2
2
  import virtualfs from './virtual-fs';
3
3
  import { pack } from './helpers/tools';
4
4
  import { isObject } from './helpers/variableType';
5
+ import URLResolver from './URLResolver';
5
6
 
6
7
  class pdfmake {
7
8
 
8
9
  constructor() {
9
10
  this.virtualfs = virtualfs;
10
- this.urlResolver = null;
11
+ this.urlAccessPolicy = undefined;
11
12
  }
12
13
 
13
14
  /**
@@ -27,12 +28,33 @@ class pdfmake {
27
28
  options.progressCallback = this.progressCallback;
28
29
  options.tableLayouts = this.tableLayouts;
29
30
 
30
- let printer = new Printer(this.fonts, this.virtualfs, this.urlResolver());
31
+ const isServer = typeof process !== 'undefined' && process?.versions?.node;
32
+ if (typeof this.urlAccessPolicy === 'undefined' && isServer) {
33
+ console.warn(
34
+ 'No URL access policy defined. Consider using setUrlAccessPolicy() to restrict external resource downloads.'
35
+ );
36
+ }
37
+
38
+ let urlResolver = new URLResolver(this.virtualfs);
39
+ urlResolver.setUrlAccessPolicy(this.urlAccessPolicy);
40
+
41
+ let printer = new Printer(this.fonts, this.virtualfs, urlResolver);
31
42
  const pdfDocumentPromise = printer.createPdfKitDocument(docDefinition, options);
32
43
 
33
44
  return this._transformToDocument(pdfDocumentPromise);
34
45
  }
35
46
 
47
+ /**
48
+ * @param {(url: string) => boolean} callback
49
+ */
50
+ setUrlAccessPolicy(callback) {
51
+ if (callback !== undefined && typeof callback !== 'function') {
52
+ throw new Error("Parameter 'callback' has an invalid type. Function or undefined expected.");
53
+ }
54
+
55
+ this.urlAccessPolicy = callback;
56
+ }
57
+
36
58
  setProgressCallback(callback) {
37
59
  this.progressCallback = callback;
38
60
  }
@@ -1,6 +1,5 @@
1
1
  import pdfmakeBase from '../base';
2
2
  import OutputDocumentBrowser from './OutputDocumentBrowser';
3
- import URLResolver from '../URLResolver';
4
3
  import fs from 'fs';
5
4
  import configurator from 'core-js/configurator';
6
5
 
@@ -21,7 +20,6 @@ let defaultClientFonts = {
21
20
  class pdfmake extends pdfmakeBase {
22
21
  constructor() {
23
22
  super();
24
- this.urlResolver = () => new URLResolver(this.virtualfs);
25
23
  this.fonts = defaultClientFonts;
26
24
  }
27
25
 
package/src/index.js CHANGED
@@ -1,11 +1,9 @@
1
1
  const pdfmakeBase = require('./base').default;
2
2
  const OutputDocumentServer = require('./OutputDocumentServer').default;
3
- const URLResolver = require('./URLResolver').default;
4
3
 
5
4
  class pdfmake extends pdfmakeBase {
6
5
  constructor() {
7
6
  super();
8
- this.urlResolver = () => new URLResolver(this.virtualfs);
9
7
  }
10
8
 
11
9
  _transformToDocument(doc) {