When inline SVGs are styled using CSS the properties are not rendered.
My example above shows how an SVG that includes a defined fill property as:
<path fill="red" ...
But is then overridden in the browser using the CSS:
path {
fill: green;
}
will render in the browser as a green path but once converted to a canvas using html2canvas the property is lost and is instead rendered as red.
It looks like this can be solved by explicitly applying svg styles to the SVG and each of its nodes using getComputedStyle
. See this updated demo.
For my use case, all of my styles are defined in my stylesheets including the SVG dimensions. So iterating over each SVG in my document and applying:
svg.setAttribute('height', svg.clientHeight);
svg.setAttribute('width', svg.clientWidth);
and then over each node in SVG I can apply:
node.setAttribute('fill', style.getPropertyValue('fill'));
// stroke, stroke-width, and any other SVG properties will also be needed
I then remove all of the changes I made after the canvas is rendered so that media queries and hover states will be rendered as expected again.
It would be great if a solution, this one or a better one if it exists, was incorporated into html2canvas.
@G0dwin, I'm interested in trying this workaround, but in the link you provided to the updated demo, I don't see the problem being resolved:
The demo seems to link directly to the master branch of this repo, so maybe changes in html2canvas since your last post have affected the demo?
I got this working using computed-style-to-inline-style
and one workaround https://github.com/lukehorvat/computed-style-to-inline-style/issues/2
computedToInline(element, true);
for (const attribute of ["width", "height"]) {
for (const rect of element.querySelectorAll("svg ["+attribute+"]")) {
rect.style[attribute] = "";
}
}
html2canvas(element).then(function(canvas) {
// ...
});
Was able to get this working with the same method as @tlrobinson but running it inside the onclone
function of html2canvas so that it only affects the items that are going to be sent to the image.
const { containerId } = this.props;
const element = document.querySelector(`#${containerId}`);
const options = {
ignoreElements: (element) => hasClass(element, 'mapboxgl-ctrl'),
onclone: (clonedDoc) => {
// After we clone the document, inline svg fill
const container = clonedDoc.body.querySelector(`#${containerId}`);
computedStyleToInlineStyle(container, {
recursive: true,
properties: ["fill"],
});
},
}
// For testing
html2canvas(element, options).then(canvas => {
document.body.appendChild(canvas);
});
Thanks for the tip!
Here's a version of the fix that does not rely on external libraries:
function applyStyleToSvgTrees(container) {
for (let svg of container.getElementsByTagName('svg')) {
for (let el of svg.getElementsByTagName('*')) {
s = getComputedStyle(el);
for (let key of s) {
let prop = key.replace(/\-([a-z])/g, v => v[1].toUpperCase());
el.style[prop] = s[key];
}
}
}
}
html2canvas(document.body, {
onclone: (clonedDoc) => {
applyStyleToSvgTrees(clonedDoc);
},
}).then(canvas => {
document.body.appendChild(canvas);
});
@Jerther you saved my day, thanks a lot!
I confirm this works, here's a Typescript version if anyone needs it:
function applyStyleToSvg(container: Element | Document) {
for (let svg of Array.from(container.getElementsByTagName('svg'))) {
for (let element of Array.from(svg.getElementsByTagName('*')) as Array<SVGElement>) {
const computedStyle = getComputedStyle(element);
for (let property of Array.from(computedStyle)) {
element.style.setProperty(property, computedStyle.getPropertyValue(property));
}
}
}
}
Most helpful comment
I got this working using
computed-style-to-inline-style
and one workaround https://github.com/lukehorvat/computed-style-to-inline-style/issues/2