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 +2 -2
- package/src/core/PPTXTemplater.js +162 -16
- package/src/managers/TableManager.js +77 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-pptx-templater",
|
|
3
|
-
"version": "1.1.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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(
|
|
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}
|
|
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
|
-
* @
|
|
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(
|
|
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(
|
|
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
|
|
852
|
-
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
|
-
|
|
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
|
-
|
|
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
|
|
2109
|
-
const rowHeights = trsArr.map(row =>
|
|
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
|
|