node-pptx-templater 1.1.2 → 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.
- package/package.json +3 -1
- package/src/core/OutputWriter.js +5 -5
- package/src/core/PPTXTemplater.js +218 -34
- package/src/managers/ShapeManager.js +288 -85
- package/src/managers/TableManager.js +676 -284
- package/src/managers/ZipManager.js +5 -1
|
@@ -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
|
|
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 =
|
|
732
|
+
const type = normalized.type
|
|
604
733
|
let preset = 'rect'
|
|
605
|
-
let width =
|
|
606
|
-
let height =
|
|
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
|
-
|
|
611
|
-
|
|
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 = (
|
|
615
|
-
height = (
|
|
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
|
-
[
|
|
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((
|
|
627
|
-
const yEmu = Math.round((
|
|
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 =
|
|
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 (
|
|
636
|
-
if (typeof
|
|
637
|
-
const hex =
|
|
776
|
+
if (normalized.fill) {
|
|
777
|
+
if (typeof normalized.fill === 'string') {
|
|
778
|
+
const hex = normalized.fill.replace('#', '').toUpperCase()
|
|
638
779
|
const alphaXml =
|
|
639
|
-
|
|
640
|
-
? `<a:alpha val="${Math.round((100 -
|
|
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
|
|
644
|
-
const colors =
|
|
645
|
-
const 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 (
|
|
668
|
-
const bColor = (
|
|
669
|
-
const bWidth = Math.round((
|
|
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 (
|
|
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((
|
|
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 (
|
|
832
|
+
if (normalized.shadow) {
|
|
692
833
|
let blur = 5
|
|
693
834
|
let distance = 3
|
|
694
835
|
let opacity = 50
|
|
695
|
-
if (typeof
|
|
696
|
-
if (
|
|
697
|
-
if (
|
|
698
|
-
if (
|
|
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
|
-
|
|
855
|
+
normalized.rotation !== undefined ? ` rot="${Math.round(normalized.rotation * 60000)}"` : ''
|
|
715
856
|
|
|
716
857
|
// Text box body properties
|
|
717
858
|
let txBodyXml = ''
|
|
718
|
-
if (
|
|
719
|
-
const 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(
|
|
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 =
|
|
819
|
-
let h =
|
|
820
|
-
if (
|
|
821
|
-
w =
|
|
822
|
-
h =
|
|
823
|
-
} else if (
|
|
824
|
-
w =
|
|
825
|
-
h =
|
|
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 (
|
|
831
|
-
if (
|
|
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 (
|
|
837
|
-
if (
|
|
982
|
+
if (normalized.rotation !== undefined) {
|
|
983
|
+
if (normalized.rotation === null) {
|
|
838
984
|
delete xfrm['@_rot']
|
|
839
985
|
} else {
|
|
840
|
-
xfrm['@_rot'] = String(Math.round(
|
|
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 (
|
|
992
|
+
if (normalized.type) {
|
|
847
993
|
let preset = 'rect'
|
|
848
|
-
if (
|
|
849
|
-
else if (
|
|
850
|
-
else if (
|
|
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
|
-
[
|
|
853
|
-
|
|
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 =
|
|
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 (
|
|
866
|
-
if (typeof
|
|
867
|
-
const hex =
|
|
1019
|
+
if (normalized.fill) {
|
|
1020
|
+
if (typeof normalized.fill === 'string') {
|
|
1021
|
+
const hex = normalized.fill.replace('#', '').toUpperCase()
|
|
868
1022
|
const alphaXml =
|
|
869
|
-
|
|
870
|
-
? `<a:alpha val="${Math.round((100 -
|
|
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
|
|
874
|
-
const colors =
|
|
875
|
-
const 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 (
|
|
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 -
|
|
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 (
|
|
1069
|
+
if (normalized.border) {
|
|
916
1070
|
delete spPr['a:ln']
|
|
917
|
-
const bColor = (
|
|
918
|
-
const bWidth = Math.round((
|
|
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 (
|
|
1081
|
+
if (normalized.shadow !== undefined) {
|
|
928
1082
|
delete spPr['a:effectLst']
|
|
929
|
-
if (
|
|
1083
|
+
if (normalized.shadow) {
|
|
930
1084
|
let blur = 5
|
|
931
1085
|
let distance = 3
|
|
932
1086
|
let opacity = 50
|
|
933
|
-
if (typeof
|
|
934
|
-
if (
|
|
935
|
-
if (
|
|
936
|
-
if (
|
|
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 (
|
|
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((
|
|
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 (
|
|
976
|
-
let textVal =
|
|
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 =
|
|
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 (
|
|
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'
|