node-pptx-templater 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-pptx-templater",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
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",
@@ -191,4 +191,4 @@
191
191
  "LICENSE",
192
192
  "CHANGELOG.md"
193
193
  ]
194
- }
194
+ }
@@ -2556,7 +2556,11 @@ class PPTXTemplater {
2556
2556
  this.#assertLoaded()
2557
2557
  const targetIndices = this.#getTargetSlideIndices()
2558
2558
  for (const idx of targetIndices) {
2559
- this.#shapeManager.updateShapePosition(idx, shapeId, options, this.#slideManager)
2559
+ let resolvedOptions = options
2560
+ if (options.alignToCell) {
2561
+ resolvedOptions = this.#resolveAlignToCell(idx, options, shapeId, true)
2562
+ }
2563
+ this.#shapeManager.updateShapePosition(idx, shapeId, resolvedOptions, this.#slideManager)
2560
2564
  }
2561
2565
  return this
2562
2566
  }
@@ -2650,17 +2654,22 @@ class PPTXTemplater {
2650
2654
  return this.#shapeManager.validateShape(options)
2651
2655
  }
2652
2656
 
2653
- /**
2654
- * Adds a new shape dynamically to the targeted slide(s).
2655
- *
2656
- * @param {Object} options Shape configuration options.
2657
- * @returns {this} The chainable presentation templater instance.
2658
- */
2659
- async addShape(options) {
2657
+ async addShape(typeOrOptions, options = {}) {
2660
2658
  this.#assertLoaded()
2659
+ let resolvedOptions = {}
2660
+ if (typeof typeOrOptions === 'string') {
2661
+ resolvedOptions = { ...options, type: typeOrOptions }
2662
+ } else {
2663
+ resolvedOptions = { ...typeOrOptions }
2664
+ }
2665
+
2661
2666
  const targetIndices = this.#getTargetSlideIndices()
2662
2667
  for (const idx of targetIndices) {
2663
- this.#shapeManager.addShape(idx, options, this.#slideManager)
2668
+ let finalOptions = resolvedOptions
2669
+ if (resolvedOptions.alignToCell) {
2670
+ finalOptions = this.#resolveAlignToCell(idx, resolvedOptions)
2671
+ }
2672
+ this.#shapeManager.addShape(idx, finalOptions, this.#slideManager)
2664
2673
  }
2665
2674
  return this
2666
2675
  }
@@ -2676,7 +2685,11 @@ class PPTXTemplater {
2676
2685
  this.#assertLoaded()
2677
2686
  const targetIndices = this.#getTargetSlideIndices()
2678
2687
  for (const idx of targetIndices) {
2679
- this.#shapeManager.updateShape(idx, shapeId, options, this.#slideManager)
2688
+ let resolvedOptions = options
2689
+ if (options.alignToCell) {
2690
+ resolvedOptions = this.#resolveAlignToCell(idx, options, shapeId)
2691
+ }
2692
+ this.#shapeManager.updateShape(idx, shapeId, resolvedOptions, this.#slideManager)
2680
2693
  }
2681
2694
  return this
2682
2695
  }
@@ -2822,13 +2835,17 @@ class PPTXTemplater {
2822
2835
  /**
2823
2836
  * Retrieves final rendered bounds of a table cell in pixels.
2824
2837
  *
2825
- * @param {string} tableId - Table name or shape ID.
2838
+ * @param {string|Object} tableIdOrObj - Table name, shape ID, or table object.
2826
2839
  * @param {number} rowIndex - 0-based row index.
2827
2840
  * @param {number} colIndex - 0-based column index.
2828
2841
  * @returns {Object|null} Cell bounds { x, y, width, height } in pixels, or null.
2829
2842
  */
2830
- getCellBounds(tableId, rowIndex, colIndex) {
2843
+ getCellBounds(tableIdOrObj, rowIndex, colIndex) {
2831
2844
  this.#assertLoaded()
2845
+ const tableId =
2846
+ typeof tableIdOrObj === 'object'
2847
+ ? tableIdOrObj.id || tableIdOrObj.name || tableIdOrObj.tableId
2848
+ : tableIdOrObj
2832
2849
  const targetIndices = this.#getTargetSlideIndices()
2833
2850
  for (const idx of targetIndices) {
2834
2851
  try {
@@ -2851,14 +2868,21 @@ class PPTXTemplater {
2851
2868
 
2852
2869
  /**
2853
2870
  * Retrieves final rendered position of a table cell in pixels.
2871
+ * Optionally calculates centered top-left coordinates for a shape of given dimensions.
2854
2872
  *
2855
- * @param {string} tableId - Table name or shape ID.
2873
+ * @param {string|Object} tableIdOrObj - Table name, shape ID, or table object.
2856
2874
  * @param {number} rowIndex - 0-based row index.
2857
2875
  * @param {number} colIndex - 0-based column index.
2858
- * @returns {Object|null} Cell position { row, column, x, y } in pixels, or null.
2876
+ * @param {number|Object} [shapeWidthOrOptions] - Width of the shape in pixels, or options object.
2877
+ * @param {number} [shapeHeight] - Height of the shape in pixels.
2878
+ * @returns {Object|null} Cell position { row, column, x, y, width, height } in pixels, or null.
2859
2879
  */
2860
- getCellPosition(tableId, rowIndex, colIndex) {
2880
+ getCellPosition(tableIdOrObj, rowIndex, colIndex, shapeWidthOrOptions, shapeHeight) {
2861
2881
  this.#assertLoaded()
2882
+ const tableId =
2883
+ typeof tableIdOrObj === 'object'
2884
+ ? tableIdOrObj.id || tableIdOrObj.name || tableIdOrObj.tableId
2885
+ : tableIdOrObj
2862
2886
  const targetIndices = this.#getTargetSlideIndices()
2863
2887
  for (const idx of targetIndices) {
2864
2888
  try {
@@ -2867,7 +2891,9 @@ class PPTXTemplater {
2867
2891
  tableId,
2868
2892
  rowIndex,
2869
2893
  colIndex,
2870
- this.#slideManager
2894
+ this.#slideManager,
2895
+ shapeWidthOrOptions,
2896
+ shapeHeight
2871
2897
  )
2872
2898
  if (pos) return pos
2873
2899
  } catch (err) {
@@ -3548,6 +3574,126 @@ class PPTXTemplater {
3548
3574
  this.#zOrderManager.normalizeZOrder(targetIdx, this.#slideManager)
3549
3575
  return this
3550
3576
  }
3577
+
3578
+ /**
3579
+ * Aligns an existing shape to a table cell's position.
3580
+ *
3581
+ * @param {string} shapeId - Unique shape name/id in the template.
3582
+ * @param {string|Object} tableIdOrObj - Table ID string, or table object.
3583
+ * @param {number} rowIndex - 0-based row index.
3584
+ * @param {number} colIndex - 0-based column index.
3585
+ * @param {Object} [options] - Alignment options.
3586
+ * @param {'left'|'center'|'right'} [options.horizontal='center'] - Horizontal alignment.
3587
+ * @param {'top'|'middle'|'bottom'} [options.vertical='middle'] - Vertical alignment.
3588
+ * @returns {this} The chainable presentation templater instance.
3589
+ */
3590
+ alignShapeToCell(shapeId, tableIdOrObj, rowIndex, colIndex, options = {}) {
3591
+ const tableId =
3592
+ typeof tableIdOrObj === 'object'
3593
+ ? tableIdOrObj.id || tableIdOrObj.name || tableIdOrObj.tableId
3594
+ : tableIdOrObj
3595
+ this.updateShapePosition(shapeId, {
3596
+ alignToCell: {
3597
+ table: tableId,
3598
+ row: rowIndex,
3599
+ col: colIndex,
3600
+ horizontal: options.horizontal || 'center',
3601
+ vertical: options.vertical || 'middle',
3602
+ },
3603
+ })
3604
+ return this
3605
+ }
3606
+
3607
+ #resolveAlignToCell(slideIndex, options, shapeId, convertToEmus = false) {
3608
+ const align = options.alignToCell
3609
+ if (!align || !align.table) return options
3610
+
3611
+ const tableId =
3612
+ typeof align.table === 'object'
3613
+ ? align.table.id || align.table.name || align.table.tableId
3614
+ : align.table
3615
+ const row = align.row !== undefined ? align.row : 0
3616
+ const col = align.col !== undefined ? align.col : 0
3617
+
3618
+ // Get cell bounds
3619
+ const bounds = this.#tableManager.getCellBounds(
3620
+ slideIndex,
3621
+ tableId,
3622
+ row,
3623
+ col,
3624
+ this.#slideManager
3625
+ )
3626
+
3627
+ if (!bounds) return options
3628
+
3629
+ // Determine shape dimensions
3630
+ let shapeWidth = options.width
3631
+ let shapeHeight = options.height
3632
+
3633
+ if (convertToEmus) {
3634
+ if (shapeWidth !== undefined) shapeWidth = Math.round(shapeWidth / 9525)
3635
+ if (shapeHeight !== undefined) shapeHeight = Math.round(shapeHeight / 9525)
3636
+ }
3637
+
3638
+ if (shapeWidth === undefined || shapeHeight === undefined) {
3639
+ if (options.type === 'square' && options.size !== undefined) {
3640
+ shapeWidth = options.size
3641
+ shapeHeight = options.size
3642
+ } else if (options.type === 'circle' && options.radius !== undefined) {
3643
+ shapeWidth = options.radius * 2
3644
+ shapeHeight = options.radius * 2
3645
+ } else if (shapeId) {
3646
+ // Try getting existing shape dimensions
3647
+ const existing = this.#shapeManager.getShape(slideIndex, shapeId, this.#slideManager)
3648
+ if (existing) {
3649
+ shapeWidth = existing.width
3650
+ shapeHeight = existing.height
3651
+ }
3652
+ }
3653
+ }
3654
+
3655
+ // Default to fallback dimensions if still undefined
3656
+ if (shapeWidth === undefined) shapeWidth = 100
3657
+ if (shapeHeight === undefined) shapeHeight = 100
3658
+
3659
+ // Align horizontally
3660
+ let horiz = align.horizontal || align.alignX || 'center'
3661
+ horiz = String(horiz).toLowerCase()
3662
+ if (horiz === 'middle') horiz = 'center'
3663
+
3664
+ let x = bounds.x
3665
+ if (horiz === 'center') {
3666
+ x = bounds.x + (bounds.width - shapeWidth) / 2
3667
+ } else if (horiz === 'right') {
3668
+ x = bounds.x + bounds.width - shapeWidth
3669
+ }
3670
+
3671
+ // Align vertically
3672
+ let vert = align.vertical || align.alignY || 'middle'
3673
+ vert = String(vert).toLowerCase()
3674
+ if (vert === 'center') vert = 'middle'
3675
+
3676
+ let y = bounds.y
3677
+ if (vert === 'middle') {
3678
+ y = bounds.y + (bounds.height - shapeHeight) / 2
3679
+ } else if (vert === 'bottom') {
3680
+ y = bounds.y + bounds.height - shapeHeight
3681
+ }
3682
+
3683
+ const resolved = { ...options }
3684
+ if (convertToEmus) {
3685
+ resolved.x = Math.round(x * 9525)
3686
+ resolved.y = Math.round(y * 9525)
3687
+ } else {
3688
+ resolved.x = Math.round(x)
3689
+ resolved.y = Math.round(y)
3690
+ }
3691
+
3692
+ // Remove alignToCell to prevent it polluting lower levels
3693
+ delete resolved.alignToCell
3694
+
3695
+ return resolved
3696
+ }
3551
3697
  }
3552
3698
 
3553
3699
  module.exports = { PPTXTemplater }
@@ -796,6 +796,8 @@ 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
+
799
801
  const xfrm = frameObj['p:xfrm']
800
802
  const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
801
803
  const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
@@ -843,13 +845,50 @@ class TableManager {
843
845
  }
844
846
  }
845
847
 
846
- getCellPosition(slideIndex, tableId, rowIndex, colIndex, slideManager) {
848
+ getCellPosition(
849
+ slideIndex,
850
+ tableId,
851
+ rowIndex,
852
+ colIndex,
853
+ slideManager,
854
+ shapeWidthOrOptions,
855
+ shapeHeight
856
+ ) {
847
857
  const bounds = this.getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager)
858
+ if (!bounds) return null
859
+
860
+ let shapeWidth
861
+ let shapeHeightVal
862
+
863
+ if (shapeWidthOrOptions && typeof shapeWidthOrOptions === 'object') {
864
+ shapeWidth =
865
+ shapeWidthOrOptions.width !== undefined
866
+ ? shapeWidthOrOptions.width
867
+ : shapeWidthOrOptions.shapeWidth
868
+ shapeHeightVal =
869
+ shapeWidthOrOptions.height !== undefined
870
+ ? shapeWidthOrOptions.height
871
+ : shapeWidthOrOptions.shapeHeight
872
+ } else {
873
+ shapeWidth = shapeWidthOrOptions
874
+ shapeHeightVal = shapeHeight
875
+ }
876
+
877
+ let x = bounds.x
878
+ let y = bounds.y
879
+
880
+ if (shapeWidth !== undefined && shapeHeightVal !== undefined) {
881
+ x = Math.round(bounds.x + (bounds.width - shapeWidth) / 2)
882
+ y = Math.round(bounds.y + (bounds.height - shapeHeightVal) / 2)
883
+ }
884
+
848
885
  return {
849
886
  row: rowIndex,
850
887
  column: colIndex,
851
- x: bounds.x,
852
- y: bounds.y,
888
+ x,
889
+ y,
890
+ width: bounds.width,
891
+ height: bounds.height,
853
892
  }
854
893
  }
855
894
 
@@ -1352,8 +1391,17 @@ class TableManager {
1352
1391
  }
1353
1392
 
1354
1393
  // 2. Determine alignment settings
1355
- let alignX = config.alignX
1356
- let alignY = config.alignY
1394
+ let alignX = config.alignX || config.horizontal
1395
+ let alignY = config.alignY || config.vertical
1396
+
1397
+ if (alignX) {
1398
+ alignX = String(alignX).toLowerCase()
1399
+ if (alignX === 'middle') alignX = 'center'
1400
+ }
1401
+ if (alignY) {
1402
+ alignY = String(alignY).toLowerCase()
1403
+ if (alignY === 'center') alignY = 'middle'
1404
+ }
1357
1405
 
1358
1406
  if (config.position) {
1359
1407
  switch (config.position) {
@@ -1444,7 +1492,13 @@ class TableManager {
1444
1492
 
1445
1493
  // 4. Boundary Constraints Validation/Enforcement
1446
1494
  if (shapeWidth > cellWidth_px) {
1447
- shapeLeft = cellLeft_px
1495
+ if (alignX === 'center') {
1496
+ shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
1497
+ } else if (alignX === 'right') {
1498
+ shapeLeft = cellLeft_px + cellWidth_px - shapeWidth
1499
+ } else {
1500
+ shapeLeft = cellLeft_px
1501
+ }
1448
1502
  } else {
1449
1503
  shapeLeft = Math.max(
1450
1504
  cellLeft_px,
@@ -1453,7 +1507,13 @@ class TableManager {
1453
1507
  }
1454
1508
 
1455
1509
  if (shapeHeight > cellHeight_px) {
1456
- shapeTop = cellTop_px
1510
+ if (alignY === 'middle') {
1511
+ shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
1512
+ } else if (alignY === 'bottom') {
1513
+ shapeTop = cellTop_px + cellHeight_px - shapeHeight
1514
+ } else {
1515
+ shapeTop = cellTop_px
1516
+ }
1457
1517
  } else {
1458
1518
  shapeTop = Math.max(
1459
1519
  cellTop_px,
@@ -1876,6 +1936,8 @@ class TableManager {
1876
1936
  slideManager
1877
1937
  )
1878
1938
 
1939
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
1940
+
1879
1941
  const xfrm = frameObj['p:xfrm']
1880
1942
  const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
1881
1943
  const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
@@ -1959,6 +2021,8 @@ class TableManager {
1959
2021
  slideManager
1960
2022
  )
1961
2023
 
2024
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
2025
+
1962
2026
  const shapes = shapeManager.getShapes(slideIndex, slideManager)
1963
2027
  const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`
1964
2028
  const matchingShapes = shapes.filter(
@@ -2105,8 +2169,11 @@ class TableManager {
2105
2169
  const numRows = trsArr.length
2106
2170
  const numCols = colWidths.length
2107
2171
 
2108
- // Initialize rowHeights with original height or 0
2109
- const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
2172
+ // Initialize rowHeights with original height or a safe minimum floor of 228600 EMUs (~24px/pt)
2173
+ const rowHeights = trsArr.map(row => {
2174
+ const h = parseInt(row['@_h'] || 0, 10)
2175
+ return Math.max(h, 228600)
2176
+ })
2110
2177
 
2111
2178
  // Helper to get paragraph font size
2112
2179
  const getParagraphFontSize = p => {
@@ -2228,7 +2295,7 @@ class TableManager {
2228
2295
  }
2229
2296
 
2230
2297
  const totalCellHeight_emu = marT + marB + textHeight_emu
2231
- cellHeights[r][c] = totalCellHeight_emu
2298
+ cellHeights[r][c] = Math.max(totalCellHeight_emu, 228600)
2232
2299
  }
2233
2300
  }
2234
2301