node-pptx-templater 1.1.1 → 1.1.3

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,6 +5,22 @@ 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
+
8
24
  ## [1.1.0] - 2026-06-12
9
25
 
10
26
  ### Added
package/README.md CHANGED
@@ -172,27 +172,51 @@ 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. Offsets (`x`, `y`) are relative to the cell's top-left corner. Oversized shapes are scaled down proportionally to fit inside the cell.
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).
176
185
 
177
186
  ```javascript
178
- // Add a simple indicator
179
- await ppt.addCellShape('SalesTable', 2, 1, {
187
+ // Add a status icon centered in a merged cell (rowSpan=2, colSpan=2)
188
+ await ppt.addCellShape('StatusTable', 1, 1, {
180
189
  type: 'circle',
181
190
  width: 12,
182
191
  height: 12,
183
- fill: '#10B981' // Green status dot
192
+ fill: '#10B981', // Green status dot
193
+ alignX: 'center',
194
+ alignY: 'middle' // Visually centered in the merged cell
184
195
  });
185
196
 
186
- // Add a badge with text and custom offsets
187
- await ppt.addCellShape('SalesTable', 1, 2, {
197
+ // Add a badge with text and explicit offsets inside a merged cell
198
+ await ppt.addCellShape('StatusTable', 1, 1, {
188
199
  type: 'badge',
189
- text: 'Active',
190
- fill: '#3B82F6',
191
- x: 4,
192
- y: 2,
200
+ text: 'Urgent',
201
+ fill: '#EF4444',
202
+ alignX: 'left',
203
+ alignY: 'top',
204
+ x: 5,
205
+ y: 3,
193
206
  width: 50,
194
207
  height: 16
195
208
  });
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
+ });
196
220
  ```
197
221
 
198
222
  ### XML Folder Workflow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-pptx-templater",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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,6 +37,7 @@
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:all": "node scripts/run-examples-check.js",
40
41
  "prepublishOnly": "npm run lint && npm run test"
41
42
  },
42
43
  "keywords": [
@@ -161,11 +161,11 @@ class OutputWriter {
161
161
  : e.compressionMethod === 0
162
162
  ? 'STORE'
163
163
  : `UNKNOWN(${e.compressionMethod})`
164
- console.log(e.name)
165
- console.log(`compressed: ${e.compressedSize}`)
166
- console.log(`uncompressed: ${e.uncompressedSize}`)
167
- console.log(`crc: ${e.crc32}`)
168
- console.log(`method: ${methodStr}`)
164
+ logger.info(e.name)
165
+ logger.info(`compressed: ${e.compressedSize}`)
166
+ logger.info(`uncompressed: ${e.uncompressedSize}`)
167
+ logger.info(`crc: ${e.crc32}`)
168
+ logger.info(`method: ${methodStr}`)
169
169
  })
170
170
  logger.info('--- End of ZIP debug output ---')
171
171
  }
@@ -2654,6 +2654,16 @@ class PPTXTemplater {
2654
2654
  return this.#shapeManager.validateShape(options)
2655
2655
  }
2656
2656
 
2657
+ /**
2658
+ * Adds a new shape to the targeted slide(s).
2659
+ *
2660
+ * @param {string|Object} typeOrOptions Either the shape type string (e.g., 'rect', 'ellipse') or a full options object.
2661
+ * @param {Object} [options={}] Additional configuration options if type was specified as a string.
2662
+ * @returns {Promise<PPTXTemplater>} The templater instance for chaining.
2663
+ *
2664
+ * @example
2665
+ * await ppt.addShape('rect', { id: 'MyShape', x: 100, y: 100, width: 200, height: 100 });
2666
+ */
2657
2667
  async addShape(typeOrOptions, options = {}) {
2658
2668
  this.#assertLoaded()
2659
2669
  let resolvedOptions = {}
@@ -2727,6 +2737,8 @@ class PPTXTemplater {
2727
2737
 
2728
2738
  /**
2729
2739
  * Dynamically adds a shape inside a table cell based on cell coordinates.
2740
+ * Cell shapes are overlay graphics anchored independently of the table layout,
2741
+ * and adding a cell shape never modifies row heights, column widths, or table dimensions.
2730
2742
  *
2731
2743
  * @param {string} tableId - Table name or shape ID.
2732
2744
  * @param {number} rowIndex - 0-based row index.
@@ -2736,14 +2748,28 @@ class PPTXTemplater {
2736
2748
  */
2737
2749
  async addCellShape(tableId, rowIndex, colIndex, options) {
2738
2750
  this.#assertLoaded()
2751
+ let row = rowIndex
2752
+ let col = colIndex
2753
+ let opts = options
2754
+ if (rowIndex && typeof rowIndex === 'object') {
2755
+ row = rowIndex.row !== undefined ? rowIndex.row : rowIndex.rowIndex
2756
+ col =
2757
+ rowIndex.column !== undefined
2758
+ ? rowIndex.column
2759
+ : rowIndex.col !== undefined
2760
+ ? rowIndex.col
2761
+ : rowIndex.colIndex
2762
+ opts = rowIndex
2763
+ }
2764
+
2739
2765
  const targetIndices = this.#getTargetSlideIndices()
2740
2766
  for (const idx of targetIndices) {
2741
2767
  this.#tableManager.addCellShape(
2742
2768
  idx,
2743
2769
  tableId,
2744
- rowIndex,
2745
- colIndex,
2746
- options,
2770
+ row,
2771
+ col,
2772
+ opts,
2747
2773
  this.#slideManager,
2748
2774
  this.#shapeManager
2749
2775
  )
@@ -2763,15 +2789,36 @@ class PPTXTemplater {
2763
2789
  */
2764
2790
  async updateCellShape(tableId, rowIndex, colIndex, shapeIndex, options) {
2765
2791
  this.#assertLoaded()
2792
+ let row = rowIndex
2793
+ let col = colIndex
2794
+ let shpIdx = shapeIndex
2795
+ let opts = options
2796
+ if (rowIndex && typeof rowIndex === 'object') {
2797
+ row = rowIndex.row !== undefined ? rowIndex.row : rowIndex.rowIndex
2798
+ col =
2799
+ rowIndex.column !== undefined
2800
+ ? rowIndex.column
2801
+ : rowIndex.col !== undefined
2802
+ ? rowIndex.col
2803
+ : rowIndex.colIndex
2804
+ shpIdx =
2805
+ rowIndex.shapeIndex !== undefined
2806
+ ? rowIndex.shapeIndex
2807
+ : rowIndex.shape !== undefined
2808
+ ? rowIndex.shape
2809
+ : colIndex
2810
+ opts = options !== undefined ? options : rowIndex
2811
+ }
2812
+
2766
2813
  const targetIndices = this.#getTargetSlideIndices()
2767
2814
  for (const idx of targetIndices) {
2768
2815
  this.#tableManager.updateCellShape(
2769
2816
  idx,
2770
2817
  tableId,
2771
- rowIndex,
2772
- colIndex,
2773
- shapeIndex,
2774
- options,
2818
+ row,
2819
+ col,
2820
+ shpIdx,
2821
+ opts,
2775
2822
  this.#slideManager,
2776
2823
  this.#shapeManager
2777
2824
  )
@@ -2790,14 +2837,33 @@ class PPTXTemplater {
2790
2837
  */
2791
2838
  async removeCellShape(tableId, rowIndex, colIndex, shapeIndex) {
2792
2839
  this.#assertLoaded()
2840
+ let row = rowIndex
2841
+ let col = colIndex
2842
+ let shpIdx = shapeIndex
2843
+ if (rowIndex && typeof rowIndex === 'object') {
2844
+ row = rowIndex.row !== undefined ? rowIndex.row : rowIndex.rowIndex
2845
+ col =
2846
+ rowIndex.column !== undefined
2847
+ ? rowIndex.column
2848
+ : rowIndex.col !== undefined
2849
+ ? rowIndex.col
2850
+ : rowIndex.colIndex
2851
+ shpIdx =
2852
+ rowIndex.shapeIndex !== undefined
2853
+ ? rowIndex.shapeIndex
2854
+ : rowIndex.shape !== undefined
2855
+ ? rowIndex.shape
2856
+ : colIndex
2857
+ }
2858
+
2793
2859
  const targetIndices = this.#getTargetSlideIndices()
2794
2860
  for (const idx of targetIndices) {
2795
2861
  this.#tableManager.removeCellShape(
2796
2862
  idx,
2797
2863
  tableId,
2798
- rowIndex,
2799
- colIndex,
2800
- shapeIndex,
2864
+ row,
2865
+ col,
2866
+ shpIdx,
2801
2867
  this.#slideManager,
2802
2868
  this.#shapeManager
2803
2869
  )
@@ -2816,14 +2882,33 @@ class PPTXTemplater {
2816
2882
  */
2817
2883
  getCellShape(tableId, rowIndex, colIndex, shapeIndex) {
2818
2884
  this.#assertLoaded()
2885
+ let row = rowIndex
2886
+ let col = colIndex
2887
+ let shpIdx = shapeIndex
2888
+ if (rowIndex && typeof rowIndex === 'object') {
2889
+ row = rowIndex.row !== undefined ? rowIndex.row : rowIndex.rowIndex
2890
+ col =
2891
+ rowIndex.column !== undefined
2892
+ ? rowIndex.column
2893
+ : rowIndex.col !== undefined
2894
+ ? rowIndex.col
2895
+ : rowIndex.colIndex
2896
+ shpIdx =
2897
+ rowIndex.shapeIndex !== undefined
2898
+ ? rowIndex.shapeIndex
2899
+ : rowIndex.shape !== undefined
2900
+ ? rowIndex.shape
2901
+ : colIndex
2902
+ }
2903
+
2819
2904
  const targetIndices = this.#getTargetSlideIndices()
2820
2905
  for (const idx of targetIndices) {
2821
2906
  const shape = this.#tableManager.getCellShape(
2822
2907
  idx,
2823
2908
  tableId,
2824
- rowIndex,
2825
- colIndex,
2826
- shapeIndex,
2909
+ row,
2910
+ col,
2911
+ shpIdx,
2827
2912
  this.#slideManager,
2828
2913
  this.#shapeManager
2829
2914
  )
@@ -2846,16 +2931,22 @@ class PPTXTemplater {
2846
2931
  typeof tableIdOrObj === 'object'
2847
2932
  ? tableIdOrObj.id || tableIdOrObj.name || tableIdOrObj.tableId
2848
2933
  : tableIdOrObj
2934
+ let row = rowIndex
2935
+ let col = colIndex
2936
+ if (rowIndex && typeof rowIndex === 'object') {
2937
+ row = rowIndex.row !== undefined ? rowIndex.row : rowIndex.rowIndex
2938
+ col =
2939
+ rowIndex.column !== undefined
2940
+ ? rowIndex.column
2941
+ : rowIndex.col !== undefined
2942
+ ? rowIndex.col
2943
+ : rowIndex.colIndex
2944
+ }
2945
+
2849
2946
  const targetIndices = this.#getTargetSlideIndices()
2850
2947
  for (const idx of targetIndices) {
2851
2948
  try {
2852
- const bounds = this.#tableManager.getCellBounds(
2853
- idx,
2854
- tableId,
2855
- rowIndex,
2856
- colIndex,
2857
- this.#slideManager
2858
- )
2949
+ const bounds = this.#tableManager.getCellBounds(idx, tableId, row, col, this.#slideManager)
2859
2950
  if (bounds) return bounds
2860
2951
  } catch (err) {
2861
2952
  logger.debug(
@@ -2883,17 +2974,33 @@ class PPTXTemplater {
2883
2974
  typeof tableIdOrObj === 'object'
2884
2975
  ? tableIdOrObj.id || tableIdOrObj.name || tableIdOrObj.tableId
2885
2976
  : tableIdOrObj
2977
+ let row = rowIndex
2978
+ let col = colIndex
2979
+ let widthOrOpts = shapeWidthOrOptions
2980
+ let height = shapeHeight
2981
+ if (rowIndex && typeof rowIndex === 'object') {
2982
+ row = rowIndex.row !== undefined ? rowIndex.row : rowIndex.rowIndex
2983
+ col =
2984
+ rowIndex.column !== undefined
2985
+ ? rowIndex.column
2986
+ : rowIndex.col !== undefined
2987
+ ? rowIndex.col
2988
+ : rowIndex.colIndex
2989
+ widthOrOpts = colIndex
2990
+ height = shapeWidthOrOptions
2991
+ }
2992
+
2886
2993
  const targetIndices = this.#getTargetSlideIndices()
2887
2994
  for (const idx of targetIndices) {
2888
2995
  try {
2889
2996
  const pos = this.#tableManager.getCellPosition(
2890
2997
  idx,
2891
2998
  tableId,
2892
- rowIndex,
2893
- colIndex,
2999
+ row,
3000
+ col,
2894
3001
  this.#slideManager,
2895
- shapeWidthOrOptions,
2896
- shapeHeight
3002
+ widthOrOpts,
3003
+ height
2897
3004
  )
2898
3005
  if (pos) return pos
2899
3006
  } catch (err) {
@@ -237,6 +237,18 @@ class ValidationEngine {
237
237
  if (!ppt.zipManager.hasFile(resolved)) {
238
238
  errors.push(`Relationship ${rel.id} points to non-existent file: ${resolved}`)
239
239
  }
240
+
241
+ // Catch redundant relative path traversals (e.g., '../slides/slide1.xml' from 'ppt/slides/slide2.xml')
242
+ const sourceDir = partPath.split('/').slice(0, -1).join('/')
243
+ const resolvedDir = resolved.split('/').slice(0, -1).join('/')
244
+ if (sourceDir === resolvedDir) {
245
+ const expectedTarget = resolved.split('/').pop()
246
+ if (rel.target !== expectedTarget) {
247
+ errors.push(
248
+ `Relationship "${rel.id}" in ${relsPath} uses redundant relative target "${rel.target}" (expected "${expectedTarget}")`
249
+ )
250
+ }
251
+ }
240
252
  }
241
253
  }
242
254
 
@@ -100,7 +100,7 @@ class HyperlinkManager {
100
100
  const targetInfo = slideManager.getSlideInfo(targetSlideIndex)
101
101
 
102
102
  // Build relative target path from source to target slide
103
- const relativePath = `../slides/${targetInfo.zipPath.split('/').pop()}`
103
+ const relativePath = targetInfo.zipPath.split('/').pop()
104
104
 
105
105
  // Add relationship pointing to the target slide
106
106
  const rId = relationshipManager.addRelationship(
@@ -130,7 +130,7 @@ class HyperlinkManager {
130
130
  const sourceInfo = slideManager.getSlideInfo(sourceSlideIndex)
131
131
  const targetInfo = slideManager.getSlideInfo(targetSlideIndex)
132
132
 
133
- const relativePath = `../slides/${targetInfo.zipPath.split('/').pop()}`
133
+ const relativePath = targetInfo.zipPath.split('/').pop()
134
134
  const rId = relationshipManager.addRelationship(
135
135
  sourceInfo.zipPath,
136
136
  REL_TYPES.SLIDE,
@@ -167,7 +167,7 @@ class HyperlinkManager {
167
167
  const sourceInfo = slideManager.getSlideInfo(sourceSlideIndex)
168
168
  const targetInfo = slideManager.getSlideInfo(targetSlideIndex)
169
169
 
170
- const relativePath = `../slides/${targetInfo.zipPath.split('/').pop()}`
170
+ const relativePath = targetInfo.zipPath.split('/').pop()
171
171
  const rId = relationshipManager.addRelationship(
172
172
  sourceInfo.zipPath,
173
173
  REL_TYPES.SLIDE,
@@ -223,7 +223,7 @@ class TableManager {
223
223
  )
224
224
  }
225
225
 
226
- this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
226
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, true)
227
227
 
228
228
  if (cellShapes) {
229
229
  this.#processCellShapes(
@@ -796,8 +796,6 @@ class TableManager {
796
796
  getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager) {
797
797
  const { tblObj, frameObj } = this.#getTableContext(slideIndex, tableId, slideManager)
798
798
 
799
- this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
800
-
801
799
  const xfrm = frameObj['p:xfrm']
802
800
  const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
803
801
  const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
@@ -807,7 +805,7 @@ class TableManager {
807
805
  const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
808
806
 
809
807
  const trsArr = tblObj['a:tr'] || []
810
- const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
808
+ const rowHeights = this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj, false)
811
809
 
812
810
  const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
813
811
  const pr = parent.row
@@ -1304,10 +1302,10 @@ class TableManager {
1304
1302
  }
1305
1303
 
1306
1304
  #expandCellShape(config, cellBounds) {
1307
- const cellLeft_px = Math.round(cellBounds.left / 9525)
1308
- const cellTop_px = Math.round(cellBounds.top / 9525)
1309
- const cellWidth_px = Math.round(cellBounds.width / 9525)
1310
- const cellHeight_px = Math.round(cellBounds.height / 9525)
1305
+ const cellLeft_px = cellBounds.x
1306
+ const cellTop_px = cellBounds.y
1307
+ const cellWidth_px = cellBounds.width
1308
+ const cellHeight_px = cellBounds.height
1311
1309
 
1312
1310
  const parseLength = (val, maxVal) => {
1313
1311
  if (typeof val === 'string' && val.endsWith('%')) {
@@ -1745,7 +1743,7 @@ class TableManager {
1745
1743
  slideManager,
1746
1744
  shapeManager,
1747
1745
  tblObj,
1748
- frameObj
1746
+ _frameObj
1749
1747
  ) {
1750
1748
  if (!cellShapes || !shapeManager) return
1751
1749
 
@@ -1763,62 +1761,16 @@ class TableManager {
1763
1761
  }
1764
1762
  }
1765
1763
 
1766
- const xfrm = frameObj['p:xfrm']
1767
- const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
1768
- const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
1769
-
1770
- const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
1771
- const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
1772
- const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
1773
-
1774
- const trsArr = tblObj['a:tr'] || []
1775
- const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
1776
-
1777
- const getCellBounds = (r, c) => {
1778
- const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
1779
- const pr = parent.row
1780
- const pc = parent.col
1781
-
1782
- let cellLeft = tableX
1783
- for (let idx = 0; idx < pc; idx++) {
1784
- cellLeft += colWidths[idx] || 0
1785
- }
1786
-
1787
- let cellTop = tableY
1788
- for (let idx = 0; idx < pr; idx++) {
1789
- cellTop += rowHeights[idx] || 0
1790
- }
1791
-
1792
- const parentCell = trsArr[pr]?.['a:tc']?.[pc]
1793
- const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
1794
- const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
1795
-
1796
- let cellWidth = 0
1797
- for (let idx = 0; idx < gridSpan; idx++) {
1798
- cellWidth += colWidths[pc + idx] || 0
1799
- }
1800
-
1801
- let cellHeight = 0
1802
- for (let idx = 0; idx < rowSpan; idx++) {
1803
- cellHeight += rowHeights[pr + idx] || 0
1804
- }
1805
-
1806
- return {
1807
- left: cellLeft,
1808
- top: cellTop,
1809
- width: cellWidth,
1810
- height: cellHeight,
1811
- }
1812
- }
1813
-
1814
1764
  const shapesToCreate = []
1815
- const headerNames = (trsArr[0]?.['a:tc'] || []).map(cell => this.#getCellText(cell).trim())
1765
+ const headerNames = (tblObj['a:tr']?.[0]?.['a:tc'] || []).map(cell =>
1766
+ this.#getCellText(cell).trim()
1767
+ )
1816
1768
 
1817
1769
  for (let i = 0; i < rowsData.length; i++) {
1818
1770
  const rowData = rowsData[i]
1819
1771
  const finalRowIndex = isObjectRows ? i + 1 : i
1820
1772
 
1821
- const numCols = trsArr[finalRowIndex]?.['a:tc']?.length || 0
1773
+ const numCols = tblObj['a:tr']?.[finalRowIndex]?.['a:tc']?.length || 0
1822
1774
  for (let j = 0; j < numCols; j++) {
1823
1775
  const headerName = headerNames[j]
1824
1776
  let shapeFn = null
@@ -1853,16 +1805,24 @@ class TableManager {
1853
1805
  shapesToCreate.sort((a, b) => (a.config.zIndex || 0) - (b.config.zIndex || 0))
1854
1806
 
1855
1807
  shapesToCreate.forEach(item => {
1856
- const bounds = getCellBounds(item.rowIndex, item.colIndex)
1857
- const expandedConfigs = this.#expandCellShape(item.config, bounds)
1808
+ const bounds = this.getCellBounds(
1809
+ slideIndex,
1810
+ tableId,
1811
+ item.rowIndex,
1812
+ item.colIndex,
1813
+ slideManager
1814
+ )
1815
+ if (bounds) {
1816
+ const expandedConfigs = this.#expandCellShape(item.config, bounds)
1858
1817
 
1859
- expandedConfigs.forEach((expandedConfig, expIdx) => {
1860
- const finalShapeIndex =
1861
- expandedConfigs.length > 1 ? `${item.shapeIndex}_${expIdx}` : item.shapeIndex
1862
- expandedConfig.id = `cellshape_${resolvedTableId}_${item.rowIndex}_${item.colIndex}_${finalShapeIndex}`
1818
+ expandedConfigs.forEach((expandedConfig, expIdx) => {
1819
+ const finalShapeIndex =
1820
+ expandedConfigs.length > 1 ? `${item.shapeIndex}_${expIdx}` : item.shapeIndex
1821
+ expandedConfig.id = `cellshape_${resolvedTableId}_${item.rowIndex}_${item.colIndex}_${finalShapeIndex}`
1863
1822
 
1864
- shapeManager.addShape(slideIndex, expandedConfig, slideManager)
1865
- })
1823
+ shapeManager.addShape(slideIndex, expandedConfig, slideManager)
1824
+ })
1825
+ }
1866
1826
  })
1867
1827
  }
1868
1828
 
@@ -1930,55 +1890,13 @@ class TableManager {
1930
1890
  }
1931
1891
 
1932
1892
  addCellShape(slideIndex, tableId, rowIndex, colIndex, options, slideManager, shapeManager) {
1933
- const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
1934
- slideIndex,
1935
- tableId,
1936
- slideManager
1937
- )
1938
-
1939
- this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
1940
-
1941
- const xfrm = frameObj['p:xfrm']
1942
- const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
1943
- const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
1944
-
1945
- const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
1946
- const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
1947
- const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
1948
-
1949
- const trsArr = tblObj['a:tr'] || []
1950
- const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
1951
-
1952
- const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
1953
- const pr = parent.row
1954
- const pc = parent.col
1955
-
1956
- let cellLeft = tableX
1957
- for (let idx = 0; idx < pc; idx++) {
1958
- cellLeft += colWidths[idx] || 0
1959
- }
1960
-
1961
- let cellTop = tableY
1962
- for (let idx = 0; idx < pr; idx++) {
1963
- cellTop += rowHeights[idx] || 0
1964
- }
1965
-
1966
- const parentCell = trsArr[pr]?.['a:tc']?.[pc]
1967
- const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
1968
- const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
1969
-
1970
- let cellWidth = 0
1971
- for (let idx = 0; idx < gridSpan; idx++) {
1972
- cellWidth += colWidths[pc + idx] || 0
1973
- }
1893
+ const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
1974
1894
 
1975
- let cellHeight = 0
1976
- for (let idx = 0; idx < rowSpan; idx++) {
1977
- cellHeight += rowHeights[pr + idx] || 0
1895
+ const bounds = this.getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager)
1896
+ if (!bounds) {
1897
+ throw new PPTXError(`Could not calculate bounds for cell (${rowIndex}, ${colIndex})`)
1978
1898
  }
1979
1899
 
1980
- const bounds = { left: cellLeft, top: cellTop, width: cellWidth, height: cellHeight }
1981
-
1982
1900
  const shapes = shapeManager.getShapes(slideIndex, slideManager)
1983
1901
  const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_`
1984
1902
  let maxShapeIndex = -1
@@ -2015,13 +1933,7 @@ class TableManager {
2015
1933
  slideManager,
2016
1934
  shapeManager
2017
1935
  ) {
2018
- const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
2019
- slideIndex,
2020
- tableId,
2021
- slideManager
2022
- )
2023
-
2024
- this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
1936
+ const { resolvedTableId } = this.#getTableContext(slideIndex, tableId, slideManager)
2025
1937
 
2026
1938
  const shapes = shapeManager.getShapes(slideIndex, slideManager)
2027
1939
  const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`
@@ -2037,47 +1949,11 @@ class TableManager {
2037
1949
  shapeManager.deleteShape(slideIndex, s.name, slideManager)
2038
1950
  }
2039
1951
 
2040
- const xfrm = frameObj['p:xfrm']
2041
- const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
2042
- const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
2043
-
2044
- const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
2045
- const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
2046
- const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
2047
-
2048
- const trsArr = tblObj['a:tr'] || []
2049
- const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
2050
-
2051
- const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
2052
- const pr = parent.row
2053
- const pc = parent.col
2054
-
2055
- let cellLeft = tableX
2056
- for (let idx = 0; idx < pc; idx++) {
2057
- cellLeft += colWidths[idx] || 0
2058
- }
2059
-
2060
- let cellTop = tableY
2061
- for (let idx = 0; idx < pr; idx++) {
2062
- cellTop += rowHeights[idx] || 0
2063
- }
2064
-
2065
- const parentCell = trsArr[pr]?.['a:tc']?.[pc]
2066
- const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
2067
- const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
2068
-
2069
- let cellWidth = 0
2070
- for (let idx = 0; idx < gridSpan; idx++) {
2071
- cellWidth += colWidths[pc + idx] || 0
2072
- }
2073
-
2074
- let cellHeight = 0
2075
- for (let idx = 0; idx < rowSpan; idx++) {
2076
- cellHeight += rowHeights[pr + idx] || 0
1952
+ const bounds = this.getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager)
1953
+ if (!bounds) {
1954
+ throw new PPTXError(`Could not calculate bounds for cell (${rowIndex}, ${colIndex})`)
2077
1955
  }
2078
1956
 
2079
- const bounds = { left: cellLeft, top: cellTop, width: cellWidth, height: cellHeight }
2080
-
2081
1957
  const expandedConfigs = this.#expandCellShape(options, bounds)
2082
1958
 
2083
1959
  expandedConfigs.forEach((expandedConfig, expIdx) => {
@@ -2158,7 +2034,7 @@ class TableManager {
2158
2034
  }
2159
2035
  }
2160
2036
 
2161
- #calculateRowHeights(slideIndex, tableId, slideManager, tblObj) {
2037
+ #calculateRowHeights(slideIndex, tableId, slideManager, tblObj, writeToXml = true) {
2162
2038
  const trsArr = tblObj['a:tr'] || []
2163
2039
  if (trsArr.length === 0) return []
2164
2040
 
@@ -2172,7 +2048,7 @@ class TableManager {
2172
2048
  // Initialize rowHeights with original height or a safe minimum floor of 228600 EMUs (~24px/pt)
2173
2049
  const rowHeights = trsArr.map(row => {
2174
2050
  const h = parseInt(row['@_h'] || 0, 10)
2175
- return Math.max(h, 228600)
2051
+ return h > 0 ? h : 228600
2176
2052
  })
2177
2053
 
2178
2054
  // Helper to get paragraph font size
@@ -2295,7 +2171,9 @@ class TableManager {
2295
2171
  }
2296
2172
 
2297
2173
  const totalCellHeight_emu = marT + marB + textHeight_emu
2298
- cellHeights[r][c] = Math.max(totalCellHeight_emu, 228600)
2174
+ const rowTemplateHeight = parseInt(row['@_h'] || 0, 10)
2175
+ const minFloor = rowTemplateHeight > 0 ? rowTemplateHeight : 228600
2176
+ cellHeights[r][c] = Math.max(totalCellHeight_emu, minFloor)
2299
2177
  }
2300
2178
  }
2301
2179
 
@@ -2346,8 +2224,10 @@ class TableManager {
2346
2224
  }
2347
2225
 
2348
2226
  // Update row heights in XML
2349
- for (let r = 0; r < numRows; r++) {
2350
- trsArr[r]['@_h'] = String(rowHeights[r])
2227
+ if (writeToXml) {
2228
+ for (let r = 0; r < numRows; r++) {
2229
+ trsArr[r]['@_h'] = String(rowHeights[r])
2230
+ }
2351
2231
  }
2352
2232
 
2353
2233
  return rowHeights
@@ -646,7 +646,11 @@ class ZipManager {
646
646
  await fs.writeFile(targetPath, this.#dirtyBinaryFiles.get(relPath))
647
647
  } else {
648
648
  const srcPath = path.join(this.#folderRoot, relPath)
649
- await fs.copy(srcPath, targetPath)
649
+ const resolvedSrc = path.resolve(srcPath)
650
+ const resolvedDest = path.resolve(targetPath)
651
+ if (resolvedSrc !== resolvedDest) {
652
+ await fs.copy(srcPath, targetPath)
653
+ }
650
654
  }
651
655
  }
652
656
  } else {