@@ -62,6 +62,7 @@ function applyAllComputedStylesDeep(clone, original, inheritedFontFamily) {
62
62
63
63
let styleMap = { } ;
64
64
65
+ // Get all already-inline styles
65
66
let inlineStyle = clone . getAttribute ( 'style' ) ;
66
67
if ( typeof inlineStyle !== 'string' ) inlineStyle = '' ;
67
68
inlineStyle . split ( ';' ) . forEach ( s => {
@@ -71,12 +72,22 @@ function applyAllComputedStylesDeep(clone, original, inheritedFontFamily) {
71
72
}
72
73
} ) ;
73
74
75
+ // Copy *every* computed property
74
76
for ( let i = 0 ; i < computedStyle . length ; i += 1 ) {
75
77
const property = computedStyle [ i ] ;
76
78
const value = computedStyle . getPropertyValue ( property ) ;
77
79
styleMap [ property ] = value ;
78
80
}
79
81
82
+ // ---- Strongest part: force resolved color and background-color ----
83
+ // getPropertyValue always resolves to an actual color, not 'inherit'
84
+ styleMap [ 'color' ] = computedStyle . color ;
85
+ styleMap [ 'background-color' ] = computedStyle . backgroundColor ;
86
+ // Add font props for safety (if they matter to your charts)
87
+ styleMap [ 'font-family' ] = computedStyle . fontFamily || inheritedFontFamily || '' ;
88
+ styleMap [ 'font-size' ] = computedStyle . fontSize ;
89
+ styleMap [ 'font-weight' ] = computedStyle . fontWeight ;
90
+
80
91
if (
81
92
isFlex ||
82
93
computedStyle . display . includes ( 'grid' ) ||
@@ -94,9 +105,7 @@ function applyAllComputedStylesDeep(clone, original, inheritedFontFamily) {
94
105
styleMap [ prop ] = computedStyle . getPropertyValue ( prop ) ;
95
106
} ) ;
96
107
97
- if ( inheritedFontFamily ) {
98
- styleMap [ 'font-family' ] = inheritedFontFamily ;
99
- }
108
+ // No more inheritedFontFamily here; use resolved computedStyle above
100
109
101
110
styleMap [ 'overflow' ] = 'visible' ;
102
111
styleMap [ 'overflow-x' ] = 'visible' ;
@@ -108,6 +117,7 @@ function applyAllComputedStylesDeep(clone, original, inheritedFontFamily) {
108
117
}
109
118
clone . setAttribute ( 'style' , styleString ) ;
110
119
120
+ // Recursively walk
111
121
const cloneChildren = clone . children || [ ] ;
112
122
const originalChildren = original . children || [ ] ;
113
123
for ( let i = 0 ; i < cloneChildren . length ; i ++ ) {
@@ -117,6 +127,7 @@ function applyAllComputedStylesDeep(clone, original, inheritedFontFamily) {
117
127
}
118
128
}
119
129
130
+
120
131
/**
121
132
* Ensures all <text> elements in the given SVG element tree have the given font family
122
133
* as both an attribute and a style property.
@@ -204,9 +215,11 @@ function extractFontFaceRules() {
204
215
try {
205
216
const rules = sheet . cssRules ;
206
217
if ( ! rules ) continue ;
207
-
208
218
for ( const rule of rules ) {
209
- if ( rule . constructor . name === "CSSFontFaceRule" || rule . cssText . startsWith ( "@font-face" ) ) {
219
+ if (
220
+ ( typeof CSSFontFaceRule !== "undefined" && rule instanceof CSSFontFaceRule ) ||
221
+ rule . cssText . trim ( ) . startsWith ( "@font-face" )
222
+ ) {
210
223
fontCssRules . push ( rule . cssText ) ;
211
224
}
212
225
}
@@ -218,21 +231,155 @@ function extractFontFaceRules() {
218
231
return fontCssRules ;
219
232
}
220
233
234
+
235
+ /**
236
+ * Injects all @font-face CSS rules into an SVG element as an embedded <style> tag within <defs>.
237
+ * This ensures custom fonts are preserved when the SVG is exported or rasterized.
238
+ *
239
+ * @param {SVGSVGElement } svgEl - The SVG element to inject font-face styles into.
240
+ */
221
241
function injectFontFaceStyles ( svgEl ) {
222
242
const fontRules = extractFontFaceRules ( ) ;
223
243
if ( ! fontRules . length ) return ;
224
-
225
244
const style = document . createElement ( 'style' ) ;
226
245
style . setAttribute ( 'type' , 'text/css' ) ;
227
246
style . textContent = fontRules . join ( '\n' ) ;
228
-
229
247
const defs = svgEl . querySelector ( 'defs' ) || document . createElementNS ( XMLNS , 'defs' ) ;
230
248
defs . appendChild ( style ) ;
231
249
if ( ! svgEl . querySelector ( 'defs' ) ) {
232
250
svgEl . insertBefore ( defs , svgEl . firstChild ) ;
233
251
}
234
252
}
235
253
254
+ /**
255
+ * Recursively inlines all computed styles from the live DOM into the corresponding elements
256
+ * within each <foreignObject> in a cloned SVG tree.
257
+ * This is used to preserve appearance for HTML content inside SVG exports.
258
+ *
259
+ * @param {SVGSVGElement } cloneSvg - The cloned SVG element containing <foreignObject> nodes to style.
260
+ * @param {SVGSVGElement } liveSvg - The original live SVG element to read computed styles from.
261
+ */
262
+ function inlineForeignObjectStylesInClone ( cloneSvg , liveSvg ) {
263
+ const cloneFOs = cloneSvg . querySelectorAll ( 'foreignObject' ) ;
264
+ const liveFOs = liveSvg . querySelectorAll ( 'foreignObject' ) ;
265
+
266
+ cloneFOs . forEach ( ( cloneFO , foIdx ) => {
267
+ const liveFO = liveFOs [ foIdx ] ;
268
+ if ( ! liveFO ) return ;
269
+
270
+ function applyStylesRecursively ( cloneNode , liveNode ) {
271
+ if ( ! cloneNode || ! liveNode ) return ;
272
+ if ( cloneNode . nodeType === 1 && liveNode . nodeType === 1 ) {
273
+ const computedStyle = window . getComputedStyle ( liveNode ) ;
274
+ let styleString = '' ;
275
+ for ( let j = 0 ; j < computedStyle . length ; j ++ ) {
276
+ const property = computedStyle [ j ] ;
277
+ styleString += `${ property } :${ computedStyle . getPropertyValue ( property ) } ;` ;
278
+ }
279
+ cloneNode . setAttribute ( 'style' , styleString ) ;
280
+ }
281
+ const cloneChildren = cloneNode . children || [ ] ;
282
+ const liveChildren = liveNode . children || [ ] ;
283
+ for ( let i = 0 ; i < cloneChildren . length ; i ++ ) {
284
+ applyStylesRecursively ( cloneChildren [ i ] , liveChildren [ i ] ) ;
285
+ }
286
+ }
287
+
288
+ applyStylesRecursively ( cloneFO , liveFO ) ;
289
+ } ) ;
290
+ }
291
+
292
+ /**
293
+ * Injects critical CSS style rules used by HTML nodes inside each <foreignObject> of the cloned SVG.
294
+ * This collects all matching CSS rules from same-origin stylesheets and inserts them
295
+ * as <style> tags inside the relevant <foreignObject> nodes.
296
+ *
297
+ * @param {SVGSVGElement } cloneSvg - The cloned SVG element containing <foreignObject> nodes.
298
+ */
299
+ function injectCriticalStylesIntoForeignObjects ( cloneSvg ) {
300
+ const foreignObjects = cloneSvg . querySelectorAll ( 'foreignObject' ) ;
301
+ foreignObjects . forEach ( fo => {
302
+ let css = '' ;
303
+ const elements = Array . from ( fo . querySelectorAll ( '*' ) ) ;
304
+ if ( elements . length === 0 ) return ;
305
+
306
+ for ( const sheet of document . styleSheets ) {
307
+ let rules ;
308
+ try { rules = sheet . cssRules ; } catch { continue ; }
309
+ if ( ! rules ) continue ;
310
+ for ( const rule of rules ) {
311
+ if ( typeof CSSStyleRule !== "undefined" && ! ( rule instanceof CSSStyleRule ) ) continue ;
312
+ try {
313
+ // Only include if any element matches this selector
314
+ if ( elements . some ( el => el . matches ( rule . selectorText ) ) ) {
315
+ css += rule . cssText + "\n" ;
316
+ }
317
+ } catch { continue ; }
318
+ }
319
+ }
320
+ if ( css ) {
321
+ const styleTag = document . createElement ( 'style' ) ;
322
+ styleTag . textContent = css ;
323
+ fo . insertBefore ( styleTag , fo . firstChild ) ;
324
+ }
325
+ } ) ;
326
+ }
327
+
328
+ /**
329
+ * Ensures the correct HTML namespace is set on the root element of each <foreignObject> within an SVG.
330
+ * Adds xmlns="http://www.w3.org/1999/xhtml" to the root HTML node if not present.
331
+ *
332
+ * @param {SVGSVGElement } cloneSvg - The cloned SVG element containing <foreignObject> nodes.
333
+ */
334
+ function ensureForeignObjectRootNamespace ( cloneSvg ) {
335
+ const foreignObjects = cloneSvg . querySelectorAll ( 'foreignObject' ) ;
336
+ foreignObjects . forEach ( fo => {
337
+ const root = fo . firstElementChild ;
338
+ if ( root && root . tagName . toLowerCase ( ) !== 'svg' ) {
339
+ root . setAttribute ( 'xmlns' , 'http://www.w3.org/1999/xhtml' ) ;
340
+ }
341
+ } ) ;
342
+ }
343
+
344
+ /**
345
+ * Recursively applies all computed styles from the live DOM into the corresponding HTML elements
346
+ * inside <foreignObject> nodes in the cloned SVG, ensuring visual fidelity.
347
+ *
348
+ * @param {SVGSVGElement } cloneSvg - The cloned SVG element containing <foreignObject> nodes.
349
+ * @param {SVGSVGElement } liveSvg - The original SVG element to read computed styles from.
350
+ */
351
+ function inlineForeignObjectHTMLComputedStyles ( cloneSvg , liveSvg ) {
352
+ const cloneFOs = cloneSvg . querySelectorAll ( 'foreignObject' ) ;
353
+ const liveFOs = liveSvg . querySelectorAll ( 'foreignObject' ) ;
354
+ cloneFOs . forEach ( ( cloneFO , idx ) => {
355
+ const liveFO = liveFOs [ idx ] ;
356
+ if ( ! cloneFO || ! liveFO ) return ;
357
+ const cloneRoot = cloneFO . firstElementChild ;
358
+ const liveRoot = liveFO . firstElementChild ;
359
+ if ( cloneRoot && liveRoot ) {
360
+ walkAllAndApply ( cloneRoot , liveRoot ) ;
361
+ }
362
+ } ) ;
363
+ }
364
+
365
+ /**
366
+ * Recursively walks two parallel DOM trees, applying all computed styles from the live node
367
+ * to the cloned node and all of their descendants.
368
+ *
369
+ * @param {Element } cloneNode - The cloned DOM node to apply styles to.
370
+ * @param {Element } liveNode - The live DOM node to read computed styles from.
371
+ */
372
+ function walkAllAndApply ( cloneNode , liveNode ) {
373
+ if ( cloneNode . nodeType !== 1 || ! liveNode ) return ;
374
+ applyAllComputedStylesDeep ( cloneNode , liveNode ) ;
375
+ const cloneChildren = cloneNode . children || [ ] ;
376
+ const liveChildren = liveNode . children || [ ] ;
377
+ for ( let i = 0 ; i < cloneChildren . length ; i ++ ) {
378
+ walkAllAndApply ( cloneChildren [ i ] , liveChildren [ i ] ) ;
379
+ }
380
+ }
381
+
382
+
236
383
/**
237
384
* Converts a DOM element (including HTML, SVG, and canvas) into a high-resolution PNG data URL.
238
385
*
@@ -289,14 +436,16 @@ async function domToPng({ container, scale = 2 }) {
289
436
const liveSvg = container . querySelector ( 'svg[aria-label]' ) ;
290
437
const cloneSvg = clone . querySelector ( 'svg[aria-label]' ) ;
291
438
if ( liveSvg && cloneSvg ) {
292
- const bbox = liveSvg . getBoundingClientRect ( ) ;
293
- const svgWidth = bbox . width ;
294
- const svgHeight = bbox . height ;
295
-
296
- // Only modify the clone
439
+ ensureForeignObjectRootNamespace ( cloneSvg ) ;
440
+ injectCriticalStylesIntoForeignObjects ( cloneSvg ) ;
441
+ inlineForeignObjectStylesInClone ( cloneSvg , liveSvg ) ;
442
+ inlineForeignObjectHTMLComputedStyles ( cloneSvg , liveSvg ) ;
297
443
applyAllSvgComputedStylesInline ( cloneSvg ) ;
298
444
setFontFamilyOnAllSvgTextElements ( cloneSvg , containerFontFamily ) ;
299
445
446
+ const bbox = liveSvg . getBoundingClientRect ( ) ;
447
+ const svgWidth = bbox . width ;
448
+ const svgHeight = bbox . height ;
300
449
const scaledWidth = Math . round ( svgWidth * scale ) ;
301
450
const scaledHeight = Math . round ( svgHeight * scale ) ;
302
451
const pngDataUrl = await svgElementToPngDataUrl ( cloneSvg , scaledWidth , scaledHeight ) ;
0 commit comments