@flowaccount/pdfmake 1.0.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowaccount/pdfmake",
3
- "version": "1.0.1",
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",
@@ -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
  };
@@ -990,7 +990,7 @@ LayoutBuilder.prototype._getRowSpanEndingCell = function (tableBody, rowIndex, c
990
990
  return null;
991
991
  };
992
992
 
993
- 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 }) {
994
994
  var self = this;
995
995
  var isUnbreakableRow = dontBreakRows || rowIndex <= rowsWithoutPageBreak - 1;
996
996
  var pageBreaks = [];
@@ -1053,6 +1053,13 @@ LayoutBuilder.prototype.processRow = function ({ marginX = [0, 0], dontBreakRows
1053
1053
  }
1054
1054
  }
1055
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
+
1056
1063
  // Check if row won't fit on current page
1057
1064
  // Strategy: Force break if row won't fit AND we're not too close to page boundary
1058
1065
  // "Too close" means availableHeight is very small (< 5px) - at that point forcing
@@ -1141,7 +1148,7 @@ LayoutBuilder.prototype.processRow = function ({ marginX = [0, 0], dontBreakRows
1141
1148
  }
1142
1149
 
1143
1150
  // We pass the endingSpanCell reference to store the context just after processing rowspan cell
1144
- self.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell);
1151
+ self.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell, heightOffset);
1145
1152
 
1146
1153
  if (!cell._span) {
1147
1154
  self.processNode(cell);
@@ -1328,6 +1335,8 @@ LayoutBuilder.prototype.processTable = function (tableNode) {
1328
1335
  height = undefined;
1329
1336
  }
1330
1337
 
1338
+ var heightOffset = tableNode.heightOffset != undefined ? tableNode.heightOffset : 0;
1339
+
1331
1340
  var pageBeforeProcessing = this.writer.context().page;
1332
1341
 
1333
1342
  var result = this.processRow({
@@ -1340,7 +1349,8 @@ LayoutBuilder.prototype.processTable = function (tableNode) {
1340
1349
  tableBody: tableNode.table.body,
1341
1350
  tableNode,
1342
1351
  rowIndex: i,
1343
- height
1352
+ height,
1353
+ heightOffset
1344
1354
  });
1345
1355
  addAll(tableNode.positions, result.positions);
1346
1356
 
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
 
@@ -200,6 +215,62 @@ PdfPrinter.prototype.resolveRemoteImages = function (docDefinition, timeoutMs) {
200
215
  return resolveRemoteImages.call(this, docDefinition, timeoutMs);
201
216
  };
202
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
+
203
274
  PdfPrinter.prototype.createPdfKitDocumentAsync = function (docDefinition, options) {
204
275
  if (!docDefinition || typeof docDefinition !== 'object') {
205
276
  return Promise.reject(new Error('docDefinition parameter is required and must be an object'));
@@ -808,11 +879,12 @@ function resolveRemoteImages(docDefinition, timeoutMs) {
808
879
 
809
880
  var timeout = typeof timeoutMs === 'number' && timeoutMs > 0 ? timeoutMs : undefined;
810
881
  var tasks = [];
882
+ var cacheConfig = this._cacheConfig;
811
883
 
812
884
  remoteTargets.forEach(function (descriptor, key) {
813
885
  var cacheKey = createCacheKey(descriptor.url, descriptor.headers);
814
886
  tasks.push(
815
- ensureRemoteBuffer(descriptor.url, descriptor.headers, cacheKey, cache, timeout)
887
+ ensureRemoteBuffer(descriptor.url, descriptor.headers, cacheKey, cache, timeout, cacheConfig)
816
888
  .then(function (buffer) {
817
889
  images[key] = buffer;
818
890
  })
@@ -914,9 +986,41 @@ function registerInlineImage(node, images) {
914
986
  return null;
915
987
  }
916
988
 
917
- function ensureRemoteBuffer(url, headers, cacheKey, cache, timeout) {
918
- // Cache disabled - always fetch fresh images
919
- return fetchRemote(url, headers, timeout);
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
+ }
1005
+ }
1006
+
1007
+ // Fetch remote image and cache the result
1008
+ return fetchRemote(url, headers, timeout).then(function (buffer) {
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
+
1022
+ return buffer;
1023
+ });
920
1024
  }
921
1025
 
922
1026
  function parseRemoteDescriptor(value) {