exupero / saveSvgAsPng

Save SVGs as PNGs from the browser.
MIT License
1.09k stars 357 forks source link

Fonts not loading properly #172

Open crestAT opened 6 years ago

crestAT commented 6 years ago

Thanks for this great plugin! I use it in conjuction with pivotable to export/save charts and have a problem with the fonts of the saved svg. In the console I get errors like this: console log no idea why it tries to get the font files from a "wrong" path ...

which leeds to croping of text like this - left hand side (other fonts and size): export

The question would be either to solve the errors shown in the log or use custom fonts - pls could you provide an examle on how to define it in the call - I'm sorry but I must confess I have no idea how to do this ...

Thanks in advance!

exupero commented 6 years ago

What are the correct font URLs?

crestAT commented 6 years ago

The correct URLs are eg: http://10.0.0.88/DEVELOPMENT/ajax/fonts/fontawesome-webfont.eot

Thanks for the quick response!

exupero commented 6 years ago

Can you share your CSS @font-face definition and what URL the CSS file is served as?

The problem is likely in the URL detection regex:

https://github.com/exupero/saveSvgAsPng/blob/4366fbc2fb0bd70ca6f68e5c1c0f4666e91e0f75/saveSvgAsPng.js#L7

or in how the full URL is constructed:

https://github.com/exupero/saveSvgAsPng/blob/4366fbc2fb0bd70ca6f68e5c1c0f4666e91e0f75/saveSvgAsPng.js#L98-L104

crestAT commented 6 years ago

There are two @font-face definitions loaded by:

bootstrap.min.css: @font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}

and the second in: font-awesome.min.css: @font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}

The URLs for the css files are: http://10.0.0.88/DEVELOPMENT/ajax/css/bootstrap.min.css http://10.0.0.88/DEVELOPMENT/ajax/css/font-awesome.min.css

exupero commented 6 years ago

The URLs that are being loaded are puzzling, as the test script below gives me the URLs http://10.0.0.88/DEVELOPMENT/ajax/fonts/glyphicons-halflings-regular.eot and http://10.0.0.88/DEVELOPMENT/ajax/fonts/fontawesome-webfont.eot?v=4.7.0.

Here's the test script, mostly pulled directly from the library:

const urlRegex = /url\(["']?(.+?)["']?\)/;

const detectCssFont = rule => {
  const match = rule.cssText.match(urlRegex);
  const url = (match && match[1]) || '';
  if (!url || url.match(/^data:/) || url === 'about:blank') return;
  const fullUrl =
    url.startsWith('../') ? `${rule.href}/../${url}`
    : url.startsWith('./') ? `${rule.href}/./${url}`
    : url;
  const link = document.createElement('a');
  link.href = fullUrl;
  return link.href;
};

console.log(detectCssFont({
  href: "http://10.0.0.88/DEVELOPMENT/ajax/css/bootstrap.min.css",
  cssText: "@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}",
}));

console.log(detectCssFont({
  href: "http://10.0.0.88/DEVELOPMENT/ajax/css/font-awesome.min.css",
  cssText: "@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}",
}));

To load fonts via the fonts option, try passing an array of objects like the following:

{
  url: 'http://10.0.0.88/DEVELOPMENT/ajax/fonts/glyphicons-halflings-regular.eot',
  format: 'application/vnd.ms-fontobject',
  text: "@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}",
}
crestAT commented 6 years ago

Thanks a lot keeping an eye on this, I will try your suggestions today and keep you informed!

crestAT commented 6 years ago

I tried your suggestion but it's not working, maybe the function call I set up is wrong ... ?

saveSvgAsPng(svgElement, fileName + '.png', { scale: 1, backgroundColor: "transparent", fonts: { url: 'http://10.0.0.88/DEVELOPMENT/ajax/fonts/glyphicons-halflings-regular.eot', format: 'application/vnd.ms-fontobject', text: "@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}" }});

There was no error message at the console and the download did not start ... Thanks

exupero commented 6 years ago

fonts should be a list of objects, not just an object.

shrickus commented 6 years ago

I am also having trouble with the FontAwesome font urls -- but in my case, the problem is that rule.href is not defined. So when that value is prepended onto the src urls, the generated fontList looks like this:

[
  {
    "text": "@font-face { font-family: FontAwesome; src: url(\"../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0\") format(\"embedded-opentype\"), url(\"../fonts/fontawesome-webfont.woff2?v=4.7.0\") format(\"woff2\"), url(\"../fonts/fontawesome-webfont.woff?v=4.7.0\") format(\"woff\"), url(\"../fonts/fontawesome-webfont.ttf?v=4.7.0\") format(\"truetype\"), url(\"../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular\") format(\"svg\"); font-weight: normal; font-style: normal; }",
    "format": "application/vnd.ms-fontobject",
    "url": "undefined/../../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0"
  }
]

Notice the url starts with "undefined/../.." which of course returns a 404 error in the browser.

shrickus commented 6 years ago

Unfortunately, it seems that fixing these font urls will not actually help me with my particular svg page... because the font used by all my svg <text> elements is defined in CSS as the default font for the html <body> of the page. Should that inheritance be preserved when I pass the root <svg> element to the saveSvgAsPng() function?

I have verified that it does use the font-family IF I add it as a style attribute directly to the svg tag itself. So how can I tell the library to use fonts inherited from the enclosing html elements 5 levels up in the DOM hierarchy? If I try to pass the <div> element that contains my svg, it renders nothing -- is that working as intended?

exupero commented 6 years ago

Thanks, I did find a problem with the font URL handling and pushed up 320b56aca265dc410f58ff60d1099ed2cfc4e926 to address it. Please try it out and let me know what you find. The custom font code has not been battle tested, so further bug reports are welcome.

I don't know of a way to detect inherited font rules, but I'm open to ideas. I usually supply font styles for svg text in order to ensure proper handling.

This library is not able to render HTML elements outside of an SVG tag, so it's not surprising that the render fails when passing the div that styles are attached to.

shrickus commented 6 years ago

Thanks for the quick fix...

And I think I found a way to pull in the CSS properties on the svg element, inherited from the containing html elements (e.g. <div>, <body>):

> getComputedStyle(topsvg).fontFamily
""Helvetica Neue", Arial, Helvetica, sans-serif"
> getComputedStyle(topsvg).fontSize
"14px"
> getComputedStyle(topsvg).fontWeight
"400"
> getComputedStyle(topsvg).color
"rgb(51, 51, 51)"
> getComputedStyle(topsvg).fill
"rgb(0, 0, 0)"
> getComputedStyle(topsvg).stroke
"none"
> getComputedStyle(topsvg).fillOpacity
"1"
> getComputedStyle(topsvg).fillRule
"nonzero"
> getComputedStyle(topsvg).fontStretch
"100%"
> getComputedStyle(topsvg).fontStyle
"normal"
> getComputedStyle(topsvg).outlineColor
"rgb(51, 51, 51)"
> getComputedStyle(topsvg).outlineOffset
"0px"
> getComputedStyle(topsvg).outlineStyle
"none"
> getComputedStyle(topsvg).outlineWidth
"0px"
> getComputedStyle(topsvg).strokeOpacity
"1"
> getComputedStyle(topsvg).strokeWidth
"1px"
> getComputedStyle(topsvg).textShadow
"none"

In fact, there are hundreds of CSS properties returned by the getComputedStyle() function. Looks like the hard part will be deciding which ones should be applied to the canvas rendering code... but i think the ones listed above would be a good start.

exupero commented 6 years ago

Thanks! That sounds like a much better way to handle styling than fiddling with CSS specifiers. I wonder if there's a way to inline only the properties whose values differ from the default?

shrickus commented 6 years ago

I'm thinking that might not be necessary -- what if those inherited styles are just applied as-is to the root <svg> element being rendered? Or maybe to the <canvas> element itself?

shrickus commented 6 years ago

I've been trying to apply these inherited styles using the fonts, modifyCss, and selectorRemap API options -- but I don't have enough documentation to understand the correct syntax to use (or frankly, whether I'm even using the right technique).

Can you give me an example of how to use one (or more) of these options to apply this style:

body {
    font-size: 14px;
    font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;
}

to the root <svg> element that I'm passing into the saveAsPngUri(...) function?

Assuming that works to render my svg fonts correctly, would that make sense to be included into your library as the default behavior? Or at least enabled by a new useInheritedCss option, perhaps...

exupero commented 6 years ago

Here's an example of selector remapping:

https://github.com/exupero/saveSvgAsPng/blob/320b56aca265dc410f58ff60d1099ed2cfc4e926/index.js#L44

And here's an example of modifying the CSS:

https://github.com/exupero/saveSvgAsPng/blob/320b56aca265dc410f58ff60d1099ed2cfc4e926/index.js#L50-L54

Unfortunately, as it is, the library first filters out CSS rules that don't directly apply to the node or its parent, so the body rules likely won't get passed through either function. To change that in the source, you'd need to modify this logic:

https://github.com/exupero/saveSvgAsPng/blob/320b56aca265dc410f58ff60d1099ed2cfc4e926/src/saveSvgAsPng.js#L199-L203

While I could inline body styles as applying to the SVG node (and I'm still considering it), I suspect that will address your particular case and not necessarily be a general solution to the problem of inherited styles. All the CSS options and behavior are mostly ad hoc band-aids on top of what was originally a very simple use case. That's why I'm much more interested in how to use getComputedStyles to obviate the entire problem of having to fiddle with how CSS rules are inlined.

But if you figure out what code changes would get your body styles, I'll take a look at the pull request.

shrickus commented 6 years ago

I agree that the library should not be specifically grabbing styles off the <body> element -- I was hoping for a more general solution, like you are also.

In that last bit of code, you are using the output of the generateCss(...) function to select the rules to be applied to during image rendering. Could that function was replaced with an inheritedCss(...) function that simply returned similar output, but from getComputedStyles?