node-pptx-templater 1.1.3 → 1.1.6

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.
@@ -423,6 +423,139 @@ class ShapeManager {
423
423
  }
424
424
  }
425
425
 
426
+ /**
427
+ * Cleans hex color values and maps color/border aliases.
428
+ *
429
+ * @param {Object} options Shape configuration options.
430
+ * @returns {Object} Normalized shape options.
431
+ */
432
+ normalizeShapeOptions(options) {
433
+ if (!options) return options
434
+ const normalized = { ...options }
435
+
436
+ // Fallback mapping between id and name for compatibility
437
+ if (normalized.id === undefined && normalized.name !== undefined) {
438
+ normalized.id = normalized.name
439
+ }
440
+ if (normalized.name === undefined && normalized.id !== undefined) {
441
+ normalized.name = normalized.id
442
+ }
443
+
444
+ // 1. Map color alias to fill
445
+ const fillVal = options.fill !== undefined ? options.fill : options.color
446
+ if (fillVal !== undefined) {
447
+ normalized.fill = fillVal
448
+ }
449
+
450
+ // 2. Opacity to transparency mapping
451
+ if (options.opacity !== undefined) {
452
+ normalized.transparency = Math.round((1 - parseFloat(options.opacity)) * 100)
453
+ }
454
+
455
+ // 3. Border/Stroke mapping
456
+ const strokeVal = options.stroke !== undefined ? options.stroke : options.border
457
+ if (strokeVal !== undefined && strokeVal !== null) {
458
+ const strokeWidth =
459
+ options.strokeWidth !== undefined
460
+ ? options.strokeWidth
461
+ : options.borderWidth !== undefined
462
+ ? options.borderWidth
463
+ : 1
464
+ if (typeof strokeVal === 'string') {
465
+ normalized.border = {
466
+ color: strokeVal,
467
+ width: parseFloat(strokeWidth),
468
+ }
469
+ } else if (typeof strokeVal === 'object') {
470
+ normalized.border = {
471
+ color: strokeVal.color || strokeVal.fill || '#000000',
472
+ width: parseFloat(strokeVal.width !== undefined ? strokeVal.width : strokeWidth),
473
+ }
474
+ }
475
+ } else if (options.strokeWidth !== undefined || options.borderWidth !== undefined) {
476
+ const strokeWidth =
477
+ options.strokeWidth !== undefined ? options.strokeWidth : options.borderWidth
478
+ normalized.border = {
479
+ color: '#000000',
480
+ width: parseFloat(strokeWidth),
481
+ }
482
+ }
483
+
484
+ // 4. Hex Color cleaning
485
+ const cleanHexColor = colorStr => {
486
+ if (typeof colorStr !== 'string') return colorStr
487
+ let clean = colorStr.replace('#', '').trim()
488
+ if (clean.length === 3) {
489
+ clean = clean[0] + clean[0] + clean[1] + clean[1] + clean[2] + clean[2]
490
+ } else if (clean.length === 8) {
491
+ // Support RRGGBBAA format: slice RRGGBB and use AA to override transparency if not explicitly set
492
+ const rgbPart = clean.slice(0, 6)
493
+ const alphaPart = clean.slice(6, 8)
494
+ if (options.opacity === undefined && options.transparency === undefined) {
495
+ const alphaVal = parseInt(alphaPart, 16)
496
+ normalized.transparency = Math.round((1 - alphaVal / 255) * 100)
497
+ }
498
+ clean = rgbPart
499
+ }
500
+ return '#' + clean.toUpperCase()
501
+ }
502
+
503
+ if (typeof normalized.fill === 'string') {
504
+ normalized.fill = cleanHexColor(normalized.fill)
505
+ } else if (typeof normalized.fill === 'object' && normalized.fill?.type === 'gradient') {
506
+ if (Array.isArray(normalized.fill.colors)) {
507
+ normalized.fill.colors = normalized.fill.colors.map(c => cleanHexColor(c))
508
+ }
509
+ }
510
+
511
+ if (normalized.border && typeof normalized.border.color === 'string') {
512
+ normalized.border.color = cleanHexColor(normalized.border.color)
513
+ }
514
+
515
+ return normalized
516
+ }
517
+
518
+ #reorderSpPrKeys(spPr) {
519
+ if (!spPr) return
520
+ const order = [
521
+ '@_bwMode',
522
+ 'a:xfrm',
523
+ 'a:custGeom',
524
+ 'a:prstGeom',
525
+ 'a:noFill',
526
+ 'a:solidFill',
527
+ 'a:gradFill',
528
+ 'a:blipFill',
529
+ 'a:pattFill',
530
+ 'a:grpFill',
531
+ 'a:ln',
532
+ 'a:effectLst',
533
+ 'a:effectDag',
534
+ 'a:scene3d',
535
+ 'a:sp3d',
536
+ 'a:extLst',
537
+ ]
538
+
539
+ const newSpPr = {}
540
+ for (const key of order) {
541
+ if (spPr[key] !== undefined) {
542
+ newSpPr[key] = spPr[key]
543
+ }
544
+ }
545
+ for (const key of Object.keys(spPr)) {
546
+ if (!order.includes(key)) {
547
+ newSpPr[key] = spPr[key]
548
+ }
549
+ }
550
+
551
+ for (const key of Object.keys(spPr)) {
552
+ delete spPr[key]
553
+ }
554
+ for (const key of Object.keys(newSpPr)) {
555
+ spPr[key] = newSpPr[key]
556
+ }
557
+ }
558
+
426
559
  /**
427
560
  * Validates shape configuration options.
428
561
  *
@@ -455,6 +588,9 @@ class ShapeManager {
455
588
  'downArrow',
456
589
  'leftArrow',
457
590
  'rightArrow',
591
+ 'diamond',
592
+ 'hexagon',
593
+ 'line',
458
594
  ]
459
595
  if (!options.type) {
460
596
  errors.push('Shape type is missing.')
@@ -579,7 +715,8 @@ class ShapeManager {
579
715
  * Adds a new shape to a slide.
580
716
  */
581
717
  addShape(slideIndex, options, slideManager) {
582
- const errors = this.validateShape(options)
718
+ const normalized = this.normalizeShapeOptions(options)
719
+ const errors = this.validateShape(normalized)
583
720
  if (errors.length > 0) {
584
721
  throw new PPTXError(`Shape validation failed:\n- ${errors.join('\n- ')}`)
585
722
  }
@@ -600,55 +737,67 @@ class ShapeManager {
600
737
  const newId = maxId + 1
601
738
 
602
739
  // Build shape XML
603
- const type = options.type
740
+ const type = normalized.type
604
741
  let preset = 'rect'
605
- let width = options.width || 100
606
- let height = options.height || 100
742
+ let width = normalized.width || 100
743
+ let height = normalized.height || 100
607
744
 
608
- if (type === 'square') {
745
+ if (type === 'square' || type === 'rectangle') {
609
746
  preset = 'rect'
610
- width = options.size || 100
611
- height = options.size || 100
747
+ if (type === 'square') {
748
+ width = normalized.size || 100
749
+ height = normalized.size || 100
750
+ }
612
751
  } else if (type === 'circle') {
613
752
  preset = 'ellipse'
614
- width = (options.radius || 50) * 2
615
- height = (options.radius || 50) * 2
753
+ width = (normalized.radius || 50) * 2
754
+ height = (normalized.radius || 50) * 2
616
755
  } else if (type === 'ellipse') {
617
756
  preset = 'ellipse'
618
757
  } else if (type === 'roundedRectangle') {
619
758
  preset = 'roundRect'
620
759
  } else if (
621
- ['triangle', 'star5', 'upArrow', 'downArrow', 'leftArrow', 'rightArrow'].includes(type)
760
+ [
761
+ 'triangle',
762
+ 'star5',
763
+ 'upArrow',
764
+ 'downArrow',
765
+ 'leftArrow',
766
+ 'rightArrow',
767
+ 'diamond',
768
+ 'hexagon',
769
+ 'line',
770
+ ].includes(type)
622
771
  ) {
623
772
  preset = type
624
773
  }
625
774
 
626
- const xEmu = Math.round((options.x || 0) * 9525)
627
- const yEmu = Math.round((options.y || 0) * 9525)
775
+ const xEmu = Math.round((normalized.x || 0) * 9525)
776
+ const yEmu = Math.round((normalized.y || 0) * 9525)
628
777
  const wEmu = Math.round(width * 9525)
629
778
  const hEmu = Math.round(height * 9525)
630
779
 
631
- const name = options.id || `${type.charAt(0).toUpperCase() + type.slice(1)} ${newId}`
780
+ const name = normalized.id || `${type.charAt(0).toUpperCase() + type.slice(1)} ${newId}`
632
781
 
633
782
  // Fill properties
634
783
  let fillXml = '<a:solidFill><a:srgbClr val="FFFFFF"/></a:solidFill>' // default
635
- if (options.fill) {
636
- if (typeof options.fill === 'string') {
637
- const hex = options.fill.replace('#', '')
784
+ if (normalized.fill) {
785
+ if (typeof normalized.fill === 'string') {
786
+ const hex = normalized.fill.replace('#', '').toUpperCase()
638
787
  const alphaXml =
639
- options.transparency !== undefined
640
- ? `<a:alpha val="${Math.round((100 - options.transparency) * 1000)}"/>`
788
+ normalized.transparency !== undefined
789
+ ? `<a:alpha val="${Math.round((100 - normalized.transparency) * 1000)}"/>`
641
790
  : ''
642
791
  fillXml = `<a:solidFill><a:srgbClr val="${hex}">${alphaXml}</a:srgbClr></a:solidFill>`
643
- } else if (typeof options.fill === 'object' && options.fill.type === 'gradient') {
644
- const colors = options.fill.colors || []
645
- const transparency = options.transparency
792
+ } else if (typeof normalized.fill === 'object' && normalized.fill.type === 'gradient') {
793
+ const colors = normalized.fill.colors || []
794
+ const transparency = normalized.transparency
646
795
  fillXml = `<a:gradFill flip="none" rotWithShape="1">
647
796
  <a:gsLst>
648
797
  ${colors
649
798
  .map((color, i) => {
650
799
  const pos = Math.round((i / (colors.length - 1)) * 100000)
651
- const hexColor = color.replace('#', '')
800
+ const hexColor = color.replace('#', '').toUpperCase()
652
801
  const alphaXml =
653
802
  transparency !== undefined
654
803
  ? `<a:alpha val="${Math.round((100 - transparency) * 1000)}"/>`
@@ -664,9 +813,9 @@ class ShapeManager {
664
813
 
665
814
  // Border properties
666
815
  let borderXml = ''
667
- if (options.border) {
668
- const bColor = (options.border.color || '#000000').replace('#', '')
669
- const bWidth = Math.round((options.border.width || 1) * 9525)
816
+ if (normalized.border) {
817
+ const bColor = (normalized.border.color || '#000000').replace('#', '').toUpperCase()
818
+ const bWidth = Math.round((normalized.border.width || 1) * 9525)
670
819
  borderXml = `<a:ln w="${bWidth}">
671
820
  <a:solidFill><a:srgbClr val="${bColor}"/></a:solidFill>
672
821
  </a:ln>`
@@ -676,11 +825,11 @@ class ShapeManager {
676
825
  let avLstXml = '<a:avLst/>'
677
826
  if (preset === 'roundRect') {
678
827
  let adjVal = 16667 // PPT default
679
- if (options.borderRadius !== undefined) {
828
+ if (normalized.borderRadius !== undefined) {
680
829
  const shorterSide = Math.min(width, height)
681
830
  adjVal = Math.min(
682
831
  50000,
683
- Math.max(0, Math.round((options.borderRadius / shorterSide) * 100000))
832
+ Math.max(0, Math.round((normalized.borderRadius / shorterSide) * 100000))
684
833
  )
685
834
  }
686
835
  avLstXml = `<a:avLst><a:gd name="adj" fmla="val ${adjVal}"/></a:avLst>`
@@ -688,14 +837,14 @@ class ShapeManager {
688
837
 
689
838
  // Shadow properties
690
839
  let shadowXml = ''
691
- if (options.shadow) {
840
+ if (normalized.shadow) {
692
841
  let blur = 5
693
842
  let distance = 3
694
843
  let opacity = 50
695
- if (typeof options.shadow === 'object') {
696
- if (options.shadow.blur !== undefined) blur = options.shadow.blur
697
- if (options.shadow.distance !== undefined) distance = options.shadow.distance
698
- if (options.shadow.opacity !== undefined) opacity = options.shadow.opacity
844
+ if (typeof normalized.shadow === 'object') {
845
+ if (normalized.shadow.blur !== undefined) blur = normalized.shadow.blur
846
+ if (normalized.shadow.distance !== undefined) distance = normalized.shadow.distance
847
+ if (normalized.shadow.opacity !== undefined) opacity = normalized.shadow.opacity
699
848
  }
700
849
  const blurEmu = Math.round(blur * 9525)
701
850
  const distEmu = Math.round(distance * 9525)
@@ -711,12 +860,12 @@ class ShapeManager {
711
860
 
712
861
  // Rotation attribute
713
862
  const rotAttr =
714
- options.rotation !== undefined ? ` rot="${Math.round(options.rotation * 60000)}"` : ''
863
+ normalized.rotation !== undefined ? ` rot="${Math.round(normalized.rotation * 60000)}"` : ''
715
864
 
716
865
  // Text box body properties
717
866
  let txBodyXml = ''
718
- if (options.text !== undefined && options.text !== null) {
719
- const textStyle = options.textStyle || {}
867
+ if (normalized.text !== undefined && normalized.text !== null) {
868
+ const textStyle = normalized.textStyle || {}
720
869
  const fontSizeVal = (textStyle.fontSize || 14) * 100
721
870
  const boldAttr = textStyle.bold ? ' b="1"' : ''
722
871
  const italicAttr = textStyle.italic ? ' i="1"' : ''
@@ -730,11 +879,11 @@ class ShapeManager {
730
879
 
731
880
  let colorFill = ''
732
881
  if (textStyle.color) {
733
- const colorHex = textStyle.color.replace('#', '')
882
+ const colorHex = textStyle.color.replace('#', '').toUpperCase()
734
883
  colorFill = `<a:solidFill><a:srgbClr val="${colorHex}"/></a:solidFill>`
735
884
  }
736
885
 
737
- const lines = String(options.text).split(/\r?\n/)
886
+ const lines = String(normalized.text).split(/\r?\n/)
738
887
  const paragraphsXml = lines
739
888
  .map(line => {
740
889
  return `<a:p>
@@ -781,6 +930,9 @@ class ShapeManager {
781
930
  const parsed = this.#xmlParser.parse(shapeXml, 'shape.xml')['p:sp']
782
931
  const shapeObj = Array.isArray(parsed) ? parsed[0] : parsed
783
932
 
933
+ // Reorder shape properties in-place to comply with schema ordering
934
+ this.#reorderSpPrKeys(shapeObj['p:spPr'])
935
+
784
936
  if (!spTree['p:sp']) {
785
937
  spTree['p:sp'] = []
786
938
  }
@@ -814,46 +966,56 @@ class ShapeManager {
814
966
  throw new PPTXError(`Invalid shape structure for "${shapeId}" on slide ${slideIndex}`)
815
967
  }
816
968
 
969
+ const normalized = this.normalizeShapeOptions(options)
970
+
817
971
  // Update coordinates & dimensions
818
- let w = options.width
819
- let h = options.height
820
- if (options.size !== undefined) {
821
- w = options.size
822
- h = options.size
823
- } else if (options.radius !== undefined) {
824
- w = options.radius * 2
825
- h = options.radius * 2
972
+ let w = normalized.width
973
+ let h = normalized.height
974
+ if (normalized.size !== undefined) {
975
+ w = normalized.size
976
+ h = normalized.size
977
+ } else if (normalized.radius !== undefined) {
978
+ w = normalized.radius * 2
979
+ h = normalized.radius * 2
826
980
  }
827
981
 
828
982
  const xfrm = spPr['a:xfrm']
829
983
  if (xfrm) {
830
- if (options.x !== undefined) xfrm['a:off']['@_x'] = String(Math.round(options.x * 9525))
831
- if (options.y !== undefined) xfrm['a:off']['@_y'] = String(Math.round(options.y * 9525))
984
+ if (normalized.x !== undefined) xfrm['a:off']['@_x'] = String(Math.round(normalized.x * 9525))
985
+ if (normalized.y !== undefined) xfrm['a:off']['@_y'] = String(Math.round(normalized.y * 9525))
832
986
  if (w !== undefined) xfrm['a:ext']['@_cx'] = String(Math.round(w * 9525))
833
987
  if (h !== undefined) xfrm['a:ext']['@_cy'] = String(Math.round(h * 9525))
834
988
 
835
989
  // Update rotation
836
- if (options.rotation !== undefined) {
837
- if (options.rotation === null) {
990
+ if (normalized.rotation !== undefined) {
991
+ if (normalized.rotation === null) {
838
992
  delete xfrm['@_rot']
839
993
  } else {
840
- xfrm['@_rot'] = String(Math.round(options.rotation * 60000))
994
+ xfrm['@_rot'] = String(Math.round(normalized.rotation * 60000))
841
995
  }
842
996
  }
843
997
  }
844
998
 
845
999
  // Update preset geometry type if specified
846
- if (options.type) {
1000
+ if (normalized.type) {
847
1001
  let preset = 'rect'
848
- if (options.type === 'square') preset = 'rect'
849
- else if (options.type === 'circle' || options.type === 'ellipse') preset = 'ellipse'
850
- else if (options.type === 'roundedRectangle') preset = 'roundRect'
1002
+ if (normalized.type === 'square' || normalized.type === 'rectangle') preset = 'rect'
1003
+ else if (normalized.type === 'circle' || normalized.type === 'ellipse') preset = 'ellipse'
1004
+ else if (normalized.type === 'roundedRectangle') preset = 'roundRect'
851
1005
  else if (
852
- ['triangle', 'star5', 'upArrow', 'downArrow', 'leftArrow', 'rightArrow'].includes(
853
- options.type
854
- )
1006
+ [
1007
+ 'triangle',
1008
+ 'star5',
1009
+ 'upArrow',
1010
+ 'downArrow',
1011
+ 'leftArrow',
1012
+ 'rightArrow',
1013
+ 'diamond',
1014
+ 'hexagon',
1015
+ 'line',
1016
+ ].includes(normalized.type)
855
1017
  ) {
856
- preset = options.type
1018
+ preset = normalized.type
857
1019
  }
858
1020
  if (spPr['a:prstGeom']) {
859
1021
  spPr['a:prstGeom']['@_prst'] = preset
@@ -862,23 +1024,23 @@ class ShapeManager {
862
1024
 
863
1025
  // Update fill properties
864
1026
  let fillXml = ''
865
- if (options.fill) {
866
- if (typeof options.fill === 'string') {
867
- const hex = options.fill.replace('#', '')
1027
+ if (normalized.fill) {
1028
+ if (typeof normalized.fill === 'string') {
1029
+ const hex = normalized.fill.replace('#', '').toUpperCase()
868
1030
  const alphaXml =
869
- options.transparency !== undefined
870
- ? `<a:alpha val="${Math.round((100 - options.transparency) * 1000)}"/>`
1031
+ normalized.transparency !== undefined
1032
+ ? `<a:alpha val="${Math.round((100 - normalized.transparency) * 1000)}"/>`
871
1033
  : ''
872
1034
  fillXml = `<a:solidFill><a:srgbClr val="${hex}">${alphaXml}</a:srgbClr></a:solidFill>`
873
- } else if (typeof options.fill === 'object' && options.fill.type === 'gradient') {
874
- const colors = options.fill.colors || []
875
- const transparency = options.transparency
1035
+ } else if (typeof normalized.fill === 'object' && normalized.fill.type === 'gradient') {
1036
+ const colors = normalized.fill.colors || []
1037
+ const transparency = normalized.transparency
876
1038
  fillXml = `<a:gradFill flip="none" rotWithShape="1">
877
1039
  <a:gsLst>
878
1040
  ${colors
879
1041
  .map((color, i) => {
880
1042
  const pos = Math.round((i / (colors.length - 1)) * 100000)
881
- const hexColor = color.replace('#', '')
1043
+ const hexColor = color.replace('#', '').toUpperCase()
882
1044
  const alphaXml =
883
1045
  transparency !== undefined
884
1046
  ? `<a:alpha val="${Math.round((100 - transparency) * 1000)}"/>`
@@ -902,20 +1064,20 @@ class ShapeManager {
902
1064
  const fillVal = parsedFill[fillKey]
903
1065
  spPr[fillKey] = Array.isArray(fillVal) ? fillVal[0] : fillVal
904
1066
  }
905
- } else if (options.transparency !== undefined) {
1067
+ } else if (normalized.transparency !== undefined) {
906
1068
  // Transparency only
907
1069
  const solidFill = spPr['a:solidFill']
908
1070
  if (solidFill && solidFill['a:srgbClr']) {
909
1071
  const srgbClr = solidFill['a:srgbClr']
910
- srgbClr['a:alpha'] = { '@_val': String(Math.round((100 - options.transparency) * 1000)) }
1072
+ srgbClr['a:alpha'] = { '@_val': String(Math.round((100 - normalized.transparency) * 1000)) }
911
1073
  }
912
1074
  }
913
1075
 
914
1076
  // Update border properties
915
- if (options.border) {
1077
+ if (normalized.border) {
916
1078
  delete spPr['a:ln']
917
- const bColor = (options.border.color || '#000000').replace('#', '')
918
- const bWidth = Math.round((options.border.width || 1) * 9525)
1079
+ const bColor = (normalized.border.color || '#000000').replace('#', '').toUpperCase()
1080
+ const bWidth = Math.round((normalized.border.width || 1) * 9525)
919
1081
  const borderXml = `<a:ln w="${bWidth}">
920
1082
  <a:solidFill><a:srgbClr val="${bColor}"/></a:solidFill>
921
1083
  </a:ln>`
@@ -924,16 +1086,16 @@ class ShapeManager {
924
1086
  }
925
1087
 
926
1088
  // Update shadow properties
927
- if (options.shadow !== undefined) {
1089
+ if (normalized.shadow !== undefined) {
928
1090
  delete spPr['a:effectLst']
929
- if (options.shadow) {
1091
+ if (normalized.shadow) {
930
1092
  let blur = 5
931
1093
  let distance = 3
932
1094
  let opacity = 50
933
- if (typeof options.shadow === 'object') {
934
- if (options.shadow.blur !== undefined) blur = options.shadow.blur
935
- if (options.shadow.distance !== undefined) distance = options.shadow.distance
936
- if (options.shadow.opacity !== undefined) opacity = options.shadow.opacity
1095
+ if (typeof normalized.shadow === 'object') {
1096
+ if (normalized.shadow.blur !== undefined) blur = normalized.shadow.blur
1097
+ if (normalized.shadow.distance !== undefined) distance = normalized.shadow.distance
1098
+ if (normalized.shadow.opacity !== undefined) opacity = normalized.shadow.opacity
937
1099
  }
938
1100
  const blurEmu = Math.round(blur * 9525)
939
1101
  const distEmu = Math.round(distance * 9525)
@@ -951,7 +1113,7 @@ class ShapeManager {
951
1113
  }
952
1114
 
953
1115
  // Update border radius
954
- if (options.borderRadius !== undefined) {
1116
+ if (normalized.borderRadius !== undefined) {
955
1117
  const prstGeom = spPr['a:prstGeom']
956
1118
  if (prstGeom && prstGeom['@_prst'] === 'roundRect') {
957
1119
  let curW = 100
@@ -963,7 +1125,7 @@ class ShapeManager {
963
1125
  const shorterSide = Math.min(curW, curH)
964
1126
  const adjVal = Math.min(
965
1127
  50000,
966
- Math.max(0, Math.round((options.borderRadius / shorterSide) * 100000))
1128
+ Math.max(0, Math.round((normalized.borderRadius / shorterSide) * 100000))
967
1129
  )
968
1130
  const avLstXml = `<a:avLst><a:gd name="adj" fmla="val ${adjVal}"/></a:avLst>`
969
1131
  const parsedAvLst = this.#xmlParser.parse(avLstXml, 'avLst.xml')['a:avLst']
@@ -971,14 +1133,17 @@ class ShapeManager {
971
1133
  }
972
1134
  }
973
1135
 
1136
+ // Reorder shape properties in-place to comply with schema ordering
1137
+ this.#reorderSpPrKeys(spPr)
1138
+
974
1139
  // Update text
975
- if (options.text !== undefined || options.textStyle !== undefined) {
976
- let textVal = options.text
1140
+ if (normalized.text !== undefined || normalized.textStyle !== undefined) {
1141
+ let textVal = normalized.text
977
1142
  if (textVal === undefined) {
978
1143
  textVal = this.getShapeText(shape) || ''
979
1144
  }
980
1145
 
981
- const textStyle = options.textStyle || {}
1146
+ const textStyle = normalized.textStyle || {}
982
1147
  const fontSizeVal = (textStyle.fontSize || 14) * 100
983
1148
  const boldAttr = textStyle.bold ? ' b="1"' : ''
984
1149
  const italicAttr = textStyle.italic ? ' i="1"' : ''
@@ -992,7 +1157,7 @@ class ShapeManager {
992
1157
 
993
1158
  let colorFill = ''
994
1159
  if (textStyle.color) {
995
- const colorHex = textStyle.color.replace('#', '')
1160
+ const colorHex = textStyle.color.replace('#', '').toUpperCase()
996
1161
  colorFill = `<a:solidFill><a:srgbClr val="${colorHex}"/></a:solidFill>`
997
1162
  }
998
1163
 
@@ -1081,6 +1246,36 @@ class ShapeManager {
1081
1246
 
1082
1247
  const type = mapPresetToType(preset, width, height)
1083
1248
 
1249
+ // Extract fill
1250
+ let fill = undefined
1251
+ const solidFill = res.shape['p:spPr']?.['a:solidFill']
1252
+ if (solidFill && solidFill['a:srgbClr']) {
1253
+ fill = '#' + (solidFill['a:srgbClr']['@_val'] || '').toUpperCase()
1254
+ }
1255
+
1256
+ // Extract transparency
1257
+ let transparency = undefined
1258
+ if (solidFill && solidFill['a:srgbClr']?.['a:alpha']?.['@_val']) {
1259
+ transparency = Math.round(
1260
+ (100000 - parseInt(solidFill['a:srgbClr']['a:alpha']['@_val'], 10)) / 1000
1261
+ )
1262
+ }
1263
+
1264
+ // Extract border
1265
+ let border = undefined
1266
+ const ln = res.shape['p:spPr']?.['a:ln']
1267
+ if (ln) {
1268
+ const bColor = ln['a:solidFill']?.['a:srgbClr']?.['@_val']
1269
+ const bWidth = ln['@_w'] ? parseInt(ln['@_w'], 10) / 9525 : 1
1270
+ border = {
1271
+ color: bColor ? '#' + bColor.toUpperCase() : '#000000',
1272
+ width: bWidth,
1273
+ }
1274
+ }
1275
+
1276
+ // Extract text
1277
+ const text = this.getShapeText(res.shape) || undefined
1278
+
1084
1279
  return {
1085
1280
  id,
1086
1281
  type,
@@ -1088,6 +1283,10 @@ class ShapeManager {
1088
1283
  y,
1089
1284
  width,
1090
1285
  height,
1286
+ fill,
1287
+ transparency,
1288
+ border,
1289
+ text,
1091
1290
  }
1092
1291
  }
1093
1292
  }
@@ -1111,7 +1310,19 @@ function mapPresetToType(preset, w, h) {
1111
1310
  if (preset === 'roundRect') {
1112
1311
  return 'roundedRectangle'
1113
1312
  }
1114
- if (['triangle', 'star5', 'upArrow', 'downArrow', 'leftArrow', 'rightArrow'].includes(preset)) {
1313
+ if (
1314
+ [
1315
+ 'triangle',
1316
+ 'star5',
1317
+ 'upArrow',
1318
+ 'downArrow',
1319
+ 'leftArrow',
1320
+ 'rightArrow',
1321
+ 'diamond',
1322
+ 'hexagon',
1323
+ 'line',
1324
+ ].includes(preset)
1325
+ ) {
1115
1326
  return preset
1116
1327
  }
1117
1328
  return 'rectangle'