node-pptx-templater 1.1.4 → 1.1.7
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 +0 -16
- package/README.md +10 -34
- package/package.json +1 -3
- package/src/core/PPTXTemplater.js +13 -41
- package/src/managers/ShapeManager.js +8 -0
- package/src/managers/TableManager.js +406 -78
package/CHANGELOG.md
CHANGED
|
@@ -5,22 +5,6 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [1.1.3] - 2026-06-22
|
|
9
|
-
|
|
10
|
-
### Fixed
|
|
11
|
-
|
|
12
|
-
- **Layout Preservation in `addCellShape()`**: Fixed a critical layout bug where querying cell bounds or adding cell shapes dynamically (via `addCellShape()` or `updateCellShape()`) mutated table row heights inside the XML. The sizing/positioning logic now runs in a layout-only mode, keeping the table XML completely untouched and respecting original template custom row heights.
|
|
13
|
-
|
|
14
|
-
## [1.1.2] - 2026-06-22
|
|
15
|
-
|
|
16
|
-
### Fixed
|
|
17
|
-
|
|
18
|
-
- **Slide Link Relationships Target Paths**: Fixed PowerPoint "Repair Mode" errors when adding slide-to-slide hyperlinks (via `addSlideLink` or `addImageLink`). The relative target path resolves correctly to the sibling filename `slideX.xml` instead of using the redundant parent prefix `../slides/slideX.xml` (which exited and re-entered the same directory, violating PowerPoint's relative path resolution rules).
|
|
19
|
-
|
|
20
|
-
### Added
|
|
21
|
-
|
|
22
|
-
- **Redundant Traversal Validation**: Added automated check in `ValidationEngine` to identify and error on redundant relative path traversals inside relationship files (e.g. referencing `../slides/slideX.xml` from a source slide file already located in `ppt/slides/`).
|
|
23
|
-
|
|
24
8
|
## [1.1.0] - 2026-06-12
|
|
25
9
|
|
|
26
10
|
### Added
|
package/README.md
CHANGED
|
@@ -172,51 +172,27 @@ const meta = await ppt.getTableRows('SalesTable', { includeMetadata: true });
|
|
|
172
172
|
```
|
|
173
173
|
|
|
174
174
|
### Table Cell Shapes
|
|
175
|
-
Cell shapes are overlay graphics anchored within table cells. They are positioned absolutely based on cell bounds, and **never** modify row heights, column widths, cell margins, or trigger table reflow.
|
|
176
|
-
|
|
177
|
-
#### Merged Cell Support
|
|
178
|
-
When targeting a merged cell (spanned using `rowSpan` or `colSpan`), `addCellShape()` dynamically resolves the **actual rendered bounds** of the entire merged cell region (summing the width and height of the spanned rows/columns). The shape is positioned and aligned relative to this final merged boundary. Any offset `x` and `y` specified is interpreted relative to the top-left corner of the resolved merged cell region.
|
|
179
|
-
|
|
180
|
-
#### Alignment & Presets
|
|
181
|
-
Shapes can be aligned dynamically within the cell (or merged region) using preset positions or explicit alignments:
|
|
182
|
-
* **Presets**: Use `position` values like `'top-left'`, `'center'`, `'bottom-right'`, etc.
|
|
183
|
-
* **Alignment Options**: Set `alignX: 'left' | 'center' | 'right'` and `alignY: 'top' | 'middle' | 'bottom'`. If no alignments or offsets are provided, shapes default to true centering (`'center'`/`'middle'`).
|
|
184
|
-
* **Offsets & Padding**: Offsets (`x`, `y`) are applied relative to the resolved alignment. For instance, when `alignX: 'right'` is used, the shape is placed at `cellRight - shapeWidth - x` (defaulting to a `5px` padding if `x` is omitted).
|
|
175
|
+
Cell shapes are overlay graphics anchored within table cells. They are positioned absolutely based on cell bounds, and **never** modify row heights, column widths, cell margins, or trigger table reflow. Offsets (`x`, `y`) are relative to the cell's top-left corner. Oversized shapes are scaled down proportionally to fit inside the cell.
|
|
185
176
|
|
|
186
177
|
```javascript
|
|
187
|
-
// Add a
|
|
188
|
-
await ppt.addCellShape('
|
|
178
|
+
// Add a simple indicator
|
|
179
|
+
await ppt.addCellShape('SalesTable', 2, 1, {
|
|
189
180
|
type: 'circle',
|
|
190
181
|
width: 12,
|
|
191
182
|
height: 12,
|
|
192
|
-
fill: '#10B981'
|
|
193
|
-
alignX: 'center',
|
|
194
|
-
alignY: 'middle' // Visually centered in the merged cell
|
|
183
|
+
fill: '#10B981' // Green status dot
|
|
195
184
|
});
|
|
196
185
|
|
|
197
|
-
// Add a badge with text and
|
|
198
|
-
await ppt.addCellShape('
|
|
186
|
+
// Add a badge with text and custom offsets
|
|
187
|
+
await ppt.addCellShape('SalesTable', 1, 2, {
|
|
199
188
|
type: 'badge',
|
|
200
|
-
text: '
|
|
201
|
-
fill: '#
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
x: 5,
|
|
205
|
-
y: 3,
|
|
189
|
+
text: 'Active',
|
|
190
|
+
fill: '#3B82F6',
|
|
191
|
+
x: 4,
|
|
192
|
+
y: 2,
|
|
206
193
|
width: 50,
|
|
207
194
|
height: 16
|
|
208
195
|
});
|
|
209
|
-
|
|
210
|
-
// Add an indicator with bottom-right alignment and custom padding
|
|
211
|
-
await ppt.addCellShape('StatusTable', 1, 1, {
|
|
212
|
-
type: 'icon',
|
|
213
|
-
icon: 'up',
|
|
214
|
-
size: 14,
|
|
215
|
-
alignX: 'right',
|
|
216
|
-
alignY: 'bottom',
|
|
217
|
-
x: 4,
|
|
218
|
-
y: 2
|
|
219
|
-
});
|
|
220
196
|
```
|
|
221
197
|
|
|
222
198
|
### XML Folder Workflow
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-pptx-templater",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "High-performance, low-level PowerPoint (PPTX) OpenXML template engine for Node.js. Dynamically replace text, insert images, update charts (with Excel workbook data caching), and merge table cells without PowerPoint corruption or Repair Mode prompts.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -37,8 +37,6 @@
|
|
|
37
37
|
"example:table-extraction": "node examples/table-extraction.js",
|
|
38
38
|
"example:nested-rows": "node examples/nested-table-rows.js",
|
|
39
39
|
"example:xml-folder": "node examples/xml-folder-workflow.js",
|
|
40
|
-
"example:shape-cells": "node examples/shape-cells.js",
|
|
41
|
-
"example:all": "node scripts/run-examples-check.js",
|
|
42
40
|
"prepublishOnly": "npm run lint && npm run test"
|
|
43
41
|
},
|
|
44
42
|
"keywords": [
|
|
@@ -473,10 +473,14 @@ class PPTXTemplater {
|
|
|
473
473
|
throw new PPTXError(`Destination is a file: ${outputPath}`)
|
|
474
474
|
}
|
|
475
475
|
const files = fs.readdirSync(resolvedOut)
|
|
476
|
-
if (files.length > 0
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
476
|
+
if (files.length > 0) {
|
|
477
|
+
if (!options.overwrite) {
|
|
478
|
+
throw new PPTXError(
|
|
479
|
+
`Destination directory "${outputPath}" is not empty. Set overwrite: true to overwrite.`
|
|
480
|
+
)
|
|
481
|
+
} else {
|
|
482
|
+
await fs.emptyDir(resolvedOut)
|
|
483
|
+
}
|
|
480
484
|
}
|
|
481
485
|
} else {
|
|
482
486
|
await fs.ensureDir(resolvedOut)
|
|
@@ -1569,30 +1573,11 @@ class PPTXTemplater {
|
|
|
1569
1573
|
}
|
|
1570
1574
|
|
|
1571
1575
|
/**
|
|
1572
|
-
* Appends one or more rows to a table. Supports flat arrays
|
|
1573
|
-
* for rowspan-merged cells
|
|
1574
|
-
* a graphic indicator directly inside a cell.
|
|
1575
|
-
*
|
|
1576
|
-
* ### Shape Cell Object
|
|
1577
|
-
* Instead of a string value you may pass an object with the following keys:
|
|
1578
|
-
*
|
|
1579
|
-
* | Key | Type | Default | Description |
|
|
1580
|
-
* |-----|------|---------|-------------|
|
|
1581
|
-
* | `type` | `string` | **required** | Shape preset: `'circle'`, `'square'`, `'rectangle'`, `'triangle'`, `'diamond'`, `'hexagon'`, `'line'`. |
|
|
1582
|
-
* | `text` | `string` | `''` | Optional label rendered next to the shape. |
|
|
1583
|
-
* | `color` | `string` | `'#4CAF50'` | Fill colour of the shape (hex). |
|
|
1584
|
-
* | `width` | `number` | `14` | Shape width in points. |
|
|
1585
|
-
* | `height` | `number` | `14` | Shape height in points. |
|
|
1586
|
-
* | `position` | `string` | `'center'` | Alignment inside the cell: `'center'`, `'left'`, `'right'`, `'top'`, `'bottom'`, `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'`. |
|
|
1587
|
-
* | `offsetX` | `number` | `0` | Additional horizontal offset from the resolved anchor (pt). |
|
|
1588
|
-
* | `offsetY` | `number` | `0` | Additional vertical offset from the resolved anchor (pt). |
|
|
1589
|
-
*
|
|
1590
|
-
* The shape is added as a floating overlay anchored to the cell; the table
|
|
1591
|
-
* dimensions (row heights, column widths) are **never mutated**.
|
|
1576
|
+
* Appends one or more rows to a table. Supports flat arrays and nested arrays
|
|
1577
|
+
* for rowspan-merged cells.
|
|
1592
1578
|
*
|
|
1593
1579
|
* @param {string} tableId - Table name or shape ID.
|
|
1594
|
-
* @param {Array<string|Array<string
|
|
1595
|
-
* a plain string, a nested array (rowspan), or a Shape Cell configuration object.
|
|
1580
|
+
* @param {Array<string|Array<string>>} rowData - Row data. Nested arrays create rowspan cells.
|
|
1596
1581
|
* @param {Object} [options] - Row insertion options.
|
|
1597
1582
|
* @param {'rowspan'|'auto'|'none'} [options.mergeStrategy='rowspan'] - How to handle nested arrays.
|
|
1598
1583
|
* `'rowspan'` creates OpenXML vertical spans, `'auto'` merges identical adjacent values,
|
|
@@ -1605,20 +1590,6 @@ class PPTXTemplater {
|
|
|
1605
1590
|
*
|
|
1606
1591
|
* // Nested row with rowspan
|
|
1607
1592
|
* ppt.addTableRow('SalesTable', ['Region', ['Q1', 'Q2'], '$5K'], { mergeStrategy: 'rowspan' });
|
|
1608
|
-
*
|
|
1609
|
-
* // KPI indicator – green circle placed in the cell centre, no label
|
|
1610
|
-
* await ppt.addTableRow('StatusTable', [
|
|
1611
|
-
* 'Alice',
|
|
1612
|
-
* 'Engineering',
|
|
1613
|
-
* { type: 'circle', color: '#4CAF50', width: 12, height: 12, position: 'center' },
|
|
1614
|
-
* ]);
|
|
1615
|
-
*
|
|
1616
|
-
* // Status badge – shape with accompanying text label, aligned to the left
|
|
1617
|
-
* await ppt.addTableRow('StatusTable', [
|
|
1618
|
-
* 'Bob',
|
|
1619
|
-
* 'Design',
|
|
1620
|
-
* { type: 'circle', color: '#F44336', text: 'Blocked', position: 'left' },
|
|
1621
|
-
* ]);
|
|
1622
1593
|
*/
|
|
1623
1594
|
addTableRow(tableId, rowData, options = {}) {
|
|
1624
1595
|
this.#assertLoaded()
|
|
@@ -1745,7 +1716,8 @@ class PPTXTemplater {
|
|
|
1745
1716
|
colIndex,
|
|
1746
1717
|
value,
|
|
1747
1718
|
options,
|
|
1748
|
-
this.#slideManager
|
|
1719
|
+
this.#slideManager,
|
|
1720
|
+
this.#shapeManager
|
|
1749
1721
|
)
|
|
1750
1722
|
}
|
|
1751
1723
|
return this
|
|
@@ -433,6 +433,14 @@ class ShapeManager {
|
|
|
433
433
|
if (!options) return options
|
|
434
434
|
const normalized = { ...options }
|
|
435
435
|
|
|
436
|
+
// Fallback mapping between id and name for compatibility
|
|
437
|
+
if (normalized.id === undefined && normalized.name !== undefined) {
|
|
438
|
+
normalized.id = normalized.name
|
|
439
|
+
}
|
|
440
|
+
if (normalized.name === undefined && normalized.id !== undefined) {
|
|
441
|
+
normalized.name = normalized.id
|
|
442
|
+
}
|
|
443
|
+
|
|
436
444
|
// 1. Map color alias to fill
|
|
437
445
|
const fillVal = options.fill !== undefined ? options.fill : options.color
|
|
438
446
|
if (fillVal !== undefined) {
|
|
@@ -80,11 +80,7 @@ class TableManager {
|
|
|
80
80
|
* @throws {TableNotFoundError} If the table is not found.
|
|
81
81
|
*/
|
|
82
82
|
updateTable(slideIndex, tableId, data, slideManager, shapeManager) {
|
|
83
|
-
const { tblObj,
|
|
84
|
-
slideIndex,
|
|
85
|
-
tableId,
|
|
86
|
-
slideManager
|
|
87
|
-
)
|
|
83
|
+
const { tblObj, resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
|
|
88
84
|
|
|
89
85
|
const trs = tblObj['a:tr'] || []
|
|
90
86
|
if (trs.length === 0) {
|
|
@@ -236,6 +232,7 @@ class TableManager {
|
|
|
236
232
|
)
|
|
237
233
|
}
|
|
238
234
|
|
|
235
|
+
this.#calculateColumnWidths(slideIndex, tableId, slideManager, tblObj, true)
|
|
239
236
|
this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, true)
|
|
240
237
|
|
|
241
238
|
if (cellShapes) {
|
|
@@ -248,11 +245,14 @@ class TableManager {
|
|
|
248
245
|
cellShapes,
|
|
249
246
|
slideManager,
|
|
250
247
|
shapeManager,
|
|
251
|
-
tblObj
|
|
252
|
-
frameObj
|
|
248
|
+
tblObj
|
|
253
249
|
)
|
|
254
250
|
}
|
|
255
251
|
|
|
252
|
+
if (shapeManager) {
|
|
253
|
+
this.#repositionAllTableCellShapes(slideIndex, tableId, slideManager, shapeManager)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
256
|
logger.debug(
|
|
257
257
|
`Updated table "${tableId}" with ${rowsData.length} rows and ${finalMerges.length} merges`
|
|
258
258
|
)
|
|
@@ -484,6 +484,13 @@ class TableManager {
|
|
|
484
484
|
)
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
|
+
|
|
488
|
+
this.#calculateColumnWidths(slideIndex, tableId, slideManager, tblObj, true)
|
|
489
|
+
this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, true)
|
|
490
|
+
|
|
491
|
+
if (shapeManager) {
|
|
492
|
+
this.#repositionAllTableCellShapes(slideIndex, tableId, slideManager, shapeManager)
|
|
493
|
+
}
|
|
487
494
|
}
|
|
488
495
|
|
|
489
496
|
/**
|
|
@@ -506,6 +513,9 @@ class TableManager {
|
|
|
506
513
|
|
|
507
514
|
slideManager.markSlideObjDirty(slideIndex)
|
|
508
515
|
|
|
516
|
+
this.#calculateColumnWidths(slideIndex, tableId, slideManager, tblObj, true)
|
|
517
|
+
this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, true)
|
|
518
|
+
|
|
509
519
|
if (shapeManager) {
|
|
510
520
|
this.#adjustCellShapesAfterRowShift(
|
|
511
521
|
slideIndex,
|
|
@@ -557,6 +567,9 @@ class TableManager {
|
|
|
557
567
|
|
|
558
568
|
slideManager.markSlideObjDirty(slideIndex)
|
|
559
569
|
|
|
570
|
+
this.#calculateColumnWidths(slideIndex, tableId, slideManager, tblObj, true)
|
|
571
|
+
this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, true)
|
|
572
|
+
|
|
560
573
|
if (shapeManager) {
|
|
561
574
|
this.#adjustCellShapesAfterRowShift(
|
|
562
575
|
slideIndex,
|
|
@@ -629,7 +642,16 @@ class TableManager {
|
|
|
629
642
|
* @param {Object} options
|
|
630
643
|
* @param {SlideManager} slideManager
|
|
631
644
|
*/
|
|
632
|
-
updateCell(
|
|
645
|
+
updateCell(
|
|
646
|
+
slideIndex,
|
|
647
|
+
tableId,
|
|
648
|
+
rowIndex,
|
|
649
|
+
colIndex,
|
|
650
|
+
value,
|
|
651
|
+
options = {},
|
|
652
|
+
slideManager,
|
|
653
|
+
shapeManager = null
|
|
654
|
+
) {
|
|
633
655
|
const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
|
|
634
656
|
|
|
635
657
|
const row = tblObj['a:tr']?.[rowIndex]
|
|
@@ -680,6 +702,13 @@ class TableManager {
|
|
|
680
702
|
}
|
|
681
703
|
|
|
682
704
|
slideManager.markSlideObjDirty(slideIndex)
|
|
705
|
+
|
|
706
|
+
this.#calculateColumnWidths(slideIndex, tableId, slideManager, tblObj, true)
|
|
707
|
+
this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, true)
|
|
708
|
+
|
|
709
|
+
if (shapeManager) {
|
|
710
|
+
this.#repositionAllTableCellShapes(slideIndex, tableId, slideManager, shapeManager)
|
|
711
|
+
}
|
|
683
712
|
}
|
|
684
713
|
|
|
685
714
|
/**
|
|
@@ -840,6 +869,9 @@ class TableManager {
|
|
|
840
869
|
|
|
841
870
|
slideManager.markSlideObjDirty(slideIndex)
|
|
842
871
|
|
|
872
|
+
this.#calculateColumnWidths(slideIndex, tableId, slideManager, tblObj, true)
|
|
873
|
+
this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, true)
|
|
874
|
+
|
|
843
875
|
if (shapeManager) {
|
|
844
876
|
this.#repositionCellShapesInRegion(
|
|
845
877
|
slideIndex,
|
|
@@ -852,6 +884,7 @@ class TableManager {
|
|
|
852
884
|
slideManager,
|
|
853
885
|
shapeManager
|
|
854
886
|
)
|
|
887
|
+
this.#repositionAllTableCellShapes(slideIndex, tableId, slideManager, shapeManager)
|
|
855
888
|
}
|
|
856
889
|
}
|
|
857
890
|
|
|
@@ -957,6 +990,13 @@ class TableManager {
|
|
|
957
990
|
}
|
|
958
991
|
}
|
|
959
992
|
|
|
993
|
+
this.#calculateColumnWidths(slideIndex, tableId, actualSlideManager, tblObj, true)
|
|
994
|
+
this.#calculateRowHeights(slideIndex, tableId, actualSlideManager, tblObj, true)
|
|
995
|
+
|
|
996
|
+
if (shapeManager) {
|
|
997
|
+
this.#repositionAllTableCellShapes(slideIndex, tableId, actualSlideManager, shapeManager)
|
|
998
|
+
}
|
|
999
|
+
|
|
960
1000
|
actualSlideManager.markSlideObjDirty(slideIndex)
|
|
961
1001
|
}
|
|
962
1002
|
|
|
@@ -1126,11 +1166,8 @@ class TableManager {
|
|
|
1126
1166
|
const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
|
|
1127
1167
|
const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
|
|
1128
1168
|
|
|
1129
|
-
const
|
|
1130
|
-
const
|
|
1131
|
-
const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
|
|
1132
|
-
|
|
1133
|
-
const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, false)
|
|
1169
|
+
const colWidths = this.#calculateColumnWidths(slideIndex, tableId, slideManager, tblObj, true)
|
|
1170
|
+
const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, true)
|
|
1134
1171
|
|
|
1135
1172
|
const R = this.getMergeRegion(slideIndex, tableId, rowIndex, colIndex, slideManager)
|
|
1136
1173
|
let pr = rowIndex
|
|
@@ -1719,40 +1756,52 @@ class TableManager {
|
|
|
1719
1756
|
}
|
|
1720
1757
|
|
|
1721
1758
|
// 2. Determine alignment settings
|
|
1722
|
-
let alignX =
|
|
1723
|
-
let alignY =
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
.
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1759
|
+
let alignX = config.alignX
|
|
1760
|
+
let alignY = config.alignY
|
|
1761
|
+
|
|
1762
|
+
if (config.position) {
|
|
1763
|
+
switch (config.position) {
|
|
1764
|
+
case 'top-left':
|
|
1765
|
+
if (!alignX) alignX = 'left'
|
|
1766
|
+
if (!alignY) alignY = 'top'
|
|
1767
|
+
break
|
|
1768
|
+
case 'top-center':
|
|
1769
|
+
case 'top':
|
|
1770
|
+
if (!alignX) alignX = 'center'
|
|
1771
|
+
if (!alignY) alignY = 'top'
|
|
1772
|
+
break
|
|
1773
|
+
case 'top-right':
|
|
1774
|
+
if (!alignX) alignX = 'right'
|
|
1775
|
+
if (!alignY) alignY = 'top'
|
|
1776
|
+
break
|
|
1777
|
+
case 'middle-left':
|
|
1778
|
+
case 'left':
|
|
1779
|
+
if (!alignX) alignX = 'left'
|
|
1780
|
+
if (!alignY) alignY = 'middle'
|
|
1781
|
+
break
|
|
1782
|
+
case 'center':
|
|
1783
|
+
case 'middle-center':
|
|
1784
|
+
if (!alignX) alignX = 'center'
|
|
1785
|
+
if (!alignY) alignY = 'middle'
|
|
1786
|
+
break
|
|
1787
|
+
case 'middle-right':
|
|
1788
|
+
case 'right':
|
|
1789
|
+
if (!alignX) alignX = 'right'
|
|
1790
|
+
if (!alignY) alignY = 'middle'
|
|
1791
|
+
break
|
|
1792
|
+
case 'bottom-left':
|
|
1793
|
+
if (!alignX) alignX = 'left'
|
|
1794
|
+
if (!alignY) alignY = 'bottom'
|
|
1795
|
+
break
|
|
1796
|
+
case 'bottom-center':
|
|
1797
|
+
case 'bottom':
|
|
1798
|
+
if (!alignX) alignX = 'center'
|
|
1799
|
+
if (!alignY) alignY = 'bottom'
|
|
1800
|
+
break
|
|
1801
|
+
case 'bottom-right':
|
|
1802
|
+
if (!alignX) alignX = 'right'
|
|
1803
|
+
if (!alignY) alignY = 'bottom'
|
|
1804
|
+
break
|
|
1756
1805
|
}
|
|
1757
1806
|
}
|
|
1758
1807
|
|
|
@@ -2056,8 +2105,7 @@ class TableManager {
|
|
|
2056
2105
|
cellShapes,
|
|
2057
2106
|
slideManager,
|
|
2058
2107
|
shapeManager,
|
|
2059
|
-
tblObj
|
|
2060
|
-
_frameObj
|
|
2108
|
+
tblObj
|
|
2061
2109
|
) {
|
|
2062
2110
|
if (!cellShapes || !shapeManager) return
|
|
2063
2111
|
|
|
@@ -2506,6 +2554,253 @@ class TableManager {
|
|
|
2506
2554
|
}
|
|
2507
2555
|
}
|
|
2508
2556
|
|
|
2557
|
+
#calculateColumnWidths(slideIndex, tableId, slideManager, tblObj, writeToXml = true) {
|
|
2558
|
+
const trsArr = tblObj['a:tr'] || []
|
|
2559
|
+
if (trsArr.length === 0) return []
|
|
2560
|
+
|
|
2561
|
+
const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
|
|
2562
|
+
const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
|
|
2563
|
+
const originalWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
|
|
2564
|
+
const totalTableWidth = originalWidths.reduce((sum, w) => sum + w, 0)
|
|
2565
|
+
const numCols = originalWidths.length
|
|
2566
|
+
if (numCols === 0) return []
|
|
2567
|
+
|
|
2568
|
+
// Helper to get paragraph font info
|
|
2569
|
+
const getParagraphFontInfo = p => {
|
|
2570
|
+
let maxSz = null
|
|
2571
|
+
let typeface = null
|
|
2572
|
+
|
|
2573
|
+
const getTypeface = pr => {
|
|
2574
|
+
if (!pr) return null
|
|
2575
|
+
if (pr['a:latin']?.['@_typeface']) return pr['a:latin']['@_typeface']
|
|
2576
|
+
if (pr['a:ea']?.['@_typeface']) return pr['a:ea']['@_typeface']
|
|
2577
|
+
if (pr['a:cs']?.['@_typeface']) return pr['a:cs']['@_typeface']
|
|
2578
|
+
return null
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
if (p['a:pPr']?.['a:defRPr']) {
|
|
2582
|
+
const defRPr = p['a:pPr']['a:defRPr']
|
|
2583
|
+
if (defRPr['@_sz']) {
|
|
2584
|
+
maxSz = parseInt(defRPr['@_sz'], 10) / 100
|
|
2585
|
+
}
|
|
2586
|
+
typeface = getTypeface(defRPr)
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
if (p['a:r']) {
|
|
2590
|
+
const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
|
|
2591
|
+
for (const r of runs) {
|
|
2592
|
+
if (r['a:rPr']) {
|
|
2593
|
+
const rPr = r['a:rPr']
|
|
2594
|
+
if (rPr['@_sz']) {
|
|
2595
|
+
const szVal = parseInt(rPr['@_sz'], 10) / 100
|
|
2596
|
+
if (maxSz === null || szVal > maxSz) {
|
|
2597
|
+
maxSz = szVal
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
const tf = getTypeface(rPr)
|
|
2601
|
+
if (tf) {
|
|
2602
|
+
typeface = tf
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
if (maxSz === null && p['a:endParaRPr']) {
|
|
2609
|
+
const endParaRPr = p['a:endParaRPr']
|
|
2610
|
+
if (endParaRPr['@_sz']) {
|
|
2611
|
+
maxSz = parseInt(endParaRPr['@_sz'], 10) / 100
|
|
2612
|
+
}
|
|
2613
|
+
const tf = getTypeface(endParaRPr)
|
|
2614
|
+
if (tf) {
|
|
2615
|
+
typeface = tf
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
return {
|
|
2620
|
+
fontSize: maxSz !== null ? maxSz : 14,
|
|
2621
|
+
typeface: typeface || 'Arial',
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
const getFontAspect = tf => {
|
|
2626
|
+
if (!tf) return 0.55
|
|
2627
|
+
const name = String(tf).toLowerCase()
|
|
2628
|
+
if (name.includes('algerian')) return 0.85
|
|
2629
|
+
return 0.55
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
const getCellMargins = cell => {
|
|
2633
|
+
const tcPr = cell['a:tcPr']
|
|
2634
|
+
const marL = tcPr?.['@_marL'] !== undefined ? parseInt(tcPr['@_marL'], 10) : 91440
|
|
2635
|
+
const marR = tcPr?.['@_marR'] !== undefined ? parseInt(tcPr['@_marR'], 10) : 91440
|
|
2636
|
+
return { marL, marR }
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
const colWeights = new Array(numCols).fill(0)
|
|
2640
|
+
const hasText = new Array(numCols).fill(false)
|
|
2641
|
+
|
|
2642
|
+
for (let c = 0; c < numCols; c++) {
|
|
2643
|
+
let maxCellWeight = 0
|
|
2644
|
+
|
|
2645
|
+
for (let r = 0; r < trsArr.length; r++) {
|
|
2646
|
+
const row = trsArr[r]
|
|
2647
|
+
const cell = row['a:tc']?.[c]
|
|
2648
|
+
if (!cell || cell['@_hMerge']) continue
|
|
2649
|
+
|
|
2650
|
+
const gridSpan = cell['@_gridSpan'] ? parseInt(cell['@_gridSpan'], 10) : 1
|
|
2651
|
+
if (gridSpan > 1) continue
|
|
2652
|
+
|
|
2653
|
+
const { marL, marR } = getCellMargins(cell)
|
|
2654
|
+
let cellTextWidth = 0
|
|
2655
|
+
|
|
2656
|
+
const txBody = cell['a:txBody']
|
|
2657
|
+
if (txBody) {
|
|
2658
|
+
const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
|
|
2659
|
+
for (const p of paras) {
|
|
2660
|
+
const fontInfo = getParagraphFontInfo(p)
|
|
2661
|
+
const aspect = getFontAspect(fontInfo.typeface)
|
|
2662
|
+
let pText = ''
|
|
2663
|
+
if (p['a:r']) {
|
|
2664
|
+
const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
|
|
2665
|
+
for (const r of runs) {
|
|
2666
|
+
if (r['a:t']) {
|
|
2667
|
+
pText += String(r['a:t'])
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
if (pText.trim()) {
|
|
2673
|
+
hasText[c] = true
|
|
2674
|
+
const words = pText.split(/\s+/)
|
|
2675
|
+
let longestWordLen = 0
|
|
2676
|
+
for (const w of words) {
|
|
2677
|
+
if (w.length > longestWordLen) longestWordLen = w.length
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
const minWordWidth = longestWordLen * fontInfo.fontSize * aspect * 9525 + marL + marR
|
|
2681
|
+
const idealChars = Math.min(30, pText.length)
|
|
2682
|
+
const idealTextWidth = idealChars * fontInfo.fontSize * aspect * 9525 + marL + marR
|
|
2683
|
+
|
|
2684
|
+
const cellIdeal = Math.max(minWordWidth, idealTextWidth)
|
|
2685
|
+
if (cellIdeal > cellTextWidth) {
|
|
2686
|
+
cellTextWidth = cellIdeal
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
const cellMin = cellTextWidth
|
|
2693
|
+
if (cellMin > maxCellWeight) {
|
|
2694
|
+
maxCellWeight = cellMin
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
colWeights[c] = maxCellWeight
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
const preferredWidths = new Array(numCols).fill(0)
|
|
2702
|
+
for (let c = 0; c < numCols; c++) {
|
|
2703
|
+
let floor = 500000
|
|
2704
|
+
if (!hasText[c]) {
|
|
2705
|
+
floor = Math.min(originalWidths[c] || 500000, 500000)
|
|
2706
|
+
}
|
|
2707
|
+
preferredWidths[c] = Math.max(colWeights[c], floor)
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
const sumPreferred = preferredWidths.reduce((sum, w) => sum + w, 0)
|
|
2711
|
+
|
|
2712
|
+
let finalWidths = [...preferredWidths]
|
|
2713
|
+
if (sumPreferred > 0) {
|
|
2714
|
+
const scale = totalTableWidth / sumPreferred
|
|
2715
|
+
finalWidths = preferredWidths.map((w, idx) => {
|
|
2716
|
+
const scaled = Math.round(w * scale)
|
|
2717
|
+
const minAllowed = Math.min(originalWidths[idx] || 300000, 300000)
|
|
2718
|
+
return Math.max(scaled, minAllowed)
|
|
2719
|
+
})
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
const sumFinal = finalWidths.reduce((sum, w) => sum + w, 0)
|
|
2723
|
+
const diff = totalTableWidth - sumFinal
|
|
2724
|
+
if (diff !== 0 && finalWidths.length > 0) {
|
|
2725
|
+
finalWidths[finalWidths.length - 1] += diff
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
if (writeToXml) {
|
|
2729
|
+
const tblGrid = tblObj['a:tblGrid']
|
|
2730
|
+
if (tblGrid) {
|
|
2731
|
+
if (Array.isArray(tblGrid['a:gridCol'])) {
|
|
2732
|
+
finalWidths.forEach((w, idx) => {
|
|
2733
|
+
if (tblGrid['a:gridCol'][idx]) {
|
|
2734
|
+
tblGrid['a:gridCol'][idx]['@_w'] = String(w)
|
|
2735
|
+
}
|
|
2736
|
+
})
|
|
2737
|
+
} else if (tblGrid['a:gridCol'] && numCols === 1) {
|
|
2738
|
+
tblGrid['a:gridCol']['@_w'] = String(finalWidths[0])
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
return finalWidths
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
#repositionAllTableCellShapes(slideIndex, tableId, slideManager, shapeManager) {
|
|
2747
|
+
if (!shapeManager) return
|
|
2748
|
+
|
|
2749
|
+
const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
|
|
2750
|
+
|
|
2751
|
+
const anchorsToReposition = []
|
|
2752
|
+
for (const [name, anchor] of this.#cellShapeAnchors) {
|
|
2753
|
+
if (anchor.slideIndex === slideIndex && anchor.resolvedTableId === resolvedTableId) {
|
|
2754
|
+
anchorsToReposition.push({ name, anchor })
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
anchorsToReposition.sort((a, b) => {
|
|
2759
|
+
if (a.anchor.rowIndex !== b.anchor.rowIndex) {
|
|
2760
|
+
return a.anchor.rowIndex - b.anchor.rowIndex
|
|
2761
|
+
}
|
|
2762
|
+
if (a.anchor.colIndex !== b.anchor.colIndex) {
|
|
2763
|
+
return a.anchor.colIndex - b.anchor.colIndex
|
|
2764
|
+
}
|
|
2765
|
+
const aBase =
|
|
2766
|
+
typeof a.anchor.shapeIndex === 'string'
|
|
2767
|
+
? parseInt(a.anchor.shapeIndex.split('_')[0], 10)
|
|
2768
|
+
: a.anchor.shapeIndex
|
|
2769
|
+
const bBase =
|
|
2770
|
+
typeof b.anchor.shapeIndex === 'string'
|
|
2771
|
+
? parseInt(b.anchor.shapeIndex.split('_')[0], 10)
|
|
2772
|
+
: b.anchor.shapeIndex
|
|
2773
|
+
return aBase - bBase
|
|
2774
|
+
})
|
|
2775
|
+
|
|
2776
|
+
for (const { name } of anchorsToReposition) {
|
|
2777
|
+
try {
|
|
2778
|
+
shapeManager.deleteShape(slideIndex, name, slideManager)
|
|
2779
|
+
} catch (e) {
|
|
2780
|
+
logger.warn(`Failed to delete cell shape "${name}" during table reposition: ${e.message}`)
|
|
2781
|
+
}
|
|
2782
|
+
this.#cellShapeAnchors.delete(name)
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
for (const { anchor } of anchorsToReposition) {
|
|
2786
|
+
try {
|
|
2787
|
+
this.addCellShape(
|
|
2788
|
+
slideIndex,
|
|
2789
|
+
anchor.tableId,
|
|
2790
|
+
anchor.rowIndex,
|
|
2791
|
+
anchor.colIndex,
|
|
2792
|
+
anchor.config,
|
|
2793
|
+
slideManager,
|
|
2794
|
+
shapeManager
|
|
2795
|
+
)
|
|
2796
|
+
} catch (e) {
|
|
2797
|
+
logger.warn(
|
|
2798
|
+
`Failed to reposition cell shape for (${anchor.rowIndex}, ${anchor.colIndex}): ${e.message}`
|
|
2799
|
+
)
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2509
2804
|
/**
|
|
2510
2805
|
* Generates a new rowId for the given row object.
|
|
2511
2806
|
*/
|
|
@@ -2555,35 +2850,76 @@ class TableManager {
|
|
|
2555
2850
|
const numRows = trsArr.length
|
|
2556
2851
|
const numCols = colWidths.length
|
|
2557
2852
|
|
|
2558
|
-
// Initialize rowHeights with original height or a safe minimum floor of 228600 EMUs (~24px/pt)
|
|
2559
2853
|
const rowHeights = trsArr.map(row => {
|
|
2560
2854
|
const h = parseInt(row['@_h'] || 0, 10)
|
|
2561
2855
|
return h > 0 ? h : 228600
|
|
2562
2856
|
})
|
|
2563
2857
|
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
let
|
|
2567
|
-
|
|
2568
|
-
|
|
2858
|
+
const getParagraphFontInfo = p => {
|
|
2859
|
+
let maxSz = null
|
|
2860
|
+
let typeface = null
|
|
2861
|
+
|
|
2862
|
+
const getTypeface = pr => {
|
|
2863
|
+
if (!pr) return null
|
|
2864
|
+
if (pr['a:latin']?.['@_typeface']) return pr['a:latin']['@_typeface']
|
|
2865
|
+
if (pr['a:ea']?.['@_typeface']) return pr['a:ea']['@_typeface']
|
|
2866
|
+
if (pr['a:cs']?.['@_typeface']) return pr['a:cs']['@_typeface']
|
|
2867
|
+
return null
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
if (p['a:pPr']?.['a:defRPr']) {
|
|
2871
|
+
const defRPr = p['a:pPr']['a:defRPr']
|
|
2872
|
+
if (defRPr['@_sz']) {
|
|
2873
|
+
maxSz = parseInt(defRPr['@_sz'], 10) / 100
|
|
2874
|
+
}
|
|
2875
|
+
typeface = getTypeface(defRPr)
|
|
2569
2876
|
}
|
|
2877
|
+
|
|
2570
2878
|
if (p['a:r']) {
|
|
2571
2879
|
const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
|
|
2572
2880
|
for (const r of runs) {
|
|
2573
|
-
if (r['a:rPr']
|
|
2574
|
-
const
|
|
2575
|
-
if (
|
|
2576
|
-
|
|
2881
|
+
if (r['a:rPr']) {
|
|
2882
|
+
const rPr = r['a:rPr']
|
|
2883
|
+
if (rPr['@_sz']) {
|
|
2884
|
+
const szVal = parseInt(rPr['@_sz'], 10) / 100
|
|
2885
|
+
if (maxSz === null || szVal > maxSz) {
|
|
2886
|
+
maxSz = szVal
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
const tf = getTypeface(rPr)
|
|
2890
|
+
if (tf) {
|
|
2891
|
+
typeface = tf
|
|
2577
2892
|
}
|
|
2578
2893
|
}
|
|
2579
2894
|
}
|
|
2580
2895
|
}
|
|
2581
|
-
|
|
2896
|
+
|
|
2897
|
+
if (maxSz === null && p['a:endParaRPr']) {
|
|
2898
|
+
const endParaRPr = p['a:endParaRPr']
|
|
2899
|
+
if (endParaRPr['@_sz']) {
|
|
2900
|
+
maxSz = parseInt(endParaRPr['@_sz'], 10) / 100
|
|
2901
|
+
}
|
|
2902
|
+
const tf = getTypeface(endParaRPr)
|
|
2903
|
+
if (tf) {
|
|
2904
|
+
typeface = tf
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
return {
|
|
2909
|
+
fontSize: maxSz !== null ? maxSz : 14,
|
|
2910
|
+
typeface: typeface || 'Arial',
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
const getFontAspect = tf => {
|
|
2915
|
+
if (!tf) return 0.55
|
|
2916
|
+
const name = String(tf).toLowerCase()
|
|
2917
|
+
if (name.includes('algerian')) return 0.85
|
|
2918
|
+
return 0.55
|
|
2582
2919
|
}
|
|
2583
2920
|
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
const charWidth = fontSize * 0.65
|
|
2921
|
+
const wrapText = (text, availWidth_px, fontSize, aspect = 0.55) => {
|
|
2922
|
+
const charWidth = fontSize * aspect
|
|
2587
2923
|
const words = text.split(/(\s+)/)
|
|
2588
2924
|
let linesCount = 0
|
|
2589
2925
|
let currentLineLen = 0
|
|
@@ -2616,7 +2952,6 @@ class TableManager {
|
|
|
2616
2952
|
return linesCount
|
|
2617
2953
|
}
|
|
2618
2954
|
|
|
2619
|
-
// Helper to get cell margins
|
|
2620
2955
|
const getCellMargins = cell => {
|
|
2621
2956
|
const tcPr = cell['a:tcPr']
|
|
2622
2957
|
const marL = tcPr?.['@_marL'] !== undefined ? parseInt(tcPr['@_marL'], 10) : 91440
|
|
@@ -2626,7 +2961,6 @@ class TableManager {
|
|
|
2626
2961
|
return { marL, marR, marT, marB }
|
|
2627
2962
|
}
|
|
2628
2963
|
|
|
2629
|
-
// Calculate required height for each cell
|
|
2630
2964
|
const cellHeights = Array.from({ length: numRows }, () => new Array(numCols).fill(0))
|
|
2631
2965
|
|
|
2632
2966
|
for (let r = 0; r < numRows; r++) {
|
|
@@ -2639,7 +2973,6 @@ class TableManager {
|
|
|
2639
2973
|
const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
|
|
2640
2974
|
const gridSpan = cell['@_gridSpan'] ? parseInt(cell['@_gridSpan'], 10) : 1
|
|
2641
2975
|
|
|
2642
|
-
// Calculate cell width
|
|
2643
2976
|
let cellWidth = 0
|
|
2644
2977
|
for (let idx = 0; idx < gridSpan; idx++) {
|
|
2645
2978
|
cellWidth += colWidths[parent.col + idx] || 0
|
|
@@ -2649,13 +2982,15 @@ class TableManager {
|
|
|
2649
2982
|
const availWidth = cellWidth - marL - marR
|
|
2650
2983
|
const availWidth_px = Math.max(1, availWidth / 9525)
|
|
2651
2984
|
|
|
2652
|
-
// Calculate text height
|
|
2653
2985
|
const txBody = cell['a:txBody']
|
|
2654
2986
|
let textHeight_emu = 0
|
|
2655
2987
|
if (txBody) {
|
|
2656
2988
|
const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
|
|
2657
2989
|
for (const p of paras) {
|
|
2658
|
-
const
|
|
2990
|
+
const fontInfo = getParagraphFontInfo(p)
|
|
2991
|
+
const fontSize = fontInfo.fontSize
|
|
2992
|
+
const aspect = getFontAspect(fontInfo.typeface)
|
|
2993
|
+
|
|
2659
2994
|
let pText = ''
|
|
2660
2995
|
if (p['a:r']) {
|
|
2661
2996
|
const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
|
|
@@ -2666,8 +3001,8 @@ class TableManager {
|
|
|
2666
3001
|
}
|
|
2667
3002
|
}
|
|
2668
3003
|
|
|
2669
|
-
const linesCount = wrapText(pText, availWidth_px, fontSize)
|
|
2670
|
-
const lineHeight_emu = fontSize * 20780
|
|
3004
|
+
const linesCount = wrapText(pText, availWidth_px, fontSize, aspect)
|
|
3005
|
+
const lineHeight_emu = fontSize * 20780
|
|
2671
3006
|
|
|
2672
3007
|
let pHeight_emu = linesCount * lineHeight_emu
|
|
2673
3008
|
if (p['a:pPr']?.['a:spcBef']?.['a:spcPts']?.['@_val']) {
|
|
@@ -2687,10 +3022,8 @@ class TableManager {
|
|
|
2687
3022
|
}
|
|
2688
3023
|
}
|
|
2689
3024
|
|
|
2690
|
-
// Now resolve row heights based on required cell heights
|
|
2691
|
-
// First, non-vertically-merged cells define row heights directly
|
|
2692
3025
|
for (let r = 0; r < numRows; r++) {
|
|
2693
|
-
let maxCellHeight = rowHeights[r]
|
|
3026
|
+
let maxCellHeight = rowHeights[r]
|
|
2694
3027
|
const row = trsArr[r]
|
|
2695
3028
|
const tcs = row['a:tc'] || []
|
|
2696
3029
|
for (let c = 0; c < numCols; c++) {
|
|
@@ -2706,7 +3039,6 @@ class TableManager {
|
|
|
2706
3039
|
rowHeights[r] = maxCellHeight
|
|
2707
3040
|
}
|
|
2708
3041
|
|
|
2709
|
-
// Next, adjust for vertically merged cells (rowSpan > 1)
|
|
2710
3042
|
for (let r = 0; r < numRows; r++) {
|
|
2711
3043
|
const row = trsArr[r]
|
|
2712
3044
|
const tcs = row['a:tc'] || []
|
|
@@ -2716,13 +3048,11 @@ class TableManager {
|
|
|
2716
3048
|
const rowSpan = cell['@_rowSpan'] ? parseInt(cell['@_rowSpan'], 10) : 1
|
|
2717
3049
|
if (rowSpan > 1) {
|
|
2718
3050
|
const reqHeight = cellHeights[r][c]
|
|
2719
|
-
// Sum currently allocated row heights for spanned rows
|
|
2720
3051
|
let currentSpanHeight = 0
|
|
2721
3052
|
for (let idx = 0; idx < rowSpan; idx++) {
|
|
2722
3053
|
currentSpanHeight += rowHeights[r + idx] || 0
|
|
2723
3054
|
}
|
|
2724
3055
|
if (reqHeight > currentSpanHeight) {
|
|
2725
|
-
// Distribute the extra required height equally across all spanned rows
|
|
2726
3056
|
const diff = reqHeight - currentSpanHeight
|
|
2727
3057
|
const extraPerRow = Math.ceil(diff / rowSpan)
|
|
2728
3058
|
for (let idx = 0; idx < rowSpan; idx++) {
|
|
@@ -2733,13 +3063,11 @@ class TableManager {
|
|
|
2733
3063
|
}
|
|
2734
3064
|
}
|
|
2735
3065
|
|
|
2736
|
-
// Update row heights in XML
|
|
2737
3066
|
if (writeToXml) {
|
|
2738
3067
|
for (let r = 0; r < numRows; r++) {
|
|
2739
3068
|
trsArr[r]['@_h'] = String(rowHeights[r])
|
|
2740
3069
|
}
|
|
2741
3070
|
}
|
|
2742
|
-
|
|
2743
3071
|
return rowHeights
|
|
2744
3072
|
}
|
|
2745
3073
|
|