dom-to-pptx 1.0.2 → 1.0.3

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.
@@ -2,7 +2,26 @@
2
2
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('pptxgenjs')) :
3
3
  typeof define === 'function' && define.amd ? define(['exports', 'pptxgenjs'], factory) :
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.domToPptx = {}, global.PptxGenJS));
5
- })(this, (function (exports, PptxGenJS) { 'use strict';
5
+ })(this, (function (exports, PptxGenJSImport) { 'use strict';
6
+
7
+ function _interopNamespaceDefault(e) {
8
+ var n = Object.create(null);
9
+ if (e) {
10
+ Object.keys(e).forEach(function (k) {
11
+ if (k !== 'default') {
12
+ var d = Object.getOwnPropertyDescriptor(e, k);
13
+ Object.defineProperty(n, k, d.get ? d : {
14
+ enumerable: true,
15
+ get: function () { return e[k]; }
16
+ });
17
+ }
18
+ });
19
+ }
20
+ n.default = e;
21
+ return Object.freeze(n);
22
+ }
23
+
24
+ var PptxGenJSImport__namespace = /*#__PURE__*/_interopNamespaceDefault(PptxGenJSImport);
6
25
 
7
26
  // src/utils.js
8
27
 
@@ -531,475 +550,491 @@
531
550
  });
532
551
  }
533
552
 
534
- // src/index.js
535
-
536
- const PPI = 96;
537
- const PX_TO_INCH = 1 / PPI;
538
-
539
- /**
540
- * Main export function. Accepts single element or an array.
541
- * @param {HTMLElement | string | Array<HTMLElement | string>} target - The root element(s) to convert.
542
- * @param {Object} options - { fileName: string }
543
- */
544
- async function exportToPptx(target, options = {}) {
545
- const pptx = new PptxGenJS();
546
- pptx.layout = 'LAYOUT_16x9';
547
-
548
- // Standardize input to an array, ensuring single or multiple elements are handled consistently
549
- const elements = Array.isArray(target) ? target : [target];
550
-
551
- for (const el of elements) {
552
- const root = typeof el === 'string' ? document.querySelector(el) : el;
553
- if (!root) {
554
- console.warn('Element not found, skipping slide:', el);
555
- continue;
556
- }
557
-
558
- const slide = pptx.addSlide();
559
- await processSlide(root, slide, pptx);
560
- }
561
-
562
- const fileName = options.fileName || 'export.pptx';
563
- pptx.writeFile({ fileName });
564
- }
565
-
566
- /**
567
- * Worker function to process a single DOM element into a single PPTX slide.
568
- * @param {HTMLElement} root - The root element for this slide.
569
- * @param {PptxGenJS.Slide} slide - The PPTX slide object to add content to.
570
- * @param {PptxGenJS} pptx - The main PPTX instance.
571
- */
572
- async function processSlide(root, slide, pptx) {
573
- const rootRect = root.getBoundingClientRect();
574
- const PPTX_WIDTH_IN = 10;
575
- const PPTX_HEIGHT_IN = 5.625;
576
-
577
- const contentWidthIn = rootRect.width * PX_TO_INCH;
578
- const contentHeightIn = rootRect.height * PX_TO_INCH;
579
- const scale = Math.min(PPTX_WIDTH_IN / contentWidthIn, PPTX_HEIGHT_IN / contentHeightIn);
580
-
581
- const layoutConfig = {
582
- rootX: rootRect.x,
583
- rootY: rootRect.y,
584
- scale: scale,
585
- offX: (PPTX_WIDTH_IN - contentWidthIn * scale) / 2,
586
- offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
587
- };
588
-
589
- const renderQueue = [];
590
- let domOrderCounter = 0;
591
-
592
- async function collect(node) {
593
- const order = domOrderCounter++; // Assign a DOM order for z-index tie-breaking
594
- const result = await createRenderItem(node, layoutConfig, order, pptx);
595
- if (result) {
596
- if (result.items) renderQueue.push(...result.items); // Add any generated render items to the queue
597
- if (result.stopRecursion) return; // Stop processing children if the item fully consumed the node
598
- }
599
- for (const child of node.children) await collect(child);
600
- }
601
-
602
- await collect(root);
603
-
604
- renderQueue.sort((a, b) => {
605
- if (a.zIndex !== b.zIndex) return a.zIndex - b.zIndex;
606
- return a.domOrder - b.domOrder;
607
- });
608
-
609
- for (const item of renderQueue) {
610
- if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
611
- if (item.type === 'image') slide.addImage(item.options);
612
- if (item.type === 'text') slide.addText(item.textParts, item.options);
613
- }
614
- }
615
-
616
- async function createRenderItem(node, config, domOrder, pptx) {
617
- if (node.nodeType !== 1) return null;
618
- const style = window.getComputedStyle(node);
619
- if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
620
- return null;
621
-
622
- const rect = node.getBoundingClientRect();
623
- if (rect.width === 0 || rect.height === 0) return null;
624
-
625
- const zIndex = style.zIndex !== 'auto' ? parseInt(style.zIndex) : 0;
626
- const rotation = getRotation(style.transform);
627
- const elementOpacity = parseFloat(style.opacity);
628
-
629
- const widthPx = node.offsetWidth || rect.width;
630
- const heightPx = node.offsetHeight || rect.height;
631
- const unrotatedW = widthPx * PX_TO_INCH * config.scale;
632
- const unrotatedH = heightPx * PX_TO_INCH * config.scale;
633
- const centerX = rect.left + rect.width / 2;
634
- const centerY = rect.top + rect.height / 2;
635
-
636
- let x = config.offX + (centerX - config.rootX) * PX_TO_INCH * config.scale - unrotatedW / 2;
637
- let y = config.offY + (centerY - config.rootY) * PX_TO_INCH * config.scale - unrotatedH / 2;
638
- let w = unrotatedW;
639
- let h = unrotatedH;
640
-
641
- const items = [];
642
-
643
- // Image handling for SVG nodes directly
644
- if (node.nodeName.toUpperCase() === 'SVG') {
645
- const pngData = await svgToPng(node);
646
- if (pngData)
647
- items.push({
648
- type: 'image',
649
- zIndex,
650
- domOrder,
651
- options: { data: pngData, x, y, w, h, rotate: rotation },
652
- });
653
- return { items, stopRecursion: true };
654
- }
655
- // Image handling for <img> tags, including rounded corners
656
- if (node.tagName === 'IMG') {
657
- let borderRadius = parseFloat(style.borderRadius) || 0;
658
- if (borderRadius === 0) {
659
- const parentStyle = window.getComputedStyle(node.parentElement);
660
- if (parentStyle.overflow !== 'visible')
661
- borderRadius = parseFloat(parentStyle.borderRadius) || 0;
662
- }
663
- const processed = await getProcessedImage(node.src, widthPx, heightPx, borderRadius);
664
- if (processed)
665
- items.push({
666
- type: 'image',
667
- zIndex,
668
- domOrder,
669
- options: { data: processed, x, y, w, h, rotate: rotation },
670
- });
671
- return { items, stopRecursion: true };
672
- }
673
-
674
- const bgColorObj = parseColor(style.backgroundColor);
675
- const bgClip = style.webkitBackgroundClip || style.backgroundClip;
676
- const isBgClipText = bgClip === 'text';
677
- const hasGradient =
678
- !isBgClipText && style.backgroundImage && style.backgroundImage.includes('linear-gradient');
679
-
680
- const borderColorObj = parseColor(style.borderColor);
681
- const borderWidth = parseFloat(style.borderWidth);
682
- const hasBorder = borderWidth > 0 && borderColorObj.hex;
683
-
684
- const borderInfo = getBorderInfo(style, config.scale);
685
- const hasUniformBorder = borderInfo.type === 'uniform';
686
- const hasCompositeBorder = borderInfo.type === 'composite';
687
-
688
- const shadowStr = style.boxShadow;
689
- const hasShadow = shadowStr && shadowStr !== 'none';
690
- const borderRadius = parseFloat(style.borderRadius) || 0;
691
- const softEdge = getSoftEdges(style.filter, config.scale);
692
-
693
- let isImageWrapper = false;
694
- const imgChild = Array.from(node.children).find((c) => c.tagName === 'IMG');
695
- if (imgChild) {
696
- const childW = imgChild.offsetWidth || imgChild.getBoundingClientRect().width;
697
- const childH = imgChild.offsetHeight || imgChild.getBoundingClientRect().height;
698
- if (childW >= widthPx - 2 && childH >= heightPx - 2) isImageWrapper = true;
699
- }
700
-
701
- let textPayload = null;
702
- const isText = isTextContainer(node);
703
-
704
- if (isText) {
705
- const textParts = [];
706
- const isList = style.display === 'list-item';
707
- if (isList) {
708
- const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
709
- const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
710
- x -= bulletShift;
711
- w += bulletShift;
712
- textParts.push({
713
- text: '• ',
714
- options: {
715
- // Default bullet point styling
716
- color: parseColor(style.color).hex || '000000',
717
- fontSize: fontSizePt,
718
- },
719
- });
720
- }
721
-
722
- node.childNodes.forEach((child, index) => {
723
- // Process text content, sanitizing whitespace and applying text transformations
724
- let textVal = child.nodeType === 3 ? child.nodeValue : child.textContent;
725
- let nodeStyle = child.nodeType === 1 ? window.getComputedStyle(child) : style;
726
- textVal = textVal.replace(/[\n\r\t]+/g, ' ').replace(/\s{2,}/g, ' ');
727
- if (index === 0 && !isList) textVal = textVal.trimStart();
728
- else if (index === 0) textVal = textVal.trimStart();
729
- if (index === node.childNodes.length - 1) textVal = textVal.trimEnd();
730
- if (nodeStyle.textTransform === 'uppercase') textVal = textVal.toUpperCase();
731
- if (nodeStyle.textTransform === 'lowercase') textVal = textVal.toLowerCase();
732
-
733
- if (textVal.length > 0) {
734
- textParts.push({
735
- text: textVal,
736
- options: getTextStyle(nodeStyle, config.scale),
737
- });
738
- }
739
- });
740
-
741
- if (textParts.length > 0) {
742
- let align = style.textAlign || 'left';
743
- if (align === 'start') align = 'left';
744
- if (align === 'end') align = 'right';
745
- let valign = 'top';
746
- if (style.alignItems === 'center') valign = 'middle';
747
- if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
748
-
749
- const pt = parseFloat(style.paddingTop) || 0;
750
- const pb = parseFloat(style.paddingBottom) || 0;
751
- if (Math.abs(pt - pb) < 2 && bgColorObj.hex) valign = 'middle';
752
-
753
- let padding = getPadding(style, config.scale);
754
- if (align === 'center' && valign === 'middle') padding = [0, 0, 0, 0];
755
-
756
- textPayload = { text: textParts, align, valign, inset: padding };
757
- }
758
- }
759
-
760
- if (hasGradient || (softEdge && bgColorObj.hex && !isImageWrapper)) {
761
- let bgData = null;
762
- let padIn = 0;
763
- if (softEdge) {
764
- const svgInfo = generateBlurredSVG(widthPx, heightPx, bgColorObj.hex, borderRadius, softEdge);
765
- bgData = svgInfo.data;
766
- padIn = svgInfo.padding * PX_TO_INCH * config.scale;
767
- } else {
768
- bgData = generateGradientSVG(
769
- widthPx,
770
- heightPx,
771
- style.backgroundImage,
772
- borderRadius,
773
- hasBorder ? { color: borderColorObj.hex, width: borderWidth } : null
774
- );
775
- }
776
-
777
- if (bgData) {
778
- items.push({
779
- type: 'image',
780
- zIndex,
781
- domOrder,
782
- options: {
783
- data: bgData,
784
- x: x - padIn,
785
- y: y - padIn,
786
- w: w + padIn * 2,
787
- h: h + padIn * 2,
788
- rotate: rotation,
789
- },
790
- });
791
- }
792
-
793
- if (textPayload) {
794
- items.push({
795
- type: 'text',
796
- zIndex: zIndex + 1,
797
- domOrder,
798
- textParts: textPayload.text,
799
- options: {
800
- x,
801
- y,
802
- w,
803
- h,
804
- align: textPayload.align,
805
- valign: textPayload.valign,
806
- inset: textPayload.inset,
807
- rotate: rotation,
808
- margin: 0,
809
- wrap: true,
810
- autoFit: false,
811
- },
812
- });
813
- }
814
- if (hasCompositeBorder) {
815
- // Add border shapes after the main background
816
- const borderItems = createCompositeBorderItems(
817
- borderInfo.sides,
818
- x,
819
- y,
820
- w,
821
- h,
822
- config.scale,
823
- zIndex,
824
- domOrder
825
- );
826
- items.push(...borderItems);
827
- }
828
- } else if (
829
- (bgColorObj.hex && !isImageWrapper) ||
830
- hasUniformBorder ||
831
- hasCompositeBorder ||
832
- hasShadow ||
833
- textPayload
834
- ) {
835
- const finalAlpha = elementOpacity * bgColorObj.opacity;
836
- const transparency = (1 - finalAlpha) * 100;
837
-
838
- const shapeOpts = {
839
- x,
840
- y,
841
- w,
842
- h,
843
- rotate: rotation,
844
- fill:
845
- bgColorObj.hex && !isImageWrapper
846
- ? { color: bgColorObj.hex, transparency: transparency }
847
- : { type: 'none' },
848
- // Only apply line if the border is uniform
849
- line: hasUniformBorder ? borderInfo.options : null,
850
- };
851
-
852
- if (hasShadow) {
853
- shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
854
- }
855
-
856
- const borderRadius = parseFloat(style.borderRadius) || 0;
857
- const widthPx = node.offsetWidth || rect.width;
858
- const heightPx = node.offsetHeight || rect.height;
859
- const isCircle =
860
- borderRadius >= Math.min(widthPx, heightPx) / 2 - 1 && Math.abs(widthPx - heightPx) < 2;
861
-
862
- let shapeType = pptx.ShapeType.rect;
863
- if (isCircle) shapeType = pptx.ShapeType.ellipse;
864
- else if (borderRadius > 0) {
865
- shapeType = pptx.ShapeType.roundRect;
866
- shapeOpts.rectRadius = Math.min(1, borderRadius / (Math.min(widthPx, heightPx) / 2));
867
- }
868
-
869
- // MERGE TEXT INTO SHAPE (if text exists)
870
- if (textPayload) {
871
- const textOptions = {
872
- shape: shapeType,
873
- ...shapeOpts,
874
- align: textPayload.align,
875
- valign: textPayload.valign,
876
- inset: textPayload.inset,
877
- margin: 0,
878
- wrap: true,
879
- autoFit: false,
880
- };
881
- items.push({
882
- type: 'text',
883
- zIndex,
884
- domOrder,
885
- textParts: textPayload.text,
886
- options: textOptions,
887
- });
888
- // If no text, just draw the shape
889
- } else {
890
- items.push({
891
- type: 'shape',
892
- zIndex,
893
- domOrder,
894
- shapeType,
895
- options: shapeOpts,
896
- });
897
- }
898
-
899
- // ADD COMPOSITE BORDERS (if they exist)
900
- if (hasCompositeBorder) {
901
- // Generate a single SVG image that contains all the rounded border sides
902
- const borderSvgData = generateCompositeBorderSVG(
903
- widthPx,
904
- heightPx,
905
- borderRadius,
906
- borderInfo.sides
907
- );
908
-
909
- if (borderSvgData) {
910
- items.push({
911
- type: 'image',
912
- zIndex: zIndex + 1,
913
- domOrder,
914
- options: {
915
- data: borderSvgData,
916
- x: x,
917
- y: y,
918
- w: w,
919
- h: h,
920
- rotate: rotation,
921
- },
922
- });
923
- }
924
- }
925
- }
926
-
927
- return { items, stopRecursion: !!textPayload };
928
- }
929
-
930
- /**
931
- * Helper function to create individual border shapes
932
- */
933
- function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder) {
934
- const items = [];
935
- const pxToInch = 1 / 96;
936
-
937
- // TOP BORDER
938
- if (sides.top.width > 0) {
939
- items.push({
940
- type: 'shape',
941
- zIndex: zIndex + 1,
942
- domOrder,
943
- shapeType: 'rect',
944
- options: {
945
- x: x,
946
- y: y,
947
- w: w,
948
- h: sides.top.width * pxToInch * scale,
949
- fill: { color: sides.top.color },
950
- },
951
- });
952
- }
953
- // RIGHT BORDER
954
- if (sides.right.width > 0) {
955
- items.push({
956
- type: 'shape',
957
- zIndex: zIndex + 1,
958
- domOrder,
959
- shapeType: 'rect',
960
- options: {
961
- x: x + w - sides.right.width * pxToInch * scale,
962
- y: y,
963
- w: sides.right.width * pxToInch * scale,
964
- h: h,
965
- fill: { color: sides.right.color },
966
- },
967
- });
968
- }
969
- // BOTTOM BORDER
970
- if (sides.bottom.width > 0) {
971
- items.push({
972
- type: 'shape',
973
- zIndex: zIndex + 1,
974
- domOrder,
975
- shapeType: 'rect',
976
- options: {
977
- x: x,
978
- y: y + h - sides.bottom.width * pxToInch * scale,
979
- w: w,
980
- h: sides.bottom.width * pxToInch * scale,
981
- fill: { color: sides.bottom.color },
982
- },
983
- });
984
- }
985
- // LEFT BORDER
986
- if (sides.left.width > 0) {
987
- items.push({
988
- type: 'shape',
989
- zIndex: zIndex + 1,
990
- domOrder,
991
- shapeType: 'rect',
992
- options: {
993
- x: x,
994
- y: y,
995
- w: sides.left.width * pxToInch * scale,
996
- h: h,
997
- fill: { color: sides.left.color },
998
- },
999
- });
1000
- }
1001
-
1002
- return items;
553
+ // src/index.js
554
+ // Normalize import so consumers get the constructor whether `pptxgenjs`
555
+ // was published as a default export or CommonJS module with a `default` property.
556
+ const PptxGenJS = PptxGenJSImport__namespace?.default ?? PptxGenJSImport__namespace;
557
+
558
+ const PPI = 96;
559
+ const PX_TO_INCH = 1 / PPI;
560
+
561
+ /**
562
+ * Main export function. Accepts single element or an array.
563
+ * @param {HTMLElement | string | Array<HTMLElement | string>} target - The root element(s) to convert.
564
+ * @param {Object} options - { fileName: string }
565
+ */
566
+ async function exportToPptx(target, options = {}) {
567
+ // Resolve the actual constructor in case `pptxgenjs` was imported/required
568
+ // with different shapes (function, { default: fn }, or { PptxGenJS: fn }).
569
+ const resolvePptxConstructor = (pkg) => {
570
+ if (!pkg) return null;
571
+ if (typeof pkg === 'function') return pkg;
572
+ if (pkg && typeof pkg.default === 'function') return pkg.default;
573
+ if (pkg && typeof pkg.PptxGenJS === 'function') return pkg.PptxGenJS;
574
+ if (pkg && pkg.PptxGenJS && typeof pkg.PptxGenJS.default === 'function') return pkg.PptxGenJS.default;
575
+ return null;
576
+ };
577
+
578
+ const PptxConstructor = resolvePptxConstructor(PptxGenJS);
579
+ if (!PptxConstructor) throw new Error('PptxGenJS constructor not found. Ensure `pptxgenjs` is installed or included as a script.');
580
+ const pptx = new PptxConstructor();
581
+ pptx.layout = 'LAYOUT_16x9';
582
+
583
+ // Standardize input to an array, ensuring single or multiple elements are handled consistently
584
+ const elements = Array.isArray(target) ? target : [target];
585
+
586
+ for (const el of elements) {
587
+ const root = typeof el === 'string' ? document.querySelector(el) : el;
588
+ if (!root) {
589
+ console.warn('Element not found, skipping slide:', el);
590
+ continue;
591
+ }
592
+
593
+ const slide = pptx.addSlide();
594
+ await processSlide(root, slide, pptx);
595
+ }
596
+
597
+ const fileName = options.fileName || 'export.pptx';
598
+ pptx.writeFile({ fileName });
599
+ }
600
+
601
+ /**
602
+ * Worker function to process a single DOM element into a single PPTX slide.
603
+ * @param {HTMLElement} root - The root element for this slide.
604
+ * @param {PptxGenJS.Slide} slide - The PPTX slide object to add content to.
605
+ * @param {PptxGenJS} pptx - The main PPTX instance.
606
+ */
607
+ async function processSlide(root, slide, pptx) {
608
+ const rootRect = root.getBoundingClientRect();
609
+ const PPTX_WIDTH_IN = 10;
610
+ const PPTX_HEIGHT_IN = 5.625;
611
+
612
+ const contentWidthIn = rootRect.width * PX_TO_INCH;
613
+ const contentHeightIn = rootRect.height * PX_TO_INCH;
614
+ const scale = Math.min(PPTX_WIDTH_IN / contentWidthIn, PPTX_HEIGHT_IN / contentHeightIn);
615
+
616
+ const layoutConfig = {
617
+ rootX: rootRect.x,
618
+ rootY: rootRect.y,
619
+ scale: scale,
620
+ offX: (PPTX_WIDTH_IN - contentWidthIn * scale) / 2,
621
+ offY: (PPTX_HEIGHT_IN - contentHeightIn * scale) / 2,
622
+ };
623
+
624
+ const renderQueue = [];
625
+ let domOrderCounter = 0;
626
+
627
+ async function collect(node) {
628
+ const order = domOrderCounter++; // Assign a DOM order for z-index tie-breaking
629
+ const result = await createRenderItem(node, layoutConfig, order, pptx);
630
+ if (result) {
631
+ if (result.items) renderQueue.push(...result.items); // Add any generated render items to the queue
632
+ if (result.stopRecursion) return; // Stop processing children if the item fully consumed the node
633
+ }
634
+ for (const child of node.children) await collect(child);
635
+ }
636
+
637
+ await collect(root);
638
+
639
+ renderQueue.sort((a, b) => {
640
+ if (a.zIndex !== b.zIndex) return a.zIndex - b.zIndex;
641
+ return a.domOrder - b.domOrder;
642
+ });
643
+
644
+ for (const item of renderQueue) {
645
+ if (item.type === 'shape') slide.addShape(item.shapeType, item.options);
646
+ if (item.type === 'image') slide.addImage(item.options);
647
+ if (item.type === 'text') slide.addText(item.textParts, item.options);
648
+ }
649
+ }
650
+
651
+ async function createRenderItem(node, config, domOrder, pptx) {
652
+ if (node.nodeType !== 1) return null;
653
+ const style = window.getComputedStyle(node);
654
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
655
+ return null;
656
+
657
+ const rect = node.getBoundingClientRect();
658
+ if (rect.width === 0 || rect.height === 0) return null;
659
+
660
+ const zIndex = style.zIndex !== 'auto' ? parseInt(style.zIndex) : 0;
661
+ const rotation = getRotation(style.transform);
662
+ const elementOpacity = parseFloat(style.opacity);
663
+
664
+ const widthPx = node.offsetWidth || rect.width;
665
+ const heightPx = node.offsetHeight || rect.height;
666
+ const unrotatedW = widthPx * PX_TO_INCH * config.scale;
667
+ const unrotatedH = heightPx * PX_TO_INCH * config.scale;
668
+ const centerX = rect.left + rect.width / 2;
669
+ const centerY = rect.top + rect.height / 2;
670
+
671
+ let x = config.offX + (centerX - config.rootX) * PX_TO_INCH * config.scale - unrotatedW / 2;
672
+ let y = config.offY + (centerY - config.rootY) * PX_TO_INCH * config.scale - unrotatedH / 2;
673
+ let w = unrotatedW;
674
+ let h = unrotatedH;
675
+
676
+ const items = [];
677
+
678
+ // Image handling for SVG nodes directly
679
+ if (node.nodeName.toUpperCase() === 'SVG') {
680
+ const pngData = await svgToPng(node);
681
+ if (pngData)
682
+ items.push({
683
+ type: 'image',
684
+ zIndex,
685
+ domOrder,
686
+ options: { data: pngData, x, y, w, h, rotate: rotation },
687
+ });
688
+ return { items, stopRecursion: true };
689
+ }
690
+ // Image handling for <img> tags, including rounded corners
691
+ if (node.tagName === 'IMG') {
692
+ let borderRadius = parseFloat(style.borderRadius) || 0;
693
+ if (borderRadius === 0) {
694
+ const parentStyle = window.getComputedStyle(node.parentElement);
695
+ if (parentStyle.overflow !== 'visible')
696
+ borderRadius = parseFloat(parentStyle.borderRadius) || 0;
697
+ }
698
+ const processed = await getProcessedImage(node.src, widthPx, heightPx, borderRadius);
699
+ if (processed)
700
+ items.push({
701
+ type: 'image',
702
+ zIndex,
703
+ domOrder,
704
+ options: { data: processed, x, y, w, h, rotate: rotation },
705
+ });
706
+ return { items, stopRecursion: true };
707
+ }
708
+
709
+ const bgColorObj = parseColor(style.backgroundColor);
710
+ const bgClip = style.webkitBackgroundClip || style.backgroundClip;
711
+ const isBgClipText = bgClip === 'text';
712
+ const hasGradient =
713
+ !isBgClipText && style.backgroundImage && style.backgroundImage.includes('linear-gradient');
714
+
715
+ const borderColorObj = parseColor(style.borderColor);
716
+ const borderWidth = parseFloat(style.borderWidth);
717
+ const hasBorder = borderWidth > 0 && borderColorObj.hex;
718
+
719
+ const borderInfo = getBorderInfo(style, config.scale);
720
+ const hasUniformBorder = borderInfo.type === 'uniform';
721
+ const hasCompositeBorder = borderInfo.type === 'composite';
722
+
723
+ const shadowStr = style.boxShadow;
724
+ const hasShadow = shadowStr && shadowStr !== 'none';
725
+ const borderRadius = parseFloat(style.borderRadius) || 0;
726
+ const softEdge = getSoftEdges(style.filter, config.scale);
727
+
728
+ let isImageWrapper = false;
729
+ const imgChild = Array.from(node.children).find((c) => c.tagName === 'IMG');
730
+ if (imgChild) {
731
+ const childW = imgChild.offsetWidth || imgChild.getBoundingClientRect().width;
732
+ const childH = imgChild.offsetHeight || imgChild.getBoundingClientRect().height;
733
+ if (childW >= widthPx - 2 && childH >= heightPx - 2) isImageWrapper = true;
734
+ }
735
+
736
+ let textPayload = null;
737
+ const isText = isTextContainer(node);
738
+
739
+ if (isText) {
740
+ const textParts = [];
741
+ const isList = style.display === 'list-item';
742
+ if (isList) {
743
+ const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
744
+ const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
745
+ x -= bulletShift;
746
+ w += bulletShift;
747
+ textParts.push({
748
+ text: '• ',
749
+ options: {
750
+ // Default bullet point styling
751
+ color: parseColor(style.color).hex || '000000',
752
+ fontSize: fontSizePt,
753
+ },
754
+ });
755
+ }
756
+
757
+ node.childNodes.forEach((child, index) => {
758
+ // Process text content, sanitizing whitespace and applying text transformations
759
+ let textVal = child.nodeType === 3 ? child.nodeValue : child.textContent;
760
+ let nodeStyle = child.nodeType === 1 ? window.getComputedStyle(child) : style;
761
+ textVal = textVal.replace(/[\n\r\t]+/g, ' ').replace(/\s{2,}/g, ' ');
762
+ if (index === 0 && !isList) textVal = textVal.trimStart();
763
+ else if (index === 0) textVal = textVal.trimStart();
764
+ if (index === node.childNodes.length - 1) textVal = textVal.trimEnd();
765
+ if (nodeStyle.textTransform === 'uppercase') textVal = textVal.toUpperCase();
766
+ if (nodeStyle.textTransform === 'lowercase') textVal = textVal.toLowerCase();
767
+
768
+ if (textVal.length > 0) {
769
+ textParts.push({
770
+ text: textVal,
771
+ options: getTextStyle(nodeStyle, config.scale),
772
+ });
773
+ }
774
+ });
775
+
776
+ if (textParts.length > 0) {
777
+ let align = style.textAlign || 'left';
778
+ if (align === 'start') align = 'left';
779
+ if (align === 'end') align = 'right';
780
+ let valign = 'top';
781
+ if (style.alignItems === 'center') valign = 'middle';
782
+ if (style.justifyContent === 'center' && style.display.includes('flex')) align = 'center';
783
+
784
+ const pt = parseFloat(style.paddingTop) || 0;
785
+ const pb = parseFloat(style.paddingBottom) || 0;
786
+ if (Math.abs(pt - pb) < 2 && bgColorObj.hex) valign = 'middle';
787
+
788
+ let padding = getPadding(style, config.scale);
789
+ if (align === 'center' && valign === 'middle') padding = [0, 0, 0, 0];
790
+
791
+ textPayload = { text: textParts, align, valign, inset: padding };
792
+ }
793
+ }
794
+
795
+ if (hasGradient || (softEdge && bgColorObj.hex && !isImageWrapper)) {
796
+ let bgData = null;
797
+ let padIn = 0;
798
+ if (softEdge) {
799
+ const svgInfo = generateBlurredSVG(widthPx, heightPx, bgColorObj.hex, borderRadius, softEdge);
800
+ bgData = svgInfo.data;
801
+ padIn = svgInfo.padding * PX_TO_INCH * config.scale;
802
+ } else {
803
+ bgData = generateGradientSVG(
804
+ widthPx,
805
+ heightPx,
806
+ style.backgroundImage,
807
+ borderRadius,
808
+ hasBorder ? { color: borderColorObj.hex, width: borderWidth } : null
809
+ );
810
+ }
811
+
812
+ if (bgData) {
813
+ items.push({
814
+ type: 'image',
815
+ zIndex,
816
+ domOrder,
817
+ options: {
818
+ data: bgData,
819
+ x: x - padIn,
820
+ y: y - padIn,
821
+ w: w + padIn * 2,
822
+ h: h + padIn * 2,
823
+ rotate: rotation,
824
+ },
825
+ });
826
+ }
827
+
828
+ if (textPayload) {
829
+ items.push({
830
+ type: 'text',
831
+ zIndex: zIndex + 1,
832
+ domOrder,
833
+ textParts: textPayload.text,
834
+ options: {
835
+ x,
836
+ y,
837
+ w,
838
+ h,
839
+ align: textPayload.align,
840
+ valign: textPayload.valign,
841
+ inset: textPayload.inset,
842
+ rotate: rotation,
843
+ margin: 0,
844
+ wrap: true,
845
+ autoFit: false,
846
+ },
847
+ });
848
+ }
849
+ if (hasCompositeBorder) {
850
+ // Add border shapes after the main background
851
+ const borderItems = createCompositeBorderItems(
852
+ borderInfo.sides,
853
+ x,
854
+ y,
855
+ w,
856
+ h,
857
+ config.scale,
858
+ zIndex,
859
+ domOrder
860
+ );
861
+ items.push(...borderItems);
862
+ }
863
+ } else if (
864
+ (bgColorObj.hex && !isImageWrapper) ||
865
+ hasUniformBorder ||
866
+ hasCompositeBorder ||
867
+ hasShadow ||
868
+ textPayload
869
+ ) {
870
+ const finalAlpha = elementOpacity * bgColorObj.opacity;
871
+ const transparency = (1 - finalAlpha) * 100;
872
+
873
+ const shapeOpts = {
874
+ x,
875
+ y,
876
+ w,
877
+ h,
878
+ rotate: rotation,
879
+ fill:
880
+ bgColorObj.hex && !isImageWrapper
881
+ ? { color: bgColorObj.hex, transparency: transparency }
882
+ : { type: 'none' },
883
+ // Only apply line if the border is uniform
884
+ line: hasUniformBorder ? borderInfo.options : null,
885
+ };
886
+
887
+ if (hasShadow) {
888
+ shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
889
+ }
890
+
891
+ const borderRadius = parseFloat(style.borderRadius) || 0;
892
+ const widthPx = node.offsetWidth || rect.width;
893
+ const heightPx = node.offsetHeight || rect.height;
894
+ const isCircle =
895
+ borderRadius >= Math.min(widthPx, heightPx) / 2 - 1 && Math.abs(widthPx - heightPx) < 2;
896
+
897
+ let shapeType = pptx.ShapeType.rect;
898
+ if (isCircle) shapeType = pptx.ShapeType.ellipse;
899
+ else if (borderRadius > 0) {
900
+ shapeType = pptx.ShapeType.roundRect;
901
+ shapeOpts.rectRadius = Math.min(1, borderRadius / (Math.min(widthPx, heightPx) / 2));
902
+ }
903
+
904
+ // MERGE TEXT INTO SHAPE (if text exists)
905
+ if (textPayload) {
906
+ const textOptions = {
907
+ shape: shapeType,
908
+ ...shapeOpts,
909
+ align: textPayload.align,
910
+ valign: textPayload.valign,
911
+ inset: textPayload.inset,
912
+ margin: 0,
913
+ wrap: true,
914
+ autoFit: false,
915
+ };
916
+ items.push({
917
+ type: 'text',
918
+ zIndex,
919
+ domOrder,
920
+ textParts: textPayload.text,
921
+ options: textOptions,
922
+ });
923
+ // If no text, just draw the shape
924
+ } else {
925
+ items.push({
926
+ type: 'shape',
927
+ zIndex,
928
+ domOrder,
929
+ shapeType,
930
+ options: shapeOpts,
931
+ });
932
+ }
933
+
934
+ // ADD COMPOSITE BORDERS (if they exist)
935
+ if (hasCompositeBorder) {
936
+ // Generate a single SVG image that contains all the rounded border sides
937
+ const borderSvgData = generateCompositeBorderSVG(
938
+ widthPx,
939
+ heightPx,
940
+ borderRadius,
941
+ borderInfo.sides
942
+ );
943
+
944
+ if (borderSvgData) {
945
+ items.push({
946
+ type: 'image',
947
+ zIndex: zIndex + 1,
948
+ domOrder,
949
+ options: {
950
+ data: borderSvgData,
951
+ x: x,
952
+ y: y,
953
+ w: w,
954
+ h: h,
955
+ rotate: rotation,
956
+ },
957
+ });
958
+ }
959
+ }
960
+ }
961
+
962
+ return { items, stopRecursion: !!textPayload };
963
+ }
964
+
965
+ /**
966
+ * Helper function to create individual border shapes
967
+ */
968
+ function createCompositeBorderItems(sides, x, y, w, h, scale, zIndex, domOrder) {
969
+ const items = [];
970
+ const pxToInch = 1 / 96;
971
+
972
+ // TOP BORDER
973
+ if (sides.top.width > 0) {
974
+ items.push({
975
+ type: 'shape',
976
+ zIndex: zIndex + 1,
977
+ domOrder,
978
+ shapeType: 'rect',
979
+ options: {
980
+ x: x,
981
+ y: y,
982
+ w: w,
983
+ h: sides.top.width * pxToInch * scale,
984
+ fill: { color: sides.top.color },
985
+ },
986
+ });
987
+ }
988
+ // RIGHT BORDER
989
+ if (sides.right.width > 0) {
990
+ items.push({
991
+ type: 'shape',
992
+ zIndex: zIndex + 1,
993
+ domOrder,
994
+ shapeType: 'rect',
995
+ options: {
996
+ x: x + w - sides.right.width * pxToInch * scale,
997
+ y: y,
998
+ w: sides.right.width * pxToInch * scale,
999
+ h: h,
1000
+ fill: { color: sides.right.color },
1001
+ },
1002
+ });
1003
+ }
1004
+ // BOTTOM BORDER
1005
+ if (sides.bottom.width > 0) {
1006
+ items.push({
1007
+ type: 'shape',
1008
+ zIndex: zIndex + 1,
1009
+ domOrder,
1010
+ shapeType: 'rect',
1011
+ options: {
1012
+ x: x,
1013
+ y: y + h - sides.bottom.width * pxToInch * scale,
1014
+ w: w,
1015
+ h: sides.bottom.width * pxToInch * scale,
1016
+ fill: { color: sides.bottom.color },
1017
+ },
1018
+ });
1019
+ }
1020
+ // LEFT BORDER
1021
+ if (sides.left.width > 0) {
1022
+ items.push({
1023
+ type: 'shape',
1024
+ zIndex: zIndex + 1,
1025
+ domOrder,
1026
+ shapeType: 'rect',
1027
+ options: {
1028
+ x: x,
1029
+ y: y,
1030
+ w: sides.left.width * pxToInch * scale,
1031
+ h: h,
1032
+ fill: { color: sides.left.color },
1033
+ },
1034
+ });
1035
+ }
1036
+
1037
+ return items;
1003
1038
  }
1004
1039
 
1005
1040
  exports.exportToPptx = exportToPptx;