@flowaccount/pdfmake 1.0.0 → 1.0.2
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/build/pdfmake.js +32800 -31746
- package/build/pdfmake.min.js +2 -2
- package/build/pdfmake.min.js.map +1 -1
- package/package.json +2 -2
- package/src/documentContext.js +2 -2
- package/src/layoutBuilder.js +264 -5
- package/src/printer.js +113 -8
- package/src/tableProcessor.js +133 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowaccount/pdfmake",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Client/server side PDF printing in pure JavaScript",
|
|
5
5
|
"main": "src/printer.js",
|
|
6
6
|
"browser": "build/pdfmake.js",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"format:check": "prettier --check .",
|
|
69
69
|
"publish:local": "cp .npmrc.local .npmrc && npm version 0.2.20-local.$(date +%s) --no-git-tag-version && npm publish --registry=http://localhost:4872",
|
|
70
70
|
"publish:staging": "npm run build && npm run test && npm version prerelease --preid=staging --no-git-tag-version && npm publish --tag staging --access public",
|
|
71
|
-
"publish:production": "npm run build && npm run test && npm publish
|
|
71
|
+
"publish:production": "npm run build && npm run test && npm publish",
|
|
72
72
|
"version:patch": "npm version patch",
|
|
73
73
|
"version:minor": "npm version minor",
|
|
74
74
|
"version:major": "npm version major"
|
package/src/documentContext.js
CHANGED
|
@@ -75,7 +75,7 @@ DocumentContext.prototype.resetMarginXTopParent = function () {
|
|
|
75
75
|
this.marginXTopParent = null;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
DocumentContext.prototype.beginColumn = function (width, offset, endingCell) {
|
|
78
|
+
DocumentContext.prototype.beginColumn = function (width, offset, endingCell, heightOffset) {
|
|
79
79
|
var saved = this.snapshots[this.snapshots.length - 1];
|
|
80
80
|
|
|
81
81
|
this.calculateBottomMost(saved, endingCell);
|
|
@@ -84,7 +84,7 @@ DocumentContext.prototype.beginColumn = function (width, offset, endingCell) {
|
|
|
84
84
|
this.x = this.x + this.lastColumnWidth + (offset || 0);
|
|
85
85
|
this.y = saved.y;
|
|
86
86
|
this.availableWidth = width; //saved.availableWidth - offset;
|
|
87
|
-
this.availableHeight = saved.availableHeight;
|
|
87
|
+
this.availableHeight = saved.availableHeight - (heightOffset || 0);
|
|
88
88
|
|
|
89
89
|
this.lastColumnWidth = width;
|
|
90
90
|
};
|
package/src/layoutBuilder.js
CHANGED
|
@@ -231,12 +231,14 @@ LayoutBuilder.prototype.applyFooterGapOption = function(opt) {
|
|
|
231
231
|
if (!opt) return;
|
|
232
232
|
|
|
233
233
|
if (typeof opt !== 'object') {
|
|
234
|
-
this._footerGapOption = { enabled: true };
|
|
234
|
+
this._footerGapOption = { enabled: true, forcePageBreakForAllRows: false };
|
|
235
235
|
return;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
this._footerGapOption = {
|
|
239
239
|
enabled: opt.enabled !== false,
|
|
240
|
+
minRowHeight: typeof opt.minRowHeight === 'number' ? opt.minRowHeight : undefined, // Optional fallback - system auto-calculates from cell content if not provided
|
|
241
|
+
forcePageBreakForAllRows: opt.forcePageBreakForAllRows === true, // Force page break for all rows (not just inline images)
|
|
240
242
|
columns: opt.columns ? {
|
|
241
243
|
widths: Array.isArray(opt.columns.widths) ? opt.columns.widths.slice() : undefined,
|
|
242
244
|
widthLength: opt.columns.widths.length || 0,
|
|
@@ -818,6 +820,154 @@ LayoutBuilder.prototype._colLeftOffset = function (i, gaps) {
|
|
|
818
820
|
return 0;
|
|
819
821
|
};
|
|
820
822
|
|
|
823
|
+
/**
|
|
824
|
+
* Checks if a cell or node contains an inline image.
|
|
825
|
+
*
|
|
826
|
+
* @param {object} node - The node to check for inline images.
|
|
827
|
+
* @returns {boolean} True if the node contains an inline image; otherwise, false.
|
|
828
|
+
*/
|
|
829
|
+
LayoutBuilder.prototype._containsInlineImage = function (node) {
|
|
830
|
+
if (!node) {
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Direct image node
|
|
835
|
+
if (node.image) {
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
// Check in table
|
|
841
|
+
if (node.table && isArray(node.table.body)) {
|
|
842
|
+
for (var r = 0; r < node.table.body.length; r++) {
|
|
843
|
+
if (isArray(node.table.body[r])) {
|
|
844
|
+
for (var c = 0; c < node.table.body[r].length; c++) {
|
|
845
|
+
if (this._containsInlineImage(node.table.body[r][c])) {
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return false;
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Gets the maximum image height from cells in a row.
|
|
859
|
+
*
|
|
860
|
+
* @param {Array<object>} cells - Array of cell objects in a row.
|
|
861
|
+
* @returns {number} The maximum image height found in the cells.
|
|
862
|
+
*/
|
|
863
|
+
LayoutBuilder.prototype._getMaxImageHeight = function (cells) {
|
|
864
|
+
var maxHeight = 0;
|
|
865
|
+
|
|
866
|
+
for (var i = 0; i < cells.length; i++) {
|
|
867
|
+
var cellHeight = this._getImageHeightFromNode(cells[i]);
|
|
868
|
+
if (cellHeight > maxHeight) {
|
|
869
|
+
maxHeight = cellHeight;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return maxHeight;
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Gets the maximum estimated height from cells in a row.
|
|
878
|
+
* Checks for measured heights (_height property) and content-based heights.
|
|
879
|
+
*
|
|
880
|
+
* @param {Array<object>} cells - Array of cell objects in a row.
|
|
881
|
+
* @returns {number} The maximum estimated height found in the cells, or 0 if cannot estimate.
|
|
882
|
+
*/
|
|
883
|
+
LayoutBuilder.prototype._getMaxCellHeight = function (cells) {
|
|
884
|
+
var maxHeight = 0;
|
|
885
|
+
|
|
886
|
+
for (var i = 0; i < cells.length; i++) {
|
|
887
|
+
var cell = cells[i];
|
|
888
|
+
if (!cell || cell._span) {
|
|
889
|
+
continue; // Skip null cells and span placeholders
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
var cellHeight = 0;
|
|
893
|
+
|
|
894
|
+
// Check if cell has measured height from docMeasure phase
|
|
895
|
+
if (cell._height) {
|
|
896
|
+
cellHeight = cell._height;
|
|
897
|
+
}
|
|
898
|
+
// Check for image content
|
|
899
|
+
else if (cell.image && cell._maxHeight) {
|
|
900
|
+
cellHeight = cell._maxHeight;
|
|
901
|
+
}
|
|
902
|
+
// Check for nested content with height
|
|
903
|
+
else {
|
|
904
|
+
cellHeight = this._getImageHeightFromNode(cell);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (cellHeight > maxHeight) {
|
|
908
|
+
maxHeight = cellHeight;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return maxHeight;
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Recursively gets image height from a node.
|
|
917
|
+
*
|
|
918
|
+
* @param {object} node - The node to extract image height from.
|
|
919
|
+
* @returns {number} The image height if found; otherwise, 0.
|
|
920
|
+
*/
|
|
921
|
+
LayoutBuilder.prototype._getImageHeightFromNode = function (node) {
|
|
922
|
+
if (!node) {
|
|
923
|
+
return 0;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Direct image node with height
|
|
927
|
+
if (node.image && node._height) {
|
|
928
|
+
return node._height;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
var maxHeight = 0;
|
|
932
|
+
|
|
933
|
+
// Check in stack
|
|
934
|
+
if (isArray(node.stack)) {
|
|
935
|
+
for (var i = 0; i < node.stack.length; i++) {
|
|
936
|
+
var h = this._getImageHeightFromNode(node.stack[i]);
|
|
937
|
+
if (h > maxHeight) {
|
|
938
|
+
maxHeight = h;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Check in columns
|
|
944
|
+
if (isArray(node.columns)) {
|
|
945
|
+
for (var j = 0; j < node.columns.length; j++) {
|
|
946
|
+
var h2 = this._getImageHeightFromNode(node.columns[j]);
|
|
947
|
+
if (h2 > maxHeight) {
|
|
948
|
+
maxHeight = h2;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Check in table
|
|
954
|
+
if (node.table && isArray(node.table.body)) {
|
|
955
|
+
for (var r = 0; r < node.table.body.length; r++) {
|
|
956
|
+
if (isArray(node.table.body[r])) {
|
|
957
|
+
for (var c = 0; c < node.table.body[r].length; c++) {
|
|
958
|
+
var h3 = this._getImageHeightFromNode(node.table.body[r][c]);
|
|
959
|
+
if (h3 > maxHeight) {
|
|
960
|
+
maxHeight = h3;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
return maxHeight;
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
|
|
821
971
|
/**
|
|
822
972
|
* Retrieves the ending cell for a row span in case it exists in a specified table column.
|
|
823
973
|
*
|
|
@@ -840,7 +990,7 @@ LayoutBuilder.prototype._getRowSpanEndingCell = function (tableBody, rowIndex, c
|
|
|
840
990
|
return null;
|
|
841
991
|
};
|
|
842
992
|
|
|
843
|
-
LayoutBuilder.prototype.processRow = function ({ marginX = [0, 0], dontBreakRows = false, rowsWithoutPageBreak = 0, cells, widths, gaps, tableNode, tableBody, rowIndex, height }) {
|
|
993
|
+
LayoutBuilder.prototype.processRow = function ({ marginX = [0, 0], dontBreakRows = false, rowsWithoutPageBreak = 0, cells, widths, gaps, tableNode, tableBody, rowIndex, height, heightOffset = 0 }) {
|
|
844
994
|
var self = this;
|
|
845
995
|
var isUnbreakableRow = dontBreakRows || rowIndex <= rowsWithoutPageBreak - 1;
|
|
846
996
|
var pageBreaks = [];
|
|
@@ -848,8 +998,108 @@ LayoutBuilder.prototype.processRow = function ({ marginX = [0, 0], dontBreakRows
|
|
|
848
998
|
var positions = [];
|
|
849
999
|
var willBreakByHeight = false;
|
|
850
1000
|
var columnAlignIndexes = {};
|
|
1001
|
+
var hasInlineImage = false;
|
|
851
1002
|
widths = widths || cells;
|
|
852
1003
|
|
|
1004
|
+
// Check if row contains inline images
|
|
1005
|
+
if (!isUnbreakableRow) {
|
|
1006
|
+
for (var cellIdx = 0; cellIdx < cells.length; cellIdx++) {
|
|
1007
|
+
if (self._containsInlineImage(cells[cellIdx])) {
|
|
1008
|
+
hasInlineImage = true;
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Check if row would cause page break and force move to next page first
|
|
1015
|
+
// This keeps the entire row together on the new page
|
|
1016
|
+
// Apply when: forcePageBreakForAllRows is enabled OR row has inline images
|
|
1017
|
+
|
|
1018
|
+
// Priority for forcePageBreakForAllRows setting:
|
|
1019
|
+
// 1. Table-specific layout.forcePageBreakForAllRows
|
|
1020
|
+
// 2. Tables with footerGapCollect: 'product-items' (auto-enabled)
|
|
1021
|
+
// 3. Global footerGapOption.forcePageBreakForAllRows
|
|
1022
|
+
var tableLayout = tableNode && tableNode._layout;
|
|
1023
|
+
var footerGapOpt = self.writer.context()._footerGapOption;
|
|
1024
|
+
var shouldForcePageBreak = false;
|
|
1025
|
+
|
|
1026
|
+
if (tableLayout && tableLayout.forcePageBreakForAllRows !== undefined) {
|
|
1027
|
+
// Table-specific setting takes precedence
|
|
1028
|
+
shouldForcePageBreak = tableLayout.forcePageBreakForAllRows === true;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (!isUnbreakableRow && (shouldForcePageBreak || hasInlineImage)) {
|
|
1032
|
+
var availableHeight = self.writer.context().availableHeight;
|
|
1033
|
+
|
|
1034
|
+
// Calculate estimated height from actual cell content
|
|
1035
|
+
var estimatedHeight = height; // Use provided height if available
|
|
1036
|
+
|
|
1037
|
+
if (!estimatedHeight) {
|
|
1038
|
+
// Try to get maximum cell height from measured content
|
|
1039
|
+
var maxCellHeight = self._getMaxCellHeight(cells);
|
|
1040
|
+
|
|
1041
|
+
if (maxCellHeight > 0) {
|
|
1042
|
+
// Add padding for table borders and cell padding (approximate)
|
|
1043
|
+
// Using smaller padding to avoid overly conservative page break detection
|
|
1044
|
+
var tablePadding = 10; // Account for row padding and borders
|
|
1045
|
+
estimatedHeight = maxCellHeight + tablePadding;
|
|
1046
|
+
} else {
|
|
1047
|
+
// Fallback: use minRowHeight from table layout or global config if provided
|
|
1048
|
+
// Priority: table-specific layout > global footerGapOption > default 80
|
|
1049
|
+
// Using higher default (80px) to handle text rows with wrapping and multiple lines
|
|
1050
|
+
// This is conservative but prevents text rows from being split across pages
|
|
1051
|
+
var minRowHeight = (tableLayout && tableLayout.minRowHeight) || (footerGapOpt && footerGapOpt.minRowHeight) || 80;
|
|
1052
|
+
estimatedHeight = minRowHeight;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Apply heightOffset from table definition to adjust page break calculation
|
|
1057
|
+
// This allows fine-tuning of page break detection for specific tables
|
|
1058
|
+
// heightOffset is passed as parameter from processTable
|
|
1059
|
+
if (heightOffset) {
|
|
1060
|
+
estimatedHeight = (estimatedHeight || 0) + heightOffset;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Check if row won't fit on current page
|
|
1064
|
+
// Strategy: Force break if row won't fit AND we're not too close to page boundary
|
|
1065
|
+
// "Too close" means availableHeight is very small (< 5px) - at that point forcing
|
|
1066
|
+
// a break would create a nearly-blank page
|
|
1067
|
+
var minSpaceThreshold = 5; // Only skip forced break if < 5px space left
|
|
1068
|
+
|
|
1069
|
+
if (estimatedHeight > availableHeight && availableHeight > minSpaceThreshold) {
|
|
1070
|
+
var currentPage = self.writer.context().page;
|
|
1071
|
+
var currentY = self.writer.context().y;
|
|
1072
|
+
|
|
1073
|
+
// Draw vertical lines to fill the gap from current position to page break
|
|
1074
|
+
// This ensures vertical lines extend all the way to the bottom of the page
|
|
1075
|
+
if (tableNode && tableNode._tableProcessor && rowIndex > 0) {
|
|
1076
|
+
tableNode._tableProcessor.drawVerticalLinesForForcedPageBreak(
|
|
1077
|
+
rowIndex,
|
|
1078
|
+
self.writer,
|
|
1079
|
+
currentY,
|
|
1080
|
+
currentY + availableHeight
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Move to next page before processing row
|
|
1085
|
+
self.writer.context().moveDown(availableHeight);
|
|
1086
|
+
self.writer.moveToNextPage();
|
|
1087
|
+
|
|
1088
|
+
// Track this page break so tableProcessor can draw borders correctly
|
|
1089
|
+
pageBreaks.push({
|
|
1090
|
+
prevPage: currentPage,
|
|
1091
|
+
prevY: currentY + availableHeight,
|
|
1092
|
+
y: self.writer.context().y,
|
|
1093
|
+
page: self.writer.context().page,
|
|
1094
|
+
forced: true // Mark as forced page break
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
// Mark that this row should not break anymore
|
|
1098
|
+
isUnbreakableRow = true;
|
|
1099
|
+
dontBreakRows = true;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
853
1103
|
// Check if row should break by height
|
|
854
1104
|
if (!isUnbreakableRow && height > self.writer.context().availableHeight) {
|
|
855
1105
|
willBreakByHeight = true;
|
|
@@ -898,7 +1148,7 @@ LayoutBuilder.prototype.processRow = function ({ marginX = [0, 0], dontBreakRows
|
|
|
898
1148
|
}
|
|
899
1149
|
|
|
900
1150
|
// We pass the endingSpanCell reference to store the context just after processing rowspan cell
|
|
901
|
-
self.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell);
|
|
1151
|
+
self.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell, heightOffset);
|
|
902
1152
|
|
|
903
1153
|
if (!cell._span) {
|
|
904
1154
|
self.processNode(cell);
|
|
@@ -1053,6 +1303,9 @@ LayoutBuilder.prototype.processTable = function (tableNode) {
|
|
|
1053
1303
|
this.nestedLevel++;
|
|
1054
1304
|
var processor = new TableProcessor(tableNode);
|
|
1055
1305
|
|
|
1306
|
+
// Store processor reference for forced page break vertical line drawing
|
|
1307
|
+
tableNode._tableProcessor = processor;
|
|
1308
|
+
|
|
1056
1309
|
processor.beginTable(this.writer);
|
|
1057
1310
|
|
|
1058
1311
|
var rowHeights = tableNode.table.heights;
|
|
@@ -1082,6 +1335,8 @@ LayoutBuilder.prototype.processTable = function (tableNode) {
|
|
|
1082
1335
|
height = undefined;
|
|
1083
1336
|
}
|
|
1084
1337
|
|
|
1338
|
+
var heightOffset = tableNode.heightOffset != undefined ? tableNode.heightOffset : 0;
|
|
1339
|
+
|
|
1085
1340
|
var pageBeforeProcessing = this.writer.context().page;
|
|
1086
1341
|
|
|
1087
1342
|
var result = this.processRow({
|
|
@@ -1094,7 +1349,8 @@ LayoutBuilder.prototype.processTable = function (tableNode) {
|
|
|
1094
1349
|
tableBody: tableNode.table.body,
|
|
1095
1350
|
tableNode,
|
|
1096
1351
|
rowIndex: i,
|
|
1097
|
-
height
|
|
1352
|
+
height,
|
|
1353
|
+
heightOffset
|
|
1098
1354
|
});
|
|
1099
1355
|
addAll(tableNode.positions, result.positions);
|
|
1100
1356
|
|
|
@@ -1107,7 +1363,10 @@ LayoutBuilder.prototype.processTable = function (tableNode) {
|
|
|
1107
1363
|
}
|
|
1108
1364
|
}
|
|
1109
1365
|
|
|
1110
|
-
|
|
1366
|
+
// Get next row cells for look-ahead page break detection
|
|
1367
|
+
var nextRowCells = (i + 1 < tableNode.table.body.length) ? tableNode.table.body[i + 1] : null;
|
|
1368
|
+
|
|
1369
|
+
processor.endRow(i, this.writer, result.pageBreaks, nextRowCells, this);
|
|
1111
1370
|
}
|
|
1112
1371
|
|
|
1113
1372
|
processor.endTable(this.writer);
|
package/src/printer.js
CHANGED
|
@@ -45,6 +45,9 @@ var findFont = function (fonts, requiredFonts, defaultFont) {
|
|
|
45
45
|
* @class Creates an instance of a PdfPrinter which turns document definition into a pdf
|
|
46
46
|
*
|
|
47
47
|
* @param {Object} fontDescriptors font definition dictionary
|
|
48
|
+
* @param {Object} [options] optional configuration
|
|
49
|
+
* @param {Number} [options.maxImageCacheSize] maximum number of images to cache (default: 100)
|
|
50
|
+
* @param {Number} [options.imageCacheTTL] cache time-to-live in milliseconds (default: 3600000 = 1 hour)
|
|
48
51
|
*
|
|
49
52
|
* @example
|
|
50
53
|
* var fontDescriptors = {
|
|
@@ -56,10 +59,22 @@ var findFont = function (fonts, requiredFonts, defaultFont) {
|
|
|
56
59
|
* }
|
|
57
60
|
* };
|
|
58
61
|
*
|
|
59
|
-
* var printer = new PdfPrinter(fontDescriptors
|
|
62
|
+
* var printer = new PdfPrinter(fontDescriptors, {
|
|
63
|
+
* maxImageCacheSize: 50,
|
|
64
|
+
* imageCacheTTL: 30 * 60 * 1000 // 30 minutes
|
|
65
|
+
* });
|
|
60
66
|
*/
|
|
61
|
-
function PdfPrinter(fontDescriptors) {
|
|
67
|
+
function PdfPrinter(fontDescriptors, options) {
|
|
62
68
|
this.fontDescriptors = fontDescriptors;
|
|
69
|
+
options = options || {};
|
|
70
|
+
|
|
71
|
+
// Cache configuration with memory leak prevention
|
|
72
|
+
this._cacheConfig = {
|
|
73
|
+
maxSize: isNumber(options.maxImageCacheSize) ? options.maxImageCacheSize : 100,
|
|
74
|
+
ttl: isNumber(options.imageCacheTTL) ? options.imageCacheTTL : 3600000 // 1 hour default
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// LRU cache: Map maintains insertion order, so we can implement LRU easily
|
|
63
78
|
this._remoteImageCache = new Map();
|
|
64
79
|
}
|
|
65
80
|
|
|
@@ -115,6 +130,10 @@ function PdfPrinter(fontDescriptors) {
|
|
|
115
130
|
* @return {Object} a pdfKit document object which can be saved or encode to data-url
|
|
116
131
|
*/
|
|
117
132
|
PdfPrinter.prototype.createPdfKitDocument = function (docDefinition, options) {
|
|
133
|
+
if (!docDefinition || typeof docDefinition !== 'object') {
|
|
134
|
+
throw new Error('docDefinition parameter is required and must be an object');
|
|
135
|
+
}
|
|
136
|
+
|
|
118
137
|
options = options || {};
|
|
119
138
|
|
|
120
139
|
docDefinition.version = docDefinition.version || '1.3';
|
|
@@ -196,7 +215,67 @@ PdfPrinter.prototype.resolveRemoteImages = function (docDefinition, timeoutMs) {
|
|
|
196
215
|
return resolveRemoteImages.call(this, docDefinition, timeoutMs);
|
|
197
216
|
};
|
|
198
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Clears the remote image cache
|
|
220
|
+
* Useful for freeing memory or forcing fresh image fetches
|
|
221
|
+
*/
|
|
222
|
+
PdfPrinter.prototype.clearImageCache = function () {
|
|
223
|
+
this._remoteImageCache.clear();
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Gets cache statistics for monitoring
|
|
228
|
+
* @return {Object} Cache statistics
|
|
229
|
+
*/
|
|
230
|
+
PdfPrinter.prototype.getImageCacheStats = function () {
|
|
231
|
+
var now = Date.now();
|
|
232
|
+
var cache = this._remoteImageCache;
|
|
233
|
+
var expired = 0;
|
|
234
|
+
var valid = 0;
|
|
235
|
+
|
|
236
|
+
cache.forEach(function (entry) {
|
|
237
|
+
if (now - entry.timestamp > this._cacheConfig.ttl) {
|
|
238
|
+
expired++;
|
|
239
|
+
} else {
|
|
240
|
+
valid++;
|
|
241
|
+
}
|
|
242
|
+
}, this);
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
size: cache.size,
|
|
246
|
+
maxSize: this._cacheConfig.maxSize,
|
|
247
|
+
ttl: this._cacheConfig.ttl,
|
|
248
|
+
validEntries: valid,
|
|
249
|
+
expiredEntries: expired
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Removes expired entries from cache
|
|
255
|
+
*/
|
|
256
|
+
PdfPrinter.prototype.cleanExpiredCache = function () {
|
|
257
|
+
var now = Date.now();
|
|
258
|
+
var cache = this._remoteImageCache;
|
|
259
|
+
var keysToDelete = [];
|
|
260
|
+
|
|
261
|
+
cache.forEach(function (entry, key) {
|
|
262
|
+
if (now - entry.timestamp > this._cacheConfig.ttl) {
|
|
263
|
+
keysToDelete.push(key);
|
|
264
|
+
}
|
|
265
|
+
}, this);
|
|
266
|
+
|
|
267
|
+
keysToDelete.forEach(function (key) {
|
|
268
|
+
cache.delete(key);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return keysToDelete.length;
|
|
272
|
+
};
|
|
273
|
+
|
|
199
274
|
PdfPrinter.prototype.createPdfKitDocumentAsync = function (docDefinition, options) {
|
|
275
|
+
if (!docDefinition || typeof docDefinition !== 'object') {
|
|
276
|
+
return Promise.reject(new Error('docDefinition parameter is required and must be an object'));
|
|
277
|
+
}
|
|
278
|
+
|
|
200
279
|
var createOptions = options ? Object.assign({}, options) : {};
|
|
201
280
|
var timeout;
|
|
202
281
|
if (Object.prototype.hasOwnProperty.call(createOptions, 'remoteImageTimeout')) {
|
|
@@ -800,11 +879,12 @@ function resolveRemoteImages(docDefinition, timeoutMs) {
|
|
|
800
879
|
|
|
801
880
|
var timeout = typeof timeoutMs === 'number' && timeoutMs > 0 ? timeoutMs : undefined;
|
|
802
881
|
var tasks = [];
|
|
882
|
+
var cacheConfig = this._cacheConfig;
|
|
803
883
|
|
|
804
884
|
remoteTargets.forEach(function (descriptor, key) {
|
|
805
885
|
var cacheKey = createCacheKey(descriptor.url, descriptor.headers);
|
|
806
886
|
tasks.push(
|
|
807
|
-
ensureRemoteBuffer(descriptor.url, descriptor.headers, cacheKey, cache, timeout)
|
|
887
|
+
ensureRemoteBuffer(descriptor.url, descriptor.headers, cacheKey, cache, timeout, cacheConfig)
|
|
808
888
|
.then(function (buffer) {
|
|
809
889
|
images[key] = buffer;
|
|
810
890
|
})
|
|
@@ -906,14 +986,39 @@ function registerInlineImage(node, images) {
|
|
|
906
986
|
return null;
|
|
907
987
|
}
|
|
908
988
|
|
|
909
|
-
function ensureRemoteBuffer(url, headers, cacheKey, cache, timeout) {
|
|
910
|
-
var
|
|
911
|
-
|
|
912
|
-
|
|
989
|
+
function ensureRemoteBuffer(url, headers, cacheKey, cache, timeout, cacheConfig) {
|
|
990
|
+
var now = Date.now();
|
|
991
|
+
|
|
992
|
+
// Check if image is in cache and not expired
|
|
993
|
+
var cached = cache.get(cacheKey);
|
|
994
|
+
if (cached) {
|
|
995
|
+
var age = now - cached.timestamp;
|
|
996
|
+
if (age < cacheConfig.ttl) {
|
|
997
|
+
// Move to end (LRU: mark as recently used)
|
|
998
|
+
cache.delete(cacheKey);
|
|
999
|
+
cache.set(cacheKey, cached);
|
|
1000
|
+
return Promise.resolve(cached.buffer);
|
|
1001
|
+
} else {
|
|
1002
|
+
// Expired - remove from cache
|
|
1003
|
+
cache.delete(cacheKey);
|
|
1004
|
+
}
|
|
913
1005
|
}
|
|
914
1006
|
|
|
1007
|
+
// Fetch remote image and cache the result
|
|
915
1008
|
return fetchRemote(url, headers, timeout).then(function (buffer) {
|
|
916
|
-
cache
|
|
1009
|
+
// Implement LRU eviction if cache is full
|
|
1010
|
+
if (cache.size >= cacheConfig.maxSize) {
|
|
1011
|
+
// Remove oldest entry (first entry in Map)
|
|
1012
|
+
var firstKey = cache.keys().next().value;
|
|
1013
|
+
cache.delete(firstKey);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Store with timestamp for TTL
|
|
1017
|
+
cache.set(cacheKey, {
|
|
1018
|
+
buffer: buffer,
|
|
1019
|
+
timestamp: now
|
|
1020
|
+
});
|
|
1021
|
+
|
|
917
1022
|
return buffer;
|
|
918
1023
|
});
|
|
919
1024
|
}
|