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.
- package/CHANGELOG.md +26 -1
- package/README.md +1 -0
- package/build/pdfmake.js +612 -104
- package/build/pdfmake.js.map +1 -1
- package/build/pdfmake.min.js +2 -2
- package/build/pdfmake.min.js.map +1 -1
- package/js/DocMeasure.js +7 -2
- package/js/DocumentContext.js +228 -4
- package/js/LayoutBuilder.js +136 -8
- package/js/PDFDocument.js +3 -1
- package/js/PageElementWriter.js +109 -2
- package/js/Printer.js +1 -19
- package/js/Renderer.js +13 -0
- package/js/SVGMeasure.js +4 -2
- package/js/TableProcessor.js +1 -5
- package/js/URLResolver.js +14 -1
- package/js/base.js +19 -2
- package/js/browser-extensions/index.js +0 -2
- package/js/index.js +0 -2
- package/package.json +5 -5
- package/src/DocMeasure.js +8 -2
- package/src/DocumentContext.js +243 -9
- package/src/LayoutBuilder.js +145 -11
- package/src/PDFDocument.js +1 -1
- package/src/PageElementWriter.js +121 -2
- package/src/Printer.js +1 -20
- package/src/Renderer.js +13 -0
- package/src/SVGMeasure.js +2 -2
- package/src/TableProcessor.js +1 -5
- package/src/URLResolver.js +14 -1
- package/src/base.js +24 -2
- package/src/browser-extensions/index.js +0 -2
- package/src/index.js +0 -2
package/src/LayoutBuilder.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
package/src/PDFDocument.js
CHANGED
|
@@ -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);
|
package/src/PageElementWriter.js
CHANGED
|
@@ -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.
|
|
180
|
-
|
|
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
|
|
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 (
|
|
30
|
-
throw new Error('Invalid svg document (' +
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error('Invalid svg document (' + error + ')', { cause: error });
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
if (doc.name !== "svg") {
|
package/src/TableProcessor.js
CHANGED
|
@@ -20,7 +20,7 @@ class TableProcessor {
|
|
|
20
20
|
const prepareRowSpanData = () => {
|
|
21
21
|
let rsd = [];
|
|
22
22
|
let x = 0;
|
|
23
|
-
let lastWidth
|
|
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) {
|
package/src/URLResolver.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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) {
|