node-pptx-templater 1.1.3 → 1.1.4

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