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.
- package/CHANGELOG.md +0 -16
- package/README.md +10 -34
- package/package.json +1 -2
- package/src/core/PPTXTemplater.js +61 -11
- package/src/managers/ShapeManager.js +296 -85
- package/src/managers/TableManager.js +700 -88
|
@@ -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
|
|
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 =
|
|
740
|
+
const type = normalized.type
|
|
604
741
|
let preset = 'rect'
|
|
605
|
-
let width =
|
|
606
|
-
let height =
|
|
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
|
-
|
|
611
|
-
|
|
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 = (
|
|
615
|
-
height = (
|
|
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
|
-
[
|
|
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((
|
|
627
|
-
const yEmu = Math.round((
|
|
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 =
|
|
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 (
|
|
636
|
-
if (typeof
|
|
637
|
-
const hex =
|
|
784
|
+
if (normalized.fill) {
|
|
785
|
+
if (typeof normalized.fill === 'string') {
|
|
786
|
+
const hex = normalized.fill.replace('#', '').toUpperCase()
|
|
638
787
|
const alphaXml =
|
|
639
|
-
|
|
640
|
-
? `<a:alpha val="${Math.round((100 -
|
|
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
|
|
644
|
-
const colors =
|
|
645
|
-
const 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 (
|
|
668
|
-
const bColor = (
|
|
669
|
-
const bWidth = Math.round((
|
|
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 (
|
|
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((
|
|
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 (
|
|
840
|
+
if (normalized.shadow) {
|
|
692
841
|
let blur = 5
|
|
693
842
|
let distance = 3
|
|
694
843
|
let opacity = 50
|
|
695
|
-
if (typeof
|
|
696
|
-
if (
|
|
697
|
-
if (
|
|
698
|
-
if (
|
|
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
|
-
|
|
863
|
+
normalized.rotation !== undefined ? ` rot="${Math.round(normalized.rotation * 60000)}"` : ''
|
|
715
864
|
|
|
716
865
|
// Text box body properties
|
|
717
866
|
let txBodyXml = ''
|
|
718
|
-
if (
|
|
719
|
-
const 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(
|
|
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 =
|
|
819
|
-
let h =
|
|
820
|
-
if (
|
|
821
|
-
w =
|
|
822
|
-
h =
|
|
823
|
-
} else if (
|
|
824
|
-
w =
|
|
825
|
-
h =
|
|
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 (
|
|
831
|
-
if (
|
|
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 (
|
|
837
|
-
if (
|
|
990
|
+
if (normalized.rotation !== undefined) {
|
|
991
|
+
if (normalized.rotation === null) {
|
|
838
992
|
delete xfrm['@_rot']
|
|
839
993
|
} else {
|
|
840
|
-
xfrm['@_rot'] = String(Math.round(
|
|
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 (
|
|
1000
|
+
if (normalized.type) {
|
|
847
1001
|
let preset = 'rect'
|
|
848
|
-
if (
|
|
849
|
-
else if (
|
|
850
|
-
else if (
|
|
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
|
-
[
|
|
853
|
-
|
|
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 =
|
|
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 (
|
|
866
|
-
if (typeof
|
|
867
|
-
const hex =
|
|
1027
|
+
if (normalized.fill) {
|
|
1028
|
+
if (typeof normalized.fill === 'string') {
|
|
1029
|
+
const hex = normalized.fill.replace('#', '').toUpperCase()
|
|
868
1030
|
const alphaXml =
|
|
869
|
-
|
|
870
|
-
? `<a:alpha val="${Math.round((100 -
|
|
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
|
|
874
|
-
const colors =
|
|
875
|
-
const 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 (
|
|
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 -
|
|
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 (
|
|
1077
|
+
if (normalized.border) {
|
|
916
1078
|
delete spPr['a:ln']
|
|
917
|
-
const bColor = (
|
|
918
|
-
const bWidth = Math.round((
|
|
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 (
|
|
1089
|
+
if (normalized.shadow !== undefined) {
|
|
928
1090
|
delete spPr['a:effectLst']
|
|
929
|
-
if (
|
|
1091
|
+
if (normalized.shadow) {
|
|
930
1092
|
let blur = 5
|
|
931
1093
|
let distance = 3
|
|
932
1094
|
let opacity = 50
|
|
933
|
-
if (typeof
|
|
934
|
-
if (
|
|
935
|
-
if (
|
|
936
|
-
if (
|
|
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 (
|
|
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((
|
|
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 (
|
|
976
|
-
let textVal =
|
|
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 =
|
|
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 (
|
|
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'
|