@flowaccount/pdfmake 0.2.20-staging.2 → 1.0.1

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": "0.2.20-staging.2",
3
+ "version": "1.0.1",
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 --access public",
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"
@@ -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
  *
@@ -848,8 +998,101 @@ 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
+ // Check if row won't fit on current page
1057
+ // Strategy: Force break if row won't fit AND we're not too close to page boundary
1058
+ // "Too close" means availableHeight is very small (< 5px) - at that point forcing
1059
+ // a break would create a nearly-blank page
1060
+ var minSpaceThreshold = 5; // Only skip forced break if < 5px space left
1061
+
1062
+ if (estimatedHeight > availableHeight && availableHeight > minSpaceThreshold) {
1063
+ var currentPage = self.writer.context().page;
1064
+ var currentY = self.writer.context().y;
1065
+
1066
+ // Draw vertical lines to fill the gap from current position to page break
1067
+ // This ensures vertical lines extend all the way to the bottom of the page
1068
+ if (tableNode && tableNode._tableProcessor && rowIndex > 0) {
1069
+ tableNode._tableProcessor.drawVerticalLinesForForcedPageBreak(
1070
+ rowIndex,
1071
+ self.writer,
1072
+ currentY,
1073
+ currentY + availableHeight
1074
+ );
1075
+ }
1076
+
1077
+ // Move to next page before processing row
1078
+ self.writer.context().moveDown(availableHeight);
1079
+ self.writer.moveToNextPage();
1080
+
1081
+ // Track this page break so tableProcessor can draw borders correctly
1082
+ pageBreaks.push({
1083
+ prevPage: currentPage,
1084
+ prevY: currentY + availableHeight,
1085
+ y: self.writer.context().y,
1086
+ page: self.writer.context().page,
1087
+ forced: true // Mark as forced page break
1088
+ });
1089
+
1090
+ // Mark that this row should not break anymore
1091
+ isUnbreakableRow = true;
1092
+ dontBreakRows = true;
1093
+ }
1094
+ }
1095
+
853
1096
  // Check if row should break by height
854
1097
  if (!isUnbreakableRow && height > self.writer.context().availableHeight) {
855
1098
  willBreakByHeight = true;
@@ -1053,6 +1296,9 @@ LayoutBuilder.prototype.processTable = function (tableNode) {
1053
1296
  this.nestedLevel++;
1054
1297
  var processor = new TableProcessor(tableNode);
1055
1298
 
1299
+ // Store processor reference for forced page break vertical line drawing
1300
+ tableNode._tableProcessor = processor;
1301
+
1056
1302
  processor.beginTable(this.writer);
1057
1303
 
1058
1304
  var rowHeights = tableNode.table.heights;
@@ -1107,7 +1353,10 @@ LayoutBuilder.prototype.processTable = function (tableNode) {
1107
1353
  }
1108
1354
  }
1109
1355
 
1110
- processor.endRow(i, this.writer, result.pageBreaks);
1356
+ // Get next row cells for look-ahead page break detection
1357
+ var nextRowCells = (i + 1 < tableNode.table.body.length) ? tableNode.table.body[i + 1] : null;
1358
+
1359
+ processor.endRow(i, this.writer, result.pageBreaks, nextRowCells, this);
1111
1360
  }
1112
1361
 
1113
1362
  processor.endTable(this.writer);
package/src/printer.js CHANGED
@@ -115,6 +115,10 @@ function PdfPrinter(fontDescriptors) {
115
115
  * @return {Object} a pdfKit document object which can be saved or encode to data-url
116
116
  */
117
117
  PdfPrinter.prototype.createPdfKitDocument = function (docDefinition, options) {
118
+ if (!docDefinition || typeof docDefinition !== 'object') {
119
+ throw new Error('docDefinition parameter is required and must be an object');
120
+ }
121
+
118
122
  options = options || {};
119
123
 
120
124
  docDefinition.version = docDefinition.version || '1.3';
@@ -197,6 +201,10 @@ PdfPrinter.prototype.resolveRemoteImages = function (docDefinition, timeoutMs) {
197
201
  };
198
202
 
199
203
  PdfPrinter.prototype.createPdfKitDocumentAsync = function (docDefinition, options) {
204
+ if (!docDefinition || typeof docDefinition !== 'object') {
205
+ return Promise.reject(new Error('docDefinition parameter is required and must be an object'));
206
+ }
207
+
200
208
  var createOptions = options ? Object.assign({}, options) : {};
201
209
  var timeout;
202
210
  if (Object.prototype.hasOwnProperty.call(createOptions, 'remoteImageTimeout')) {
@@ -907,15 +915,8 @@ function registerInlineImage(node, images) {
907
915
  }
908
916
 
909
917
  function ensureRemoteBuffer(url, headers, cacheKey, cache, timeout) {
910
- var existing = cache.get(cacheKey);
911
- if (existing) {
912
- return Promise.resolve(existing);
913
- }
914
-
915
- return fetchRemote(url, headers, timeout).then(function (buffer) {
916
- cache.set(cacheKey, buffer);
917
- return buffer;
918
- });
918
+ // Cache disabled - always fetch fresh images
919
+ return fetchRemote(url, headers, timeout);
919
920
  }
920
921
 
921
922
  function parseRemoteDescriptor(value) {
@@ -194,8 +194,8 @@ TableProcessor.prototype.beginRow = function (rowIndex, writer) {
194
194
  writer.context().moveDown(this.rowPaddingTop);
195
195
  };
196
196
 
197
- TableProcessor.prototype.drawHorizontalLine = function (lineIndex, writer, overrideY, moveDown = true, forcePage) {
198
- var lineWidth = this.layout.hLineWidth(lineIndex, this.tableNode, writer);
197
+ TableProcessor.prototype.drawHorizontalLine = function (lineIndex, writer, overrideY, moveDown = true, forcePage, isPageBreak = false) {
198
+ var lineWidth = this.layout.hLineWidth(lineIndex, this.tableNode, writer, isPageBreak);
199
199
  if (lineWidth) {
200
200
  var style = this.layout.hLineStyle(lineIndex, this.tableNode);
201
201
  var dash;
@@ -310,6 +310,72 @@ TableProcessor.prototype.drawHorizontalLine = function (lineIndex, writer, overr
310
310
  }
311
311
  };
312
312
 
313
+
314
+ /**
315
+ * Draws vertical lines to fill the gap when a row is forced to the next page.
316
+ * This prevents missing vertical lines at the bottom of the page.
317
+ * @param {number} rowIndex - The index of the row being forced to next page
318
+ * @param {object} writer - The document writer
319
+ * @param {number} y0 - Starting Y position
320
+ * @param {number} y1 - Ending Y position (page break point)
321
+ */
322
+ TableProcessor.prototype.drawVerticalLinesForForcedPageBreak = function (rowIndex, writer, y0, y1) {
323
+ if (!this.rowSpanData || rowIndex <= 0) {
324
+ return;
325
+ }
326
+
327
+ var body = this.tableNode.table.body;
328
+ var prevRowIndex = rowIndex - 1; // Use previous row for cell border detection
329
+
330
+ // Get X positions for vertical lines (similar logic to endRow's getLineXs)
331
+ var xs = [];
332
+ var cols = 0;
333
+
334
+ for (var i = 0, l = body[prevRowIndex].length; i < l; i++) {
335
+ if (!cols) {
336
+ xs.push({ x: this.rowSpanData[i].left, index: i });
337
+ var item = body[prevRowIndex][i];
338
+ cols = (item._colSpan || item.colSpan || 0);
339
+ }
340
+ if (cols > 0) {
341
+ cols--;
342
+ }
343
+ }
344
+ xs.push({ x: this.rowSpanData[this.rowSpanData.length - 1].left, index: this.rowSpanData.length - 1 });
345
+
346
+ // Draw vertical lines for each column position
347
+ for (var xi = 0, xl = xs.length; xi < xl; xi++) {
348
+ var leftCellBorder = false;
349
+ var colIndex = xs[xi].index;
350
+
351
+ // Check if we need to draw a vertical line at this position
352
+ // based on cell borders from the previous row
353
+ var cell;
354
+ if (colIndex < body[prevRowIndex].length) {
355
+ cell = body[prevRowIndex][colIndex];
356
+ leftCellBorder = cell.border ? cell.border[0] : this.layout.defaultBorder;
357
+ }
358
+
359
+ // Check cell before
360
+ if (colIndex > 0 && !leftCellBorder) {
361
+ cell = body[prevRowIndex][colIndex - 1];
362
+ leftCellBorder = cell.border ? cell.border[2] : this.layout.defaultBorder;
363
+ }
364
+
365
+ if (leftCellBorder) {
366
+ this.drawVerticalLine(
367
+ xs[xi].x,
368
+ y0,
369
+ y1,
370
+ xs[xi].index,
371
+ writer,
372
+ prevRowIndex,
373
+ xi > 0 ? xs[xi - 1].index : null
374
+ );
375
+ }
376
+ }
377
+ };
378
+
313
379
  TableProcessor.prototype.drawVerticalLine = function (x, y0, y1, vLineColIndex, writer, vLineRowIndex, beforeVLineColIndex) {
314
380
  var width = this.layout.vLineWidth(vLineColIndex, this.tableNode);
315
381
  if (width === 0) {
@@ -409,9 +475,11 @@ TableProcessor.prototype.endTable = function (writer) {
409
475
  }
410
476
  };
411
477
 
412
- TableProcessor.prototype.endRow = function (rowIndex, writer, pageBreaks) {
478
+ TableProcessor.prototype.endRow = function (rowIndex, writer, pageBreaks, nextRowCells, layoutBuilder) {
413
479
  var l, i;
414
480
  var self = this;
481
+ var tableLayout = this.tableNode && this.tableNode._layout;
482
+ var nearBottomThreshold = (tableLayout && tableLayout.nearBottomThreshold) || 20;
415
483
 
416
484
  writer.tracker.stopTracking('pageChanged', this.rowCallback);
417
485
  writer.context().moveDown(this.layout.paddingBottom(rowIndex, this.tableNode));
@@ -437,7 +505,7 @@ TableProcessor.prototype.endRow = function (rowIndex, writer, pageBreaks) {
437
505
  var pageBreak = pageBreaks[i];
438
506
  ys[ys.length - 1].y1 = pageBreak.prevY;
439
507
 
440
- ys.push({ y0: pageBreak.y, page: pageBreak.prevPage + 1 });
508
+ ys.push({ y0: pageBreak.y, page: pageBreak.prevPage + 1, forced: pageBreak.forced });
441
509
  }
442
510
  }
443
511
 
@@ -473,11 +541,21 @@ TableProcessor.prototype.endRow = function (rowIndex, writer, pageBreaks) {
473
541
  }
474
542
 
475
543
  // Draw horizontal lines before the vertical lines so they are not overridden
544
+ // Only set isPageBreak=true when at TRUE page bottom (not just forced breaks)
545
+ var ctx = writer.context();
546
+ var currentPage = ctx.getCurrentPage && ctx.getCurrentPage();
547
+ var pageHeight = currentPage ? (currentPage.pageSize.height - ctx.pageMargins.bottom) : 0;
548
+
476
549
  if (willBreak && this.layout.hLineWhenBroken !== false) {
477
- this.drawHorizontalLine(rowIndex + 1, writer, y2);
550
+ // Check if we're at the true page bottom
551
+ var isAtTruePageBottom = (pageHeight - y2) <= nearBottomThreshold;
552
+ this.drawHorizontalLine(rowIndex + 1, writer, y2, true, null, isAtTruePageBottom);
478
553
  }
479
554
  if (rowBreakWithoutHeader && this.layout.hLineWhenBroken !== false) {
480
- this.drawHorizontalLine(rowIndex, writer, y1);
555
+ // Check if previous segment ended at true page bottom
556
+ var prevSegmentY = (yi > 0) ? ys[yi - 1].y1 : 0;
557
+ var prevWasAtPageBottom = (pageHeight - prevSegmentY) <= nearBottomThreshold;
558
+ this.drawHorizontalLine(rowIndex, writer, y1, true, null, prevWasAtPageBottom);
481
559
  }
482
560
 
483
561
  for (i = 0, l = xs.length; i < l; i++) {
@@ -596,7 +674,57 @@ TableProcessor.prototype.endRow = function (rowIndex, writer, pageBreaks) {
596
674
  }
597
675
  }
598
676
 
599
- this.drawHorizontalLine(rowIndex + 1, writer);
677
+ // Look ahead: Check if next row will cause a page break
678
+ // If yes, skip drawing the horizontal line at the end of current row
679
+ // This feature is OPT-IN: only enabled when forcePageBreakForAllRows === true
680
+ var shouldSkipHorizontalLine = false;
681
+
682
+ if (nextRowCells && layoutBuilder) {
683
+ // Only perform look-ahead if forcePageBreakForAllRows is explicitly enabled
684
+ // This prevents affecting existing PDFs that don't use this feature
685
+ if (tableLayout && tableLayout.forcePageBreakForAllRows === true) {
686
+ var nextRowEstimatedHeight = 0;
687
+
688
+ // Try to get maximum cell height from next row's measured content
689
+ if (layoutBuilder._getMaxCellHeight) {
690
+ var maxNextCellHeight = layoutBuilder._getMaxCellHeight(nextRowCells);
691
+ if (maxNextCellHeight > 0) {
692
+ nextRowEstimatedHeight = maxNextCellHeight + 10; // Add padding
693
+ }
694
+ }
695
+
696
+ // Fallback: use minRowHeight from table layout
697
+ if (nextRowEstimatedHeight === 0) {
698
+ var footerGapOpt = writer.context()._footerGapOption;
699
+ nextRowEstimatedHeight = (tableLayout && tableLayout.minRowHeight) || (footerGapOpt && footerGapOpt.minRowHeight) || 80;
700
+ }
701
+
702
+ // Check current available height (after current row has been processed)
703
+ var currentAvailableHeight = writer.context().availableHeight;
704
+
705
+ // Skip drawing the horizontal line if EITHER:
706
+ // 1. Next row won't fit (will cause page break), OR
707
+ // 2. We're very close to page bottom (remaining space <= threshold)
708
+ // This prevents duplicate/orphaned lines at page boundaries
709
+ if (nextRowEstimatedHeight > currentAvailableHeight || currentAvailableHeight <= nearBottomThreshold) {
710
+ // Exception: Check if this line should always be drawn (critical boundary)
711
+ // Allow layout to specify via alwaysDrawHLine callback
712
+ var shouldAlwaysDrawLine = false;
713
+ if (typeof tableLayout.alwaysDrawHLine === 'function') {
714
+ shouldAlwaysDrawLine = tableLayout.alwaysDrawHLine(rowIndex + 1, this.tableNode, writer);
715
+ }
716
+
717
+ if (!shouldAlwaysDrawLine) {
718
+ shouldSkipHorizontalLine = true;
719
+ }
720
+ }
721
+ }
722
+ }
723
+
724
+ // Draw horizontal line at the end of the row, unless next row will cause a page break
725
+ if (!shouldSkipHorizontalLine) {
726
+ this.drawHorizontalLine(rowIndex + 1, writer);
727
+ }
600
728
 
601
729
  if (this.headerRows && rowIndex === this.headerRows - 1) {
602
730
  this.headerRepeatable = writer.currentBlockToRepeatable();