node-pptx-templater 1.0.20 → 1.0.21

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/README.md CHANGED
@@ -173,6 +173,22 @@ if (!report.valid) {
173
173
  }
174
174
  ```
175
175
 
176
+ ### 5. PPTX Extraction & Rebuilding Utilities
177
+
178
+ You can programmatically extract standard zipped `.pptx` archives into OpenXML folder templates or compile them back. Target directories are created automatically:
179
+
180
+ #### Extract PPTX to Folder Template
181
+ ```javascript
182
+ const { PPTXTemplate } = require('node-pptx-templater');
183
+
184
+ await PPTXTemplate.extractPptx('./sample.pptx', './output/template', { overwrite: true });
185
+ ```
186
+
187
+ #### Rebuild PPTX from Folder Template
188
+ ```javascript
189
+ await PPTXTemplate.buildPptx('./templates/sample', './output.pptx');
190
+ ```
191
+
176
192
  ---
177
193
 
178
194
  ## 📋 OpenXML Presentation Architecture
@@ -208,6 +224,95 @@ Naively duplicating rows in slide tables can leave duplicate `rowId` values or b
208
224
 
209
225
  ---
210
226
 
227
+ ## 📊 Reading Table Data & JSON Extraction
228
+
229
+ PPTXForge allows you to read table data from your template and return it as structured JSON objects or raw arrays. This is extremely useful for reverse-engineering data or verifying layouts.
230
+
231
+ ### 1. Object-Based Extraction (Default)
232
+ By default, the first row is treated as headers, and subsequent rows are returned as objects keyed by header names. Merged cells automatically resolve to their parent cell's value:
233
+ ```javascript
234
+ const rows = await ppt.getTableRows('SalesTable');
235
+ // Returns:
236
+ // [
237
+ // { region: 'North', sales: '1200', growth: '15%' },
238
+ // { region: 'South', sales: '1800', growth: '22%' }
239
+ // ]
240
+ ```
241
+
242
+ ### 2. Raw Extraction
243
+ Return a raw 2D array of string values (excluding the header row) by passing `{ raw: true }`:
244
+ ```javascript
245
+ const rows = await ppt.getTableRows('SalesTable', { raw: true });
246
+ // Returns:
247
+ // [
248
+ // ['North', '1200', '15%'],
249
+ // ['South', '1800', '22%']
250
+ // ]
251
+ ```
252
+
253
+ ### 3. Including Table Metadata
254
+ Pass `{ includeMetadata: true }` to retrieve row counts, column counts, and the list of merged cell ranges alongside the rows:
255
+ ```javascript
256
+ const result = await ppt.getTableRows('SalesTable', { includeMetadata: true });
257
+ // Returns:
258
+ // {
259
+ // rows: [...],
260
+ // rowCount: 10,
261
+ // columnCount: 5,
262
+ // mergedCells: [ { startRow: 1, startCol: 0, endRow: 2, endCol: 0 }, ... ]
263
+ // }
264
+ ```
265
+
266
+ ---
267
+
268
+ ## 📈 Nested Table Rows & Rowspan Support
269
+
270
+ When building financial sheets, invoices, or hierarchical dashboards, you often need to stack multiple rows vertically inside a single column while spanning cells in other columns. PPTXForge supports nested arrays in `addTableRow()` to automatically handle:
271
+ - Proportional height scaling.
272
+ - Vertical merge (`vMerge`) and row span (`rowSpan`) generation.
273
+ - Dynamic layout adjustments.
274
+
275
+ ### 1. Vertical Row Nesting Example
276
+ ```javascript
277
+ await ppt.addTableRow('Table1', [
278
+ 'Region',
279
+ ['Sales', '1200'],
280
+ ['Growth', '15%']
281
+ ]);
282
+ ```
283
+ Generates:
284
+ ```text
285
+ +---------+---------+---------+
286
+ | Region | Sales | Growth |
287
+ | | 1200 | 15% |
288
+ +---------+---------+---------+
289
+ ```
290
+
291
+ ### 2. Deep Nesting Example
292
+ ```javascript
293
+ await ppt.addTableRow('Table1', [
294
+ 'Parent',
295
+ [
296
+ 'Child 1',
297
+ 'Child 2',
298
+ 'Child 3'
299
+ ]
300
+ ]);
301
+ ```
302
+ Generates 3 sub-rows where column 0 automatically spans all 3 rows.
303
+
304
+ ### 3. Merge Strategies
305
+ Configure merge behavior via `options.mergeStrategy`:
306
+ - `'auto'` (default): Creates structural merges from nested arrays, and additionally merges consecutive duplicate values in the same column.
307
+ - `'rowspan'`: Creates structural rowspan/merges strictly from the nested array structure.
308
+ - `'none'`: Pads columns to match the target generated height but does not merge cells (leaving them as individual cells).
309
+
310
+ ```javascript
311
+ await ppt.addTableRow('Table1', data, { mergeStrategy: 'rowspan' });
312
+ ```
313
+
314
+ ---
315
+
211
316
  ## 📊 Feature Comparison Matrix
212
317
 
213
318
  Compare PPTXForge with other popular PowerPoint automation libraries:
@@ -300,6 +405,41 @@ Discovers and retrieves details of an existing cell shape on the targeted slide.
300
405
  const shape = ppt.getCellShape('Table', 1, 2, 0);
301
406
  ```
302
407
 
408
+ #### `getCellBounds(tableId, rowIndex, colIndex)`
409
+ Retrieves final rendered bounds of a table cell in pixels.
410
+
411
+ * **Arguments**:
412
+ * `tableId` (`string`): Table name or shape ID.
413
+ * `rowIndex` (`number`): 0-based row index.
414
+ * `colIndex` (`number`): 0-based column index.
415
+ * **Returns**: `Object|null` - Cell bounds { x, y, width, height } in pixels, or null.
416
+
417
+ ```javascript
418
+ const bounds = ppt.getCellBounds('summary-table', 1, 1);
419
+ ```
420
+
421
+ #### `getCellPosition(tableId, rowIndex, colIndex)`
422
+ Retrieves final rendered position of a table cell in pixels.
423
+
424
+ * **Arguments**:
425
+ * `tableId` (`string`): Table name or shape ID.
426
+ * `rowIndex` (`number`): 0-based row index.
427
+ * `colIndex` (`number`): 0-based column index.
428
+ * **Returns**: `Object|null` - Cell position { row, column, x, y } in pixels, or null.
429
+
430
+ ```javascript
431
+ const pos = ppt.getCellPosition('summary-table', 1, 1);
432
+ ```
433
+
434
+ #### `getTableRows(())`
435
+ Delegates core actions to slide element sub-managers.
436
+
437
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
438
+
439
+ ```javascript
440
+ const rows = await ppt.getTableRows('SalesTable');
441
+ ```
442
+
303
443
  #### `addTableRow(())`
304
444
  Delegates core actions to slide element sub-managers.
305
445
 
@@ -1587,6 +1727,24 @@ Delegates core actions to slide element sub-managers.
1587
1727
  const ppt = await PPTXTemplate.fromPresentationXml('./template-folder');
1588
1728
  ```
1589
1729
 
1730
+ #### `extractPptx(())`
1731
+ Delegates core actions to slide element sub-managers.
1732
+
1733
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
1734
+
1735
+ ```javascript
1736
+ await PPTXTemplater.extractPptx('sample.pptx', './extracted');
1737
+ ```
1738
+
1739
+ #### `buildPptx(())`
1740
+ Delegates core actions to slide element sub-managers.
1741
+
1742
+ * **Returns**: `PPTXTemplater` - The fluent engine instance.
1743
+
1744
+ ```javascript
1745
+ await PPTXTemplater.buildPptx('./extracted', 'output.pptx');
1746
+ ```
1747
+
1590
1748
  #### `validatePresentation(())`
1591
1749
  Delegates core actions to slide element sub-managers.
1592
1750
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-pptx-templater",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
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",
@@ -340,6 +340,89 @@ class PPTXTemplater {
340
340
  return engine
341
341
  }
342
342
 
343
+ /**
344
+ * Extracts a PPTX file into an unzipped OpenXML folder structure.
345
+ *
346
+ * @static
347
+ * @param {string} pptxPath - Path to the source PPTX file.
348
+ * @param {string} outputPath - Path to the destination folder.
349
+ * @param {Object} [options] - Options (e.g. { overwrite: true }).
350
+ * @returns {Promise<void>}
351
+ */
352
+ static async extractPptx(pptxPath, outputPath, options = {}) {
353
+ const fs = require('fs-extra')
354
+ const path = require('path')
355
+
356
+ const resolvedPptx = path.resolve(pptxPath)
357
+ const resolvedOut = path.resolve(outputPath)
358
+
359
+ if (!fs.existsSync(resolvedPptx)) {
360
+ throw new PPTXError(`Source PPTX file not found: ${pptxPath}`)
361
+ }
362
+
363
+ if (fs.existsSync(resolvedOut)) {
364
+ const stats = fs.statSync(resolvedOut)
365
+ if (stats.isFile()) {
366
+ throw new PPTXError(`Destination is a file: ${outputPath}`)
367
+ }
368
+ const files = fs.readdirSync(resolvedOut)
369
+ if (files.length > 0 && !options.overwrite) {
370
+ throw new PPTXError(
371
+ `Destination directory "${outputPath}" is not empty. Set overwrite: true to overwrite.`
372
+ )
373
+ }
374
+ } else {
375
+ await fs.ensureDir(resolvedOut)
376
+ }
377
+
378
+ const engine = await PPTXTemplater.load(resolvedPptx)
379
+ await engine.#zipManager.toFolder(resolvedOut)
380
+
381
+ // Validation
382
+ const criticalParts = ['ppt/presentation.xml', 'ppt/slides', 'ppt/_rels', '[Content_Types].xml']
383
+
384
+ for (const part of criticalParts) {
385
+ const p = path.join(resolvedOut, part)
386
+ if (!fs.existsSync(p)) {
387
+ throw new PPTXError(`Extracted structure is missing critical part: ${part}`)
388
+ }
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Rebuilds a PPTX file from an unzipped OpenXML folder structure.
394
+ *
395
+ * @static
396
+ * @param {string} folderPath - Path to the source folder structure.
397
+ * @param {string} pptxPath - Path to the destination PPTX file.
398
+ * @returns {Promise<void>}
399
+ */
400
+ static async buildPptx(folderPath, pptxPath) {
401
+ const fs = require('fs-extra')
402
+ const path = require('path')
403
+
404
+ const resolvedFolder = path.resolve(folderPath)
405
+ const resolvedPptx = path.resolve(pptxPath)
406
+
407
+ if (!fs.existsSync(resolvedFolder)) {
408
+ throw new PPTXError(`Source folder not found: ${folderPath}`)
409
+ }
410
+
411
+ // Validation of the source folder
412
+ const criticalParts = ['ppt/presentation.xml', 'ppt/slides', 'ppt/_rels', '[Content_Types].xml']
413
+
414
+ for (const part of criticalParts) {
415
+ const p = path.join(resolvedFolder, part)
416
+ if (!fs.existsSync(p)) {
417
+ throw new PPTXError(`Source folder is missing critical OpenXML part: ${part}`)
418
+ }
419
+ }
420
+
421
+ const engine = await PPTXTemplater.load(resolvedFolder)
422
+ await fs.ensureDir(path.dirname(resolvedPptx))
423
+ await engine.saveToFile(resolvedPptx)
424
+ }
425
+
343
426
  /**
344
427
  * Initializes the engine by loading a PPTX file/buffer.
345
428
  * @private
@@ -1297,11 +1380,21 @@ class PPTXTemplater {
1297
1380
  }
1298
1381
 
1299
1382
  // === Table Features ===
1300
- addTableRow(tableId, rowData) {
1383
+ getTableRows(tableId, options = {}) {
1384
+ this.#assertLoaded()
1385
+ const targetIndices = this.#getTargetSlideIndices()
1386
+ if (targetIndices.length === 0) {
1387
+ throw new PPTXError('No slides active/loaded')
1388
+ }
1389
+ const idx = targetIndices[0]
1390
+ return this.#tableManager.getTableRows(idx, tableId, options, this.#slideManager)
1391
+ }
1392
+
1393
+ addTableRow(tableId, rowData, options = {}) {
1301
1394
  this.#assertLoaded()
1302
1395
  const targetIndices = this.#getTargetSlideIndices()
1303
1396
  for (const idx of targetIndices) {
1304
- this.#tableManager.addTableRow(idx, tableId, rowData, this.#slideManager)
1397
+ this.#tableManager.addTableRow(idx, tableId, rowData, this.#slideManager, options)
1305
1398
  }
1306
1399
  return this
1307
1400
  }
@@ -2099,6 +2192,66 @@ class PPTXTemplater {
2099
2192
  return null
2100
2193
  }
2101
2194
 
2195
+ /**
2196
+ * Retrieves final rendered bounds of a table cell in pixels.
2197
+ *
2198
+ * @param {string} tableId - Table name or shape ID.
2199
+ * @param {number} rowIndex - 0-based row index.
2200
+ * @param {number} colIndex - 0-based column index.
2201
+ * @returns {Object|null} Cell bounds { x, y, width, height } in pixels, or null.
2202
+ */
2203
+ getCellBounds(tableId, rowIndex, colIndex) {
2204
+ this.#assertLoaded()
2205
+ const targetIndices = this.#getTargetSlideIndices()
2206
+ for (const idx of targetIndices) {
2207
+ try {
2208
+ const bounds = this.#tableManager.getCellBounds(
2209
+ idx,
2210
+ tableId,
2211
+ rowIndex,
2212
+ colIndex,
2213
+ this.#slideManager
2214
+ )
2215
+ if (bounds) return bounds
2216
+ } catch (err) {
2217
+ logger.debug(
2218
+ `Could not get cell bounds for table ${tableId} on slide ${idx}: ${err.message}`
2219
+ )
2220
+ }
2221
+ }
2222
+ return null
2223
+ }
2224
+
2225
+ /**
2226
+ * Retrieves final rendered position of a table cell in pixels.
2227
+ *
2228
+ * @param {string} tableId - Table name or shape ID.
2229
+ * @param {number} rowIndex - 0-based row index.
2230
+ * @param {number} colIndex - 0-based column index.
2231
+ * @returns {Object|null} Cell position { row, column, x, y } in pixels, or null.
2232
+ */
2233
+ getCellPosition(tableId, rowIndex, colIndex) {
2234
+ this.#assertLoaded()
2235
+ const targetIndices = this.#getTargetSlideIndices()
2236
+ for (const idx of targetIndices) {
2237
+ try {
2238
+ const pos = this.#tableManager.getCellPosition(
2239
+ idx,
2240
+ tableId,
2241
+ rowIndex,
2242
+ colIndex,
2243
+ this.#slideManager
2244
+ )
2245
+ if (pos) return pos
2246
+ } catch (err) {
2247
+ logger.debug(
2248
+ `Could not get cell position for table ${tableId} on slide ${idx}: ${err.message}`
2249
+ )
2250
+ }
2251
+ }
2252
+ return null
2253
+ }
2254
+
2102
2255
  // === Image Features ===
2103
2256
  async replaceImage(imageIdOrName, sourcePathOrBuffer) {
2104
2257
  this.#assertLoaded()
@@ -223,6 +223,8 @@ class TableManager {
223
223
  )
224
224
  }
225
225
 
226
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
227
+
226
228
  if (cellShapes) {
227
229
  this.#processCellShapes(
228
230
  slideIndex,
@@ -251,7 +253,7 @@ class TableManager {
251
253
  * @param {string[]} rowData
252
254
  * @param {SlideManager} slideManager
253
255
  */
254
- addTableRow(slideIndex, tableId, rowData, slideManager) {
256
+ addTableRow(slideIndex, tableId, rowData, slideManager, options = {}) {
255
257
  const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
256
258
 
257
259
  const trs = tblObj['a:tr'] || []
@@ -260,18 +262,69 @@ class TableManager {
260
262
  }
261
263
 
262
264
  const lastRow = trs[trs.length - 1]
263
- const newRow = this.#xmlParser.deepClone(lastRow)
264
- this.#updateRowId(newRow)
265
+ const numCols = lastRow['a:tc']?.length || 0
265
266
 
266
- const tcs = newRow['a:tc'] || []
267
- for (let j = 0; j < tcs.length; j++) {
268
- this.#setCellTextObj(tcs[j], rowData[j] !== undefined ? rowData[j] : '')
269
- // Clear any merged indicators for the new row by default
270
- if (tcs[j]['@_hMerge']) delete tcs[j]['@_hMerge']
271
- if (tcs[j]['@_vMerge']) delete tcs[j]['@_vMerge']
267
+ // Compute target generated height
268
+ const heights = []
269
+ for (let c = 0; c < numCols; c++) {
270
+ heights.push(this.#getNestedHeight(rowData[c]))
272
271
  }
272
+ const targetHeight = Math.max(1, ...heights)
273
273
 
274
- trs.push(newRow)
274
+ // Expand each column value to targetHeight
275
+ const expandedCols = []
276
+ const strategy = options.mergeStrategy || 'auto'
277
+ for (let c = 0; c < numCols; c++) {
278
+ let colCells = this.#expandCellVal(rowData[c], targetHeight)
279
+ if (strategy === 'none') {
280
+ for (let i = 0; i < colCells.length; i++) {
281
+ if (colCells[i].vMerge) {
282
+ colCells[i] = { value: '', rowSpan: 1 }
283
+ } else {
284
+ colCells[i].rowSpan = 1
285
+ }
286
+ }
287
+ } else if (strategy === 'auto') {
288
+ colCells = this.#applyAutoMerge(colCells)
289
+ }
290
+ expandedCols.push(colCells)
291
+ }
292
+
293
+ // Clone and append rows
294
+ for (let r = 0; r < targetHeight; r++) {
295
+ const newRow = this.#xmlParser.deepClone(lastRow)
296
+ this.#updateRowId(newRow)
297
+
298
+ const tcs = newRow['a:tc'] || []
299
+ for (let c = 0; c < numCols; c++) {
300
+ const cellDef = expandedCols[c][r]
301
+ const tcObj = tcs[c]
302
+
303
+ // Clear any previous merge attributes
304
+ if (tcObj['@_hMerge']) delete tcObj['@_hMerge']
305
+ if (tcObj['@_vMerge']) delete tcObj['@_vMerge']
306
+ if (tcObj['@_gridSpan']) delete tcObj['@_gridSpan']
307
+ if (tcObj['@_rowSpan']) delete tcObj['@_rowSpan']
308
+
309
+ if (cellDef.vMerge) {
310
+ tcObj['@_vMerge'] = '1'
311
+ this.#setCellTextObj(tcObj, '')
312
+ } else {
313
+ let text = cellDef.value
314
+ let cellOpts = {}
315
+ if (cellDef.value && typeof cellDef.value === 'object') {
316
+ text = cellDef.value.value !== undefined ? cellDef.value.value : ''
317
+ cellOpts = cellDef.value
318
+ }
319
+ this.#setCellTextObj(tcObj, text)
320
+ if (cellDef.rowSpan && cellDef.rowSpan > 1 && strategy !== 'none') {
321
+ tcObj['@_rowSpan'] = String(cellDef.rowSpan)
322
+ }
323
+ this.#applyCellOptions(tcObj, cellOpts)
324
+ }
325
+ }
326
+ trs.push(newRow)
327
+ }
275
328
 
276
329
  slideManager.markSlideObjDirty(slideIndex)
277
330
  }
@@ -740,6 +793,68 @@ class TableManager {
740
793
  return { row, col }
741
794
  }
742
795
 
796
+ getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager) {
797
+ const { tblObj, frameObj } = this.#getTableContext(slideIndex, tableId, slideManager)
798
+
799
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
800
+
801
+ const xfrm = frameObj['p:xfrm']
802
+ const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
803
+ const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
804
+
805
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
806
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
807
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
808
+
809
+ const trsArr = tblObj['a:tr'] || []
810
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
811
+
812
+ const parent = this.getMergeParent(slideIndex, tableId, rowIndex, colIndex, slideManager)
813
+ const pr = parent.row
814
+ const pc = parent.col
815
+
816
+ let cellLeft = tableX
817
+ for (let idx = 0; idx < pc; idx++) {
818
+ cellLeft += colWidths[idx] || 0
819
+ }
820
+
821
+ let cellTop = tableY
822
+ for (let idx = 0; idx < pr; idx++) {
823
+ cellTop += rowHeights[idx] || 0
824
+ }
825
+
826
+ const parentCell = trsArr[pr]?.['a:tc']?.[pc]
827
+ const gridSpan = parentCell?.['@_gridSpan'] ? parseInt(parentCell['@_gridSpan'], 10) : 1
828
+ const rowSpan = parentCell?.['@_rowSpan'] ? parseInt(parentCell['@_rowSpan'], 10) : 1
829
+
830
+ let cellWidth = 0
831
+ for (let idx = 0; idx < gridSpan; idx++) {
832
+ cellWidth += colWidths[pc + idx] || 0
833
+ }
834
+
835
+ let cellHeight = 0
836
+ for (let idx = 0; idx < rowSpan; idx++) {
837
+ cellHeight += rowHeights[pr + idx] || 0
838
+ }
839
+
840
+ return {
841
+ x: Math.round(cellLeft / 9525),
842
+ y: Math.round(cellTop / 9525),
843
+ width: Math.round(cellWidth / 9525),
844
+ height: Math.round(cellHeight / 9525),
845
+ }
846
+ }
847
+
848
+ getCellPosition(slideIndex, tableId, rowIndex, colIndex, slideManager) {
849
+ const bounds = this.getCellBounds(slideIndex, tableId, rowIndex, colIndex, slideManager)
850
+ return {
851
+ row: rowIndex,
852
+ column: colIndex,
853
+ x: bounds.x,
854
+ y: bounds.y,
855
+ }
856
+ }
857
+
743
858
  /**
744
859
  * Splits a merged region containing cell (row, col).
745
860
  */
@@ -1152,45 +1267,226 @@ class TableManager {
1152
1267
  }
1153
1268
 
1154
1269
  #expandCellShape(config, cellBounds) {
1155
- const cellLeft_px = cellBounds.left / 9525
1156
- const cellTop_px = cellBounds.top / 9525
1157
- const cellWidth_px = cellBounds.width / 9525
1158
- const cellHeight_px = cellBounds.height / 9525
1270
+ const cellLeft_px = Math.round(cellBounds.left / 9525)
1271
+ const cellTop_px = Math.round(cellBounds.top / 9525)
1272
+ const cellWidth_px = Math.round(cellBounds.width / 9525)
1273
+ const cellHeight_px = Math.round(cellBounds.height / 9525)
1274
+
1275
+ const parseLength = (val, maxVal) => {
1276
+ if (typeof val === 'string' && val.endsWith('%')) {
1277
+ return (parseFloat(val) / 100) * maxVal
1278
+ }
1279
+ return val !== undefined ? parseFloat(val) : undefined
1280
+ }
1281
+
1282
+ const isCellAnchored = config.anchor !== 'slide'
1283
+
1284
+ // 1. Determine bounding box width and height
1285
+ let shapeWidth
1286
+ let shapeHeight
1159
1287
 
1288
+ if (config.type === 'progressBar') {
1289
+ shapeHeight = parseLength(config.height !== undefined ? config.height : 8, cellHeight_px)
1290
+ shapeWidth = parseLength(
1291
+ config.width !== undefined ? config.width : cellWidth_px - 10,
1292
+ cellWidth_px
1293
+ )
1294
+ } else if (config.type === 'badge') {
1295
+ const text = String(config.text !== undefined ? config.text : '')
1296
+ const fontSize = config.textStyle?.fontSize || 10
1297
+ const textWidth = text.length * fontSize * 0.6
1298
+ const paddingX = 12
1299
+ shapeWidth =
1300
+ parseLength(config.width, cellWidth_px) !== undefined
1301
+ ? parseLength(config.width, cellWidth_px)
1302
+ : textWidth + paddingX * 2
1303
+ shapeHeight =
1304
+ parseLength(config.height, cellHeight_px) !== undefined
1305
+ ? parseLength(config.height, cellHeight_px)
1306
+ : fontSize + 12
1307
+ } else if (config.type === 'icon') {
1308
+ const size = parseLength(
1309
+ config.size !== undefined ? config.size : 16,
1310
+ Math.min(cellWidth_px, cellHeight_px)
1311
+ )
1312
+ shapeWidth = size
1313
+ shapeHeight = size
1314
+ } else {
1315
+ shapeWidth = parseLength(config.width, cellWidth_px)
1316
+ if (shapeWidth === undefined) {
1317
+ const sizeVal = parseLength(config.size, Math.min(cellWidth_px, cellHeight_px))
1318
+ if (sizeVal !== undefined) {
1319
+ shapeWidth = sizeVal
1320
+ } else {
1321
+ const radiusVal = parseLength(config.radius, Math.min(cellWidth_px, cellHeight_px) / 2)
1322
+ if (radiusVal !== undefined) {
1323
+ shapeWidth = radiusVal * 2
1324
+ } else {
1325
+ shapeWidth = 12 // default
1326
+ }
1327
+ }
1328
+ }
1329
+
1330
+ shapeHeight = parseLength(config.height, cellHeight_px)
1331
+ if (shapeHeight === undefined) {
1332
+ const sizeVal = parseLength(config.size, Math.min(cellWidth_px, cellHeight_px))
1333
+ if (sizeVal !== undefined) {
1334
+ shapeHeight = sizeVal
1335
+ } else {
1336
+ const radiusVal = parseLength(config.radius, Math.min(cellWidth_px, cellHeight_px) / 2)
1337
+ if (radiusVal !== undefined) {
1338
+ shapeHeight = radiusVal * 2
1339
+ } else {
1340
+ shapeHeight = 12 // default
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+
1346
+ // 2. Determine alignment settings
1347
+ let alignX = config.alignX
1348
+ let alignY = config.alignY
1349
+
1350
+ if (config.position) {
1351
+ switch (config.position) {
1352
+ case 'top-left':
1353
+ if (!alignX) alignX = 'left'
1354
+ if (!alignY) alignY = 'top'
1355
+ break
1356
+ case 'top-center':
1357
+ case 'top':
1358
+ if (!alignX) alignX = 'center'
1359
+ if (!alignY) alignY = 'top'
1360
+ break
1361
+ case 'top-right':
1362
+ if (!alignX) alignX = 'right'
1363
+ if (!alignY) alignY = 'top'
1364
+ break
1365
+ case 'middle-left':
1366
+ case 'left':
1367
+ if (!alignX) alignX = 'left'
1368
+ if (!alignY) alignY = 'middle'
1369
+ break
1370
+ case 'center':
1371
+ case 'middle-center':
1372
+ if (!alignX) alignX = 'center'
1373
+ if (!alignY) alignY = 'middle'
1374
+ break
1375
+ case 'middle-right':
1376
+ case 'right':
1377
+ if (!alignX) alignX = 'right'
1378
+ if (!alignY) alignY = 'middle'
1379
+ break
1380
+ case 'bottom-left':
1381
+ if (!alignX) alignX = 'left'
1382
+ if (!alignY) alignY = 'bottom'
1383
+ break
1384
+ case 'bottom-center':
1385
+ case 'bottom':
1386
+ if (!alignX) alignX = 'center'
1387
+ if (!alignY) alignY = 'bottom'
1388
+ break
1389
+ case 'bottom-right':
1390
+ if (!alignX) alignX = 'right'
1391
+ if (!alignY) alignY = 'bottom'
1392
+ break
1393
+ }
1394
+ }
1395
+
1396
+ if (alignX && !alignY && config.y === undefined) {
1397
+ alignY = 'middle'
1398
+ }
1399
+ if (alignY && !alignX && config.x === undefined) {
1400
+ alignX = 'center'
1401
+ }
1402
+
1403
+ if (!alignX && !alignY && config.x === undefined && config.y === undefined) {
1404
+ alignX = 'center'
1405
+ alignY = 'middle'
1406
+ }
1407
+
1408
+ // 3. Compute coordinates
1409
+ let shapeLeft = cellLeft_px
1410
+ let shapeTop = cellTop_px
1411
+
1412
+ if (isCellAnchored) {
1413
+ if (alignX === 'left') {
1414
+ shapeLeft = cellLeft_px + (config.x !== undefined ? config.x : 5)
1415
+ } else if (alignX === 'center') {
1416
+ shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2 + (config.x || 0)
1417
+ } else if (alignX === 'right') {
1418
+ shapeLeft =
1419
+ cellLeft_px + cellWidth_px - shapeWidth - (config.x !== undefined ? config.x : 5)
1420
+ } else {
1421
+ shapeLeft =
1422
+ cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - shapeWidth) / 2)
1423
+ }
1424
+
1425
+ if (alignY === 'top') {
1426
+ shapeTop = cellTop_px + (config.y !== undefined ? config.y : 5)
1427
+ } else if (alignY === 'middle') {
1428
+ shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2 + (config.y || 0)
1429
+ } else if (alignY === 'bottom') {
1430
+ shapeTop =
1431
+ cellTop_px + cellHeight_px - shapeHeight - (config.y !== undefined ? config.y : 5)
1432
+ } else {
1433
+ shapeTop =
1434
+ cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - shapeHeight) / 2)
1435
+ }
1436
+
1437
+ // 4. Boundary Constraints Validation/Enforcement
1438
+ if (shapeWidth > cellWidth_px) {
1439
+ shapeLeft = cellLeft_px
1440
+ } else {
1441
+ shapeLeft = Math.max(
1442
+ cellLeft_px,
1443
+ Math.min(shapeLeft, cellLeft_px + cellWidth_px - shapeWidth)
1444
+ )
1445
+ }
1446
+
1447
+ if (shapeHeight > cellHeight_px) {
1448
+ shapeTop = cellTop_px
1449
+ } else {
1450
+ shapeTop = Math.max(
1451
+ cellTop_px,
1452
+ Math.min(shapeTop, cellTop_px + cellHeight_px - shapeHeight)
1453
+ )
1454
+ }
1455
+ } else {
1456
+ shapeLeft = config.x || 0
1457
+ shapeTop = config.y || 0
1458
+ }
1459
+
1460
+ // 5. Expand individual sub-elements / custom shapes
1160
1461
  if (config.type === 'progressBar') {
1161
1462
  const value = config.value !== undefined ? config.value : 0
1162
1463
  const max = config.max !== undefined ? config.max : 100
1163
1464
  const fill = config.fill || '#3B82F6'
1164
1465
  const bgFill = config.backgroundFill || '#E5E7EB'
1165
- const pbHeight = config.height !== undefined ? config.height : 8
1166
- const pbWidth = config.width !== undefined ? config.width : cellWidth_px - 10
1167
-
1168
- const pbX = cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - pbWidth) / 2)
1169
- const pbY = cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - pbHeight) / 2)
1170
1466
 
1171
1467
  const shapes = []
1172
1468
  shapes.push({
1173
1469
  type: 'roundedRectangle',
1174
1470
  fill: bgFill,
1175
- x: pbX,
1176
- y: pbY,
1177
- width: pbWidth,
1178
- height: pbHeight,
1179
- borderRadius: pbHeight / 2,
1471
+ x: shapeLeft,
1472
+ y: shapeTop,
1473
+ width: shapeWidth,
1474
+ height: shapeHeight,
1475
+ borderRadius: shapeHeight / 2,
1180
1476
  zIndex: config.zIndex,
1181
1477
  })
1182
1478
 
1183
1479
  const pct = Math.min(1, Math.max(0, value / max))
1184
1480
  if (pct > 0) {
1185
- const filledWidth = pbWidth * pct
1481
+ const filledWidth = shapeWidth * pct
1186
1482
  shapes.push({
1187
1483
  type: 'roundedRectangle',
1188
1484
  fill: fill,
1189
- x: pbX,
1190
- y: pbY,
1485
+ x: shapeLeft,
1486
+ y: shapeTop,
1191
1487
  width: filledWidth,
1192
- height: pbHeight,
1193
- borderRadius: pbHeight / 2,
1488
+ height: shapeHeight,
1489
+ borderRadius: shapeHeight / 2,
1194
1490
  zIndex: (config.zIndex || 0) + 1,
1195
1491
  })
1196
1492
  }
@@ -1200,23 +1496,15 @@ class TableManager {
1200
1496
  if (config.type === 'badge') {
1201
1497
  const text = String(config.text !== undefined ? config.text : '')
1202
1498
  const fontSize = config.textStyle?.fontSize || 10
1203
- const textWidth = text.length * fontSize * 0.6
1204
- const paddingX = 12
1205
- const badgeWidth = config.width !== undefined ? config.width : textWidth + paddingX * 2
1206
- const badgeHeight = config.height !== undefined ? config.height : fontSize + 12
1207
-
1208
- const x = cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - badgeWidth) / 2)
1209
- const y = cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - badgeHeight) / 2)
1210
-
1211
1499
  return [
1212
1500
  {
1213
1501
  type: 'roundedRectangle',
1214
1502
  fill: config.fill || '#10B981',
1215
- borderRadius: badgeHeight / 2,
1216
- x: x,
1217
- y: y,
1218
- width: badgeWidth,
1219
- height: badgeHeight,
1503
+ borderRadius: shapeHeight / 2,
1504
+ x: shapeLeft,
1505
+ y: shapeTop,
1506
+ width: shapeWidth,
1507
+ height: shapeHeight,
1220
1508
  text: text,
1221
1509
  textStyle: {
1222
1510
  color: config.textStyle?.color || '#FFFFFF',
@@ -1234,9 +1522,8 @@ class TableManager {
1234
1522
  }
1235
1523
 
1236
1524
  if (config.type === 'icon') {
1237
- const size = config.size || 16
1238
1525
  const iconFill = config.fill
1239
- const fontSize = Math.round(size * 0.8)
1526
+ const fontSize = Math.round(shapeWidth * 0.8)
1240
1527
 
1241
1528
  let baseConfig = null
1242
1529
  switch (config.icon) {
@@ -1245,8 +1532,8 @@ class TableManager {
1245
1532
  type: 'rectangle',
1246
1533
  fill: 'none',
1247
1534
  border: null,
1248
- width: size,
1249
- height: size,
1535
+ width: shapeWidth,
1536
+ height: shapeHeight,
1250
1537
  text: '✔',
1251
1538
  textStyle: {
1252
1539
  color: iconFill || '#10B981',
@@ -1261,8 +1548,8 @@ class TableManager {
1261
1548
  type: 'rectangle',
1262
1549
  fill: 'none',
1263
1550
  border: null,
1264
- width: size,
1265
- height: size,
1551
+ width: shapeWidth,
1552
+ height: shapeHeight,
1266
1553
  text: '✘',
1267
1554
  textStyle: {
1268
1555
  color: iconFill || '#EF4444',
@@ -1277,8 +1564,8 @@ class TableManager {
1277
1564
  type: 'triangle',
1278
1565
  fill: iconFill || '#F59E0B',
1279
1566
  border: null,
1280
- width: size,
1281
- height: size,
1567
+ width: shapeWidth,
1568
+ height: shapeHeight,
1282
1569
  text: '!',
1283
1570
  textStyle: {
1284
1571
  color: '#FFFFFF',
@@ -1293,7 +1580,7 @@ class TableManager {
1293
1580
  type: 'circle',
1294
1581
  fill: iconFill || '#3B82F6',
1295
1582
  border: null,
1296
- radius: size / 2,
1583
+ radius: shapeWidth / 2,
1297
1584
  text: 'i',
1298
1585
  textStyle: {
1299
1586
  color: '#FFFFFF',
@@ -1308,8 +1595,8 @@ class TableManager {
1308
1595
  type: 'star5',
1309
1596
  fill: iconFill || '#FBBF24',
1310
1597
  border: null,
1311
- width: size,
1312
- height: size,
1598
+ width: shapeWidth,
1599
+ height: shapeHeight,
1313
1600
  }
1314
1601
  break
1315
1602
  case 'up':
@@ -1317,8 +1604,8 @@ class TableManager {
1317
1604
  type: 'upArrow',
1318
1605
  fill: iconFill || '#10B981',
1319
1606
  border: null,
1320
- width: size,
1321
- height: size,
1607
+ width: shapeWidth,
1608
+ height: shapeHeight,
1322
1609
  }
1323
1610
  break
1324
1611
  case 'down':
@@ -1326,8 +1613,8 @@ class TableManager {
1326
1613
  type: 'downArrow',
1327
1614
  fill: iconFill || '#EF4444',
1328
1615
  border: null,
1329
- width: size,
1330
- height: size,
1616
+ width: shapeWidth,
1617
+ height: shapeHeight,
1331
1618
  }
1332
1619
  break
1333
1620
  case 'arrow-right':
@@ -1335,8 +1622,8 @@ class TableManager {
1335
1622
  type: 'rightArrow',
1336
1623
  fill: iconFill || '#3B82F6',
1337
1624
  border: null,
1338
- width: size,
1339
- height: size,
1625
+ width: shapeWidth,
1626
+ height: shapeHeight,
1340
1627
  }
1341
1628
  break
1342
1629
  case 'arrow-left':
@@ -1344,19 +1631,16 @@ class TableManager {
1344
1631
  type: 'leftArrow',
1345
1632
  fill: iconFill || '#3B82F6',
1346
1633
  border: null,
1347
- width: size,
1348
- height: size,
1634
+ width: shapeWidth,
1635
+ height: shapeHeight,
1349
1636
  }
1350
1637
  break
1351
1638
  default:
1352
1639
  return []
1353
1640
  }
1354
1641
 
1355
- const x = cellLeft_px + (config.x !== undefined ? config.x : (cellWidth_px - size) / 2)
1356
- const y = cellTop_px + (config.y !== undefined ? config.y : (cellHeight_px - size) / 2)
1357
-
1358
- baseConfig.x = x
1359
- baseConfig.y = y
1642
+ baseConfig.x = shapeLeft
1643
+ baseConfig.y = shapeTop
1360
1644
  baseConfig.zIndex = config.zIndex
1361
1645
  if (config.border) baseConfig.border = config.border
1362
1646
  if (config.transparency !== undefined) baseConfig.transparency = config.transparency
@@ -1366,58 +1650,11 @@ class TableManager {
1366
1650
  return [baseConfig]
1367
1651
  }
1368
1652
 
1369
- const shapeWidth =
1370
- config.width !== undefined
1371
- ? config.width
1372
- : config.size !== undefined
1373
- ? config.size
1374
- : config.radius !== undefined
1375
- ? config.radius * 2
1376
- : 12
1377
- const shapeHeight =
1378
- config.height !== undefined
1379
- ? config.height
1380
- : config.size !== undefined
1381
- ? config.size
1382
- : config.radius !== undefined
1383
- ? config.radius * 2
1384
- : 12
1385
-
1386
- let shapeLeft = cellLeft_px
1387
- let shapeTop = cellTop_px
1388
-
1389
- if (config.position === 'center') {
1390
- shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
1391
- shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
1392
- } else if (config.position === 'left') {
1393
- shapeLeft = cellLeft_px + 5
1394
- shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
1395
- } else if (config.position === 'right') {
1396
- shapeLeft = cellLeft_px + cellWidth_px - shapeWidth - 5
1397
- shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
1398
- } else if (config.position === 'top') {
1399
- shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
1400
- shapeTop = cellTop_px + 5
1401
- } else if (config.position === 'bottom') {
1402
- shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
1403
- shapeTop = cellTop_px + cellHeight_px - shapeHeight - 5
1404
- } else {
1405
- if (config.x === undefined && config.y === undefined) {
1406
- shapeLeft = cellLeft_px + (cellWidth_px - shapeWidth) / 2
1407
- shapeTop = cellTop_px + (cellHeight_px - shapeHeight) / 2
1408
- }
1409
- }
1410
-
1411
- if (config.x !== undefined) {
1412
- shapeLeft += config.x
1413
- }
1414
- if (config.y !== undefined) {
1415
- shapeTop += config.y
1416
- }
1417
-
1418
1653
  const expanded = Object.assign({}, config, {
1419
1654
  x: shapeLeft,
1420
1655
  y: shapeTop,
1656
+ width: shapeWidth,
1657
+ height: shapeHeight,
1421
1658
  })
1422
1659
 
1423
1660
  if (expanded.type === 'circle' && expanded.radius === undefined) {
@@ -1458,6 +1695,8 @@ class TableManager {
1458
1695
  }
1459
1696
  }
1460
1697
 
1698
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
1699
+
1461
1700
  const xfrm = frameObj['p:xfrm']
1462
1701
  const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
1463
1702
  const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
@@ -1561,6 +1800,69 @@ class TableManager {
1561
1800
  })
1562
1801
  }
1563
1802
 
1803
+ getTableRows(slideIndex, tableId, options = {}, slideManager) {
1804
+ const { tblObj } = this.#getTableContext(slideIndex, tableId, slideManager)
1805
+ const trs = tblObj['a:tr'] || []
1806
+ if (trs.length === 0) {
1807
+ return options.includeMetadata
1808
+ ? { rows: [], rowCount: 0, columnCount: 0, mergedCells: [] }
1809
+ : []
1810
+ }
1811
+
1812
+ const numRows = trs.length
1813
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
1814
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
1815
+ const numCols = gridColsArr.length
1816
+
1817
+ // Extract all raw cell text, resolving merges to their parent's text
1818
+ const matrix = []
1819
+ for (let r = 0; r < numRows; r++) {
1820
+ const rowCells = []
1821
+ for (let c = 0; c < numCols; c++) {
1822
+ const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
1823
+ const cell = trs[parent.row]?.['a:tc']?.[parent.col]
1824
+ const text = cell ? this.#getCellText(cell) : ''
1825
+ rowCells.push(text)
1826
+ }
1827
+ matrix.push(rowCells)
1828
+ }
1829
+
1830
+ // Header names are extracted from the first row (index 0)
1831
+ const headerNames = matrix[0].map((hText, cIdx) => {
1832
+ const cleaned = hText.trim()
1833
+ return cleaned || `column${cIdx + 1}`
1834
+ })
1835
+
1836
+ // Compute the data rows (excluding the header row at index 0)
1837
+ const dataRows = matrix.slice(1)
1838
+
1839
+ let rowsResult = []
1840
+ if (options.raw) {
1841
+ rowsResult = dataRows
1842
+ } else {
1843
+ for (const rowCells of dataRows) {
1844
+ const rowObj = {}
1845
+ for (let c = 0; c < numCols; c++) {
1846
+ const key = headerNames[c]
1847
+ rowObj[key] = rowCells[c] || ''
1848
+ }
1849
+ rowsResult.push(rowObj)
1850
+ }
1851
+ }
1852
+
1853
+ if (options.includeMetadata) {
1854
+ const mergedCells = this.getMergedCells(slideIndex, tableId, slideManager)
1855
+ return {
1856
+ rows: rowsResult,
1857
+ rowCount: numRows,
1858
+ columnCount: numCols,
1859
+ mergedCells,
1860
+ }
1861
+ }
1862
+
1863
+ return rowsResult
1864
+ }
1865
+
1564
1866
  addCellShape(slideIndex, tableId, rowIndex, colIndex, options, slideManager, shapeManager) {
1565
1867
  const { tblObj, frameObj, resolvedTableId } = this.#getTableContext(
1566
1868
  slideIndex,
@@ -1568,6 +1870,8 @@ class TableManager {
1568
1870
  slideManager
1569
1871
  )
1570
1872
 
1873
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
1874
+
1571
1875
  const xfrm = frameObj['p:xfrm']
1572
1876
  const tableX = xfrm?.['a:off']?.['@_x'] ? parseInt(xfrm['a:off']['@_x'], 10) : 0
1573
1877
  const tableY = xfrm?.['a:off']?.['@_y'] ? parseInt(xfrm['a:off']['@_y'], 10) : 0
@@ -1651,6 +1955,8 @@ class TableManager {
1651
1955
  slideManager
1652
1956
  )
1653
1957
 
1958
+ this.#calculateRowHeights(slideIndex, tableId, slideManager, tblObj)
1959
+
1654
1960
  const shapes = shapeManager.getShapes(slideIndex, slideManager)
1655
1961
  const prefix = `cellshape_${resolvedTableId}_${rowIndex}_${colIndex}_${shapeIndex}`
1656
1962
  const matchingShapes = shapes.filter(
@@ -1786,6 +2092,297 @@ class TableManager {
1786
2092
  }
1787
2093
  }
1788
2094
 
2095
+ #calculateRowHeights(slideIndex, tableId, slideManager, tblObj) {
2096
+ const trsArr = tblObj['a:tr'] || []
2097
+ if (trsArr.length === 0) return []
2098
+
2099
+ const gridCols = tblObj['a:tblGrid']?.['a:gridCol'] || []
2100
+ const gridColsArr = Array.isArray(gridCols) ? gridCols : [gridCols]
2101
+ const colWidths = gridColsArr.map(col => parseInt(col['@_w'] || 0, 10))
2102
+
2103
+ const numRows = trsArr.length
2104
+ const numCols = colWidths.length
2105
+
2106
+ // Initialize rowHeights with original height or 0
2107
+ const rowHeights = trsArr.map(row => parseInt(row['@_h'] || 0, 10))
2108
+
2109
+ // Helper to get paragraph font size
2110
+ const getParagraphFontSize = p => {
2111
+ let maxSz = 14 // default 14pt
2112
+ if (p['a:pPr']?.['a:defRPr']?.['@_sz']) {
2113
+ maxSz = parseInt(p['a:pPr']['a:defRPr']['@_sz'], 10) / 100
2114
+ }
2115
+ if (p['a:r']) {
2116
+ const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
2117
+ for (const r of runs) {
2118
+ if (r['a:rPr']?.['@_sz']) {
2119
+ const szVal = parseInt(r['a:rPr']['@_sz'], 10) / 100
2120
+ if (szVal > maxSz) {
2121
+ maxSz = szVal
2122
+ }
2123
+ }
2124
+ }
2125
+ }
2126
+ return maxSz
2127
+ }
2128
+
2129
+ // Helper to wrap text
2130
+ const wrapText = (text, availWidth_px, fontSize) => {
2131
+ const charWidth = fontSize * 0.65
2132
+ const words = text.split(/(\s+)/)
2133
+ let linesCount = 0
2134
+ let currentLineLen = 0
2135
+
2136
+ for (const word of words) {
2137
+ if (!word) continue
2138
+ const wordWidth = word.length * charWidth
2139
+ if (wordWidth > availWidth_px) {
2140
+ if (currentLineLen > 0) {
2141
+ linesCount++
2142
+ currentLineLen = 0
2143
+ }
2144
+ let remainingWidth = wordWidth
2145
+ while (remainingWidth > 0) {
2146
+ linesCount++
2147
+ remainingWidth -= availWidth_px
2148
+ }
2149
+ } else {
2150
+ if (currentLineLen + wordWidth > availWidth_px) {
2151
+ linesCount++
2152
+ currentLineLen = word.trim() ? wordWidth : 0
2153
+ } else {
2154
+ currentLineLen += wordWidth
2155
+ }
2156
+ }
2157
+ }
2158
+ if (currentLineLen > 0 || linesCount === 0) {
2159
+ linesCount++
2160
+ }
2161
+ return linesCount
2162
+ }
2163
+
2164
+ // Helper to get cell margins
2165
+ const getCellMargins = cell => {
2166
+ const tcPr = cell['a:tcPr']
2167
+ const marL = tcPr?.['@_marL'] !== undefined ? parseInt(tcPr['@_marL'], 10) : 91440
2168
+ const marR = tcPr?.['@_marR'] !== undefined ? parseInt(tcPr['@_marR'], 10) : 91440
2169
+ const marT = tcPr?.['@_marT'] !== undefined ? parseInt(tcPr['@_marT'], 10) : 45720
2170
+ const marB = tcPr?.['@_marB'] !== undefined ? parseInt(tcPr['@_marB'], 10) : 45720
2171
+ return { marL, marR, marT, marB }
2172
+ }
2173
+
2174
+ // Calculate required height for each cell
2175
+ const cellHeights = Array.from({ length: numRows }, () => new Array(numCols).fill(0))
2176
+
2177
+ for (let r = 0; r < numRows; r++) {
2178
+ const row = trsArr[r]
2179
+ const tcs = row['a:tc'] || []
2180
+ for (let c = 0; c < numCols; c++) {
2181
+ const cell = tcs[c]
2182
+ if (!cell || cell['@_hMerge'] || cell['@_vMerge']) continue
2183
+
2184
+ const parent = this.getMergeParent(slideIndex, tableId, r, c, slideManager)
2185
+ const gridSpan = cell['@_gridSpan'] ? parseInt(cell['@_gridSpan'], 10) : 1
2186
+
2187
+ // Calculate cell width
2188
+ let cellWidth = 0
2189
+ for (let idx = 0; idx < gridSpan; idx++) {
2190
+ cellWidth += colWidths[parent.col + idx] || 0
2191
+ }
2192
+
2193
+ const { marL, marR, marT, marB } = getCellMargins(cell)
2194
+ const availWidth = cellWidth - marL - marR
2195
+ const availWidth_px = Math.max(1, availWidth / 9525)
2196
+
2197
+ // Calculate text height
2198
+ const txBody = cell['a:txBody']
2199
+ let textHeight_emu = 0
2200
+ if (txBody) {
2201
+ const paras = Array.isArray(txBody['a:p']) ? txBody['a:p'] : [txBody['a:p']]
2202
+ for (const p of paras) {
2203
+ const fontSize = getParagraphFontSize(p)
2204
+ let pText = ''
2205
+ if (p['a:r']) {
2206
+ const runs = Array.isArray(p['a:r']) ? p['a:r'] : [p['a:r']]
2207
+ for (const r of runs) {
2208
+ if (r['a:t']) {
2209
+ pText += String(r['a:t'])
2210
+ }
2211
+ }
2212
+ }
2213
+
2214
+ const linesCount = wrapText(pText, availWidth_px, fontSize)
2215
+ const lineHeight_emu = fontSize * 20780 // 1.4 line height multiplier
2216
+
2217
+ let pHeight_emu = linesCount * lineHeight_emu
2218
+ if (p['a:pPr']?.['a:spcBef']?.['a:spcPts']?.['@_val']) {
2219
+ pHeight_emu += parseInt(p['a:pPr']['a:spcBef']['a:spcPts']['@_val'], 10) * 127
2220
+ }
2221
+ if (p['a:pPr']?.['a:spcAft']?.['a:spcPts']?.['@_val']) {
2222
+ pHeight_emu += parseInt(p['a:pPr']['a:spcAft']['a:spcPts']['@_val'], 10) * 127
2223
+ }
2224
+ textHeight_emu += pHeight_emu
2225
+ }
2226
+ }
2227
+
2228
+ const totalCellHeight_emu = marT + marB + textHeight_emu
2229
+ cellHeights[r][c] = totalCellHeight_emu
2230
+ }
2231
+ }
2232
+
2233
+ // Now resolve row heights based on required cell heights
2234
+ // First, non-vertically-merged cells define row heights directly
2235
+ for (let r = 0; r < numRows; r++) {
2236
+ let maxCellHeight = rowHeights[r] // Start with original template height as floor
2237
+ const row = trsArr[r]
2238
+ const tcs = row['a:tc'] || []
2239
+ for (let c = 0; c < numCols; c++) {
2240
+ const cell = tcs[c]
2241
+ if (!cell || cell['@_vMerge'] || cell['@_hMerge']) continue
2242
+ const rowSpan = cell['@_rowSpan'] ? parseInt(cell['@_rowSpan'], 10) : 1
2243
+ if (rowSpan === 1) {
2244
+ if (cellHeights[r][c] > maxCellHeight) {
2245
+ maxCellHeight = cellHeights[r][c]
2246
+ }
2247
+ }
2248
+ }
2249
+ rowHeights[r] = maxCellHeight
2250
+ }
2251
+
2252
+ // Next, adjust for vertically merged cells (rowSpan > 1)
2253
+ for (let r = 0; r < numRows; r++) {
2254
+ const row = trsArr[r]
2255
+ const tcs = row['a:tc'] || []
2256
+ for (let c = 0; c < numCols; c++) {
2257
+ const cell = tcs[c]
2258
+ if (!cell || cell['@_vMerge'] || cell['@_hMerge']) continue
2259
+ const rowSpan = cell['@_rowSpan'] ? parseInt(cell['@_rowSpan'], 10) : 1
2260
+ if (rowSpan > 1) {
2261
+ const reqHeight = cellHeights[r][c]
2262
+ // Sum currently allocated row heights for spanned rows
2263
+ let currentSpanHeight = 0
2264
+ for (let idx = 0; idx < rowSpan; idx++) {
2265
+ currentSpanHeight += rowHeights[r + idx] || 0
2266
+ }
2267
+ if (reqHeight > currentSpanHeight) {
2268
+ // Distribute the extra required height equally across all spanned rows
2269
+ const diff = reqHeight - currentSpanHeight
2270
+ const extraPerRow = Math.ceil(diff / rowSpan)
2271
+ for (let idx = 0; idx < rowSpan; idx++) {
2272
+ rowHeights[r + idx] += extraPerRow
2273
+ }
2274
+ }
2275
+ }
2276
+ }
2277
+ }
2278
+
2279
+ // Update row heights in XML
2280
+ for (let r = 0; r < numRows; r++) {
2281
+ trsArr[r]['@_h'] = String(rowHeights[r])
2282
+ }
2283
+
2284
+ return rowHeights
2285
+ }
2286
+
2287
+ #getNestedHeight(val) {
2288
+ if (Array.isArray(val)) {
2289
+ if (val.length === 0) return 1
2290
+ return val.reduce((sum, item) => sum + this.#getNestedHeight(item), 0)
2291
+ }
2292
+ return 1
2293
+ }
2294
+
2295
+ #expandCellVal(val, targetHeight) {
2296
+ if (!Array.isArray(val)) {
2297
+ const res = []
2298
+ res.push({ value: val !== undefined ? val : '', rowSpan: targetHeight })
2299
+ for (let i = 1; i < targetHeight; i++) {
2300
+ res.push({ vMerge: true })
2301
+ }
2302
+ return res
2303
+ }
2304
+
2305
+ if (val.length === 0) {
2306
+ const res = []
2307
+ res.push({ value: '', rowSpan: targetHeight })
2308
+ for (let i = 1; i < targetHeight; i++) {
2309
+ res.push({ vMerge: true })
2310
+ }
2311
+ return res
2312
+ }
2313
+
2314
+ const itemHeights = val.map(item => this.#getNestedHeight(item))
2315
+ const currentSum = itemHeights.reduce((a, b) => a + b, 0)
2316
+
2317
+ const allocatedHeights = []
2318
+ let remaining = targetHeight
2319
+ for (let i = 0; i < val.length; i++) {
2320
+ const share = Math.round((itemHeights[i] / currentSum) * targetHeight)
2321
+ allocatedHeights.push(share)
2322
+ remaining -= share
2323
+ }
2324
+
2325
+ if (remaining !== 0) {
2326
+ let idx = 0
2327
+ while (remaining > 0) {
2328
+ allocatedHeights[idx % allocatedHeights.length]++
2329
+ remaining--
2330
+ idx++
2331
+ }
2332
+ while (remaining < 0) {
2333
+ let reduced = false
2334
+ for (let i = 0; i < allocatedHeights.length; i++) {
2335
+ const actualIdx = (idx + i) % allocatedHeights.length
2336
+ if (allocatedHeights[actualIdx] > 1) {
2337
+ allocatedHeights[actualIdx]--
2338
+ remaining++
2339
+ reduced = true
2340
+ break
2341
+ }
2342
+ }
2343
+ if (!reduced) break
2344
+ idx++
2345
+ }
2346
+ }
2347
+
2348
+ const result = []
2349
+ for (let i = 0; i < val.length; i++) {
2350
+ result.push(...this.#expandCellVal(val[i], allocatedHeights[i]))
2351
+ }
2352
+ return result
2353
+ }
2354
+
2355
+ #applyAutoMerge(cells) {
2356
+ const result = [...cells]
2357
+ let i = 0
2358
+ while (i < result.length) {
2359
+ const cell = result[i]
2360
+ if (cell.vMerge) {
2361
+ i++
2362
+ continue
2363
+ }
2364
+ let count = 1
2365
+ let j = i + 1
2366
+ while (
2367
+ j < result.length &&
2368
+ !result[j].vMerge &&
2369
+ result[j].value === cell.value &&
2370
+ cell.value !== ''
2371
+ ) {
2372
+ count++
2373
+ j++
2374
+ }
2375
+ if (count > 1) {
2376
+ cell.rowSpan = count
2377
+ for (let k = i + 1; k < j; k++) {
2378
+ result[k] = { vMerge: true }
2379
+ }
2380
+ }
2381
+ i = j
2382
+ }
2383
+ return result
2384
+ }
2385
+
1789
2386
  #generateRandomUint32() {
1790
2387
  return Math.floor(Math.random() * 4294967296)
1791
2388
  }