@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/build/pdfmake.js +37925 -36986
- package/build/pdfmake.min.js +2 -2
- package/build/pdfmake.min.js.map +1 -1
- package/package.json +2 -2
- package/src/layoutBuilder.js +251 -2
- package/src/printer.js +10 -9
- package/src/tableProcessor.js +135 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowaccount/pdfmake",
|
|
3
|
-
"version": "0.
|
|
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
|
|
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/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
|
*
|
|
@@ -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
|
-
|
|
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
|
-
|
|
911
|
-
|
|
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) {
|
package/src/tableProcessor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|