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 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 status icon centered in a merged cell (rowSpan=2, colSpan=2)
188
- await ppt.addCellShape('StatusTable', 1, 1, {
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', // Green status dot
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 explicit offsets inside a merged cell
198
- await ppt.addCellShape('StatusTable', 1, 1, {
186
+ // Add a badge with text and custom offsets
187
+ await ppt.addCellShape('SalesTable', 1, 2, {
199
188
  type: 'badge',
200
- text: 'Urgent',
201
- fill: '#EF4444',
202
- alignX: 'left',
203
- alignY: 'top',
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.4",
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 && !options.overwrite) {
477
- throw new PPTXError(
478
- `Destination directory "${outputPath}" is not empty. Set overwrite: true to overwrite.`
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, nested arrays
1573
- * for rowspan-merged cells, and **Shape Cell** configuration objects that insert
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>|Object>} rowData - Row data. Each element may be
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, frameObj, resolvedTableId } = this.#getTableContext(
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(slideIndex, tableId, rowIndex, colIndex, value, options = {}, slideManager) {
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 gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
1130
- const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
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 = undefined
1723
- let alignY = undefined
1724
-
1725
- const pos = String(config.position || '')
1726
- .toLowerCase()
1727
- .trim()
1728
- if (pos) {
1729
- if (pos === 'left') {
1730
- alignX = 'left'
1731
- alignY = 'middle'
1732
- } else if (pos === 'right') {
1733
- alignX = 'right'
1734
- alignY = 'middle'
1735
- } else if (pos === 'center' || pos === 'middle') {
1736
- alignX = 'center'
1737
- alignY = 'middle'
1738
- } else if (pos === 'top') {
1739
- alignX = 'center'
1740
- alignY = 'top'
1741
- } else if (pos === 'bottom') {
1742
- alignX = 'center'
1743
- alignY = 'bottom'
1744
- } else if (pos === 'top-left') {
1745
- alignX = 'left'
1746
- alignY = 'top'
1747
- } else if (pos === 'top-right') {
1748
- alignX = 'right'
1749
- alignY = 'top'
1750
- } else if (pos === 'bottom-left') {
1751
- alignX = 'left'
1752
- alignY = 'bottom'
1753
- } else if (pos === 'bottom-right') {
1754
- alignX = 'right'
1755
- alignY = 'bottom'
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
- // Helper to get paragraph font size
2565
- const getParagraphFontSize = p => {
2566
- let maxSz = 14 // default 14pt
2567
- if (p['a:pPr']?.['a:defRPr']?.['@_sz']) {
2568
- maxSz = parseInt(p['a:pPr']['a:defRPr']['@_sz'], 10) / 100
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']?.['@_sz']) {
2574
- const szVal = parseInt(r['a:rPr']['@_sz'], 10) / 100
2575
- if (szVal > maxSz) {
2576
- maxSz = szVal
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
- return maxSz
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
- // Helper to wrap text
2585
- const wrapText = (text, availWidth_px, fontSize) => {
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 fontSize = getParagraphFontSize(p)
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 // 1.4 line height multiplier
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] // Start with original template height as floor
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