-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom font on SVG element is not correct #1463
Comments
Experiencing the same issue. Is there any temporary fix to this? Using a dropdown to select many Google Fonts, but even though they're displayed correctly on the page, the rendering is wrong (unless i have it installed on my pc). |
Temporary fix for me was #1709 (comment) by @maximgeerinck |
any updates on this? solution from @maximgeerinck didn't work for me |
Im also waiting for a fix on this one. Any update? |
The same happens to me, any updates on this? |
I got it to work by using this module
|
Got it to work by manually adding a style tag inside the SVG element. The style tag will contain the CSS for the font you want to include. chart: {
type: 'pie',
events: {
redraw: function(event) {
let style = document.createElement("style");
style.appendChild(document.createTextNode(abelCss));
const element = document.getElementById(event.target.container.id).firstChild
element.insertBefore(style, element.firstChild)
}
}
} You must do this process anytime before running the html2canvas function. The library mentioned by @lifeinchords basically does this process for you. |
I agree... I took the fiddle in the original post (https://jsfiddle.net/fgppmvxo/8/) and modified it to call computedStyleToInlineStyle before html2canvas, and the results were identical. It would be really nice if this worked. |
Does not work for me as well. |
The provided solutions do not work here: has anyone got a solution that DOES work? Example of it not working with suggested fix: |
Temporary fix for me was #1709 (comment) by @maximgeerinck as well. Thanks! |
I'm using |
Please reopen this @niklasvh |
[Quick Fix working with the joint.js Library] : You could inject a custom attribute "font-family" in each svg node : fontFamilyInjection : private fontFamilyInjection (targetElem) {
var svgElem = targetElem.getElementsByTagName("svg");
for (const node of svgElem) {
node.setAttribute("font-family", window.getComputedStyle(node, null).getPropertyValue("font-family"));
node.replaceWith(node);
}
} html2canvas : var domElement: any = document.getElementById('paper');
// Re-inject font-family
this.fontFamilyInjection(domElement);
html2canvas(domElement, {
scale: 3,
allowTaint: true,
useCORS: true
}).then(canvas => {
// Do something...
}); |
@mkubdev can you edit the jsfiddle to demonstrate your example works? It would be a great example. |
@garaboncias sure... i'll try. Maybe this is related to Google Font CDN?
Second, you should try to download directly the 'Kanit' font from GoogleFonts and import it to your app => html2canvas works as expected. |
I converted the Kanit font to |
@goliney Damned. You are right. I don't understand why i can't get it to work on jsfiddle but it's working on my apps. I can make it work on jsfiddle with Jquery's way... This should be convertible to TypeScript : https://jsfiddle.net/9Lvwcnu8/ |
@mkubdev WOW! This is awesome. Thank you for your help 🍺 |
In case anyone needs it, here is the @mkubdev's working example without jQuery: It's worth mentioning, that with |
some one can help me with reactJS please? |
Just wanted to add another comment. We just used @mkubdev's concept in our application's code. However, one thing we did differently was to leverage This is the code that converts from the stylesheets' font-face rules: import axios, { AxiosResponse } from 'axios';
type FontConversion = {
base64: string;
url: string;
}
const convertFontsToBase64 = async () => {
const fontFaceRules = getFontFaceRules();
const urls = getFontFaceUrls(fontFaceRules);
let fontFaceCss = fontFaceRules;
if (urls && urls.length > 0) {
const conversions = await convertFontsToDataUrls(urls);
fontFaceCss = replaceUrls(fontFaceCss, conversions);
}
return fontFaceCss;
};
// find every font face rule and join them into 1 big string
const getFontFaceRules = () => (
Array.from(document.styleSheets)
.flatMap((sheet) => Array.from(sheet.cssRules))
.filter((rule) => rule instanceof CSSFontFaceRule)
.map((rule) => rule.cssText)
.join('\n')
);
const getFontFaceUrls = (fontFaceRules: string) => (
// one difference here is that our regex looks for
// quotes around the URL
fontFaceRules.match(/"?https?:\/\/[^ )]+/g);
};
const convertFontsToDataUrls = async (
urls: string[],
) => {
// this lookup will be used to track what URL is being replaced
// since the originals could be in the form `"https://site.com/font.woff"`
// or just `https://site.com/font.woff`
const urlLookup: Record<string, string> = {};
// here we do each font request (through Axios) + track the URL
// for later use
const fontFetches = urls.map((url) => {
const strippedUrl = url.replace(/(^"|"$)/g, '');
urlLookup[strippedUrl] = url;
return axios.get<Blob>(strippedUrl, { responseType: 'blob' });
});
// settlePromises is an internal utility we have to wrap Promise.allSettled
// it returns a list of errors & a list of successes
const [errors, success] = await settlePromises(fontFetches);
if (errors.length) {
throw new Error('Could not generate fonts in dashboard charts');
}
const conversions = (success as AxiosResponse<Blob>[]).map(async ({ config, data }) => {
const base64 = await convertFont(data);
const { url } = config;
const originalUrl = urlLookup[url!] || url!;
return {
base64,
url: originalUrl,
};
});
return Promise.all(conversions);
};
// convert font sets up the `FileReader`, but wraps it in a `Promise`
// so we don't have to track completed conversions
const convertFont = async (data: Blob) => (
new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
resolve(reader.result as string);
};
reader.onerror = () => {
reject(Error('Could not convert font to base64'));
};
reader.readAsDataURL(data);
})
);
// this is extracted just to make the code less
// cluttered
const replaceUrls = (
initialCss: string,
conversions: FontConversion[],
) => (
conversions.reduce((css, { base64, url }) => (
css.replace(url, base64)
), initialCss)
); We then call html2canvas(node, {
onclone: async (doc) => {
await applySvgStyles(doc);
},
});
// ...later in the file...
const applySvgStyles = async (doc: Document) => {
const fontFaceCss = await convertFontsToBase64();
const svgs = clonedDocument.querySelectorAll('svg');
if (svgs.length) {
svgs.forEach((svgElement) => {
const fontFaceTag = document.createElement('style');
fontFaceTag.innerHTML = fontFaceCss;
svgElement.prepend(fontFaceTag);
});
}
}; |
I am using convertFontsToBase64.js // Heavily inspired by: https://github.com/niklasvh/html2canvas/issues/1463#issuecomment-1112428054
const convertFontsToDataUrls = async (urls) => {
const fontFetches = urls.map((url) => {
return fetch(url).then(async (response) => ({
url,
base64: await new Promise(async (resolve) => {
const reader = new FileReader()
reader.onloadend = () => {
resolve(reader.result)
}
reader.readAsDataURL(await response.blob())
}),
}))
})
return Promise.allSettled(fontFetches).then((responses) =>
responses.filter((response) => response.status === 'fulfilled').map((response) => response.value)
)
}
export default async ({ fonts }) => {
const fontFaceRules = Array.from(document.styleSheets)
.flatMap((sheet) => Array.from(sheet.cssRules))
.filter((rule) => rule instanceof CSSFontFaceRule)
.map((rule) => rule.cssText)
.join('\n')
const fontsFaces = fontFaceRules
.match(/@font-face {[^}]*}/g)
.map((fontFace) => {
const urls = fontFace.match(/(?<=url\(")(.*?)(?="\))/g)
return fontFace.match(/(?<=font-family: "?)([^"]*?)(?="?;)/g).map((fontFamily) => ({
family: fontFamily.toLowerCase(),
urls,
}))
})
.flat()
let fontFaceCss = fontFaceRules
if (fontsFaces.length) {
const urlsToConvert = fontsFaces.filter((font) => fonts.includes(font.family)).flatMap((font) => font.urls)
const conversions = await convertFontsToDataUrls(urlsToConvert)
fontFaceCss = conversions.reduce((css, { base64, url }) => css.replace(url, base64), fontFaceCss)
}
return fontFaceCss
} html2canvas onclone option onclone = (clonedDocument) => {
if (processFonts) {
const styles = document.querySelectorAll('style[data-id="keep-font-face"]')
styles.forEach((style) => {
const xpath = getXPathForElement(style.parentElement).replace(/\[\d+\]/g, '[1]')
const clonedElement = getElementByXPath(xpath, clonedDocument)
if (clonedElement) {
clonedElement.appendChild(style.cloneNode(true))
}
})
}
} In a React Hook document.querySelectorAll('[data-id="keep-font-face"]').forEach((style) => {
style.remove()
})
if (fonts.current?.length) {
const elements = [...(fontElements.current || []), ...(document.querySelectorAll('svg:not([role="img"]') || [])]
const fontFaceCss = await convertFontsToBase64({ fonts: fonts.current.map((font) => font.toLowerCase()) })
elements.forEach((element) => {
const style = document.createElement('style')
style.setAttribute('data-id', 'keep-font-face')
style.innerHTML = fontFaceCss
element.prepend(style)
})
} usage:
|
Ran into this issue. I used @gonzofish's code, prepended the SVG with a
Now it's working in Safari too. |
Managed to fix this issue using the embed-fonts-as-base64-in-style-tag solution as well. Based on code from previous commenters, here is what I ended up using: /** Uses FileReader to read a blob */
const blobToData = (blob: Blob): Promise<string> => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result + '');
reader.readAsDataURL(blob);
})
}
/** Loads an external font and returns it as an embeddable font-face with base64 source */
const getFontFace = async (familyName: string, url: string) => {
try {
const response = await fetch(url);
const blob = await response.blob();
const fontData = await blobToData(blob);
return `@font-face {
font-family: "${familyName}";
src: url("${fontData}");
}`;
} catch (err) {
console.error('Error getting font face', err);
return '';
}
}
/** Appends a <style> element with provided css to target */
const appendStyle = (css: string, target: Element) => {
const styleEl = document.createElement('style');
styleEl.appendChild(document.createTextNode(css));
target.appendChild(styleEl);
} Then, before invoking const fontFace = await getFontFace('Your font family name', YourExternalFontUrl);
appendStyle(fontFace, yourSvgElement); |
I changed the svg tag after the chart definition and embedded the font like so: `
` |
Hello, I have svg element and use google font.
But when capture this element to image the font is not correctly.
I'm using version 1.0.0-alpha.10.
Thank you for your advice.
Here is my code: https://jsfiddle.net/fgppmvxo/8/
The text was updated successfully, but these errors were encountered: