+ Custom fonts are supported but in a very rudimentary way.
+
+
+
+
+ Make sure that the custom font is applied to a non-svg element first:
+ hello
+ This will help browser to rasterize SVG correctly onto canvas.
+
+
+ @font-face declartion has to be inside document stylesheets (not in the external `link` tag)
+
+
+ Only first `url()` is inlined into svg (don't have multiple urls in the font-face).
+
+
+
+
diff --git a/saveSvgAsPng.js b/saveSvgAsPng.js
index 2d57a69..06311d7 100644
--- a/saveSvgAsPng.js
+++ b/saveSvgAsPng.js
@@ -66,8 +66,13 @@
}
}
- function styles(el, selectorRemap, modifyStyle) {
+ function styles(el, options, cssLoadedCallback) {
+ var selectorRemap = options.selectorRemap;
+ var modifyStyle = options.modifyStyle;
var css = "";
+ // each font that has extranl link is saved into queue, and processed
+ // asynchronously
+ var fontsQueue = [];
var sheets = document.styleSheets;
for (var i = 0; i < sheets.length; i++) {
try {
@@ -102,13 +107,129 @@
var cssText = modifyStyle ? modifyStyle(rule.style.cssText) : rule.style.cssText;
css += selector + " { " + cssText + " }\n";
} else if(rule.cssText.match(/^@font-face/)) {
- css += rule.cssText + '\n';
+ // below we are trying to find matches to external link. E.g.
+ // @font-face {
+ // // ...
+ // src: local('Abel'), url(https://fonts.gstatic.com/s/abel/v6/UzN-iejR1VoXU2Oc-7LsbvesZW2xOQ-xsNqO47m55DA.woff2);
+ // }
+ //
+ // This regex will save extrnal link into first capture group
+ var fontUrlRegexp = /url\(["']?(.+?)["']?\)/;
+ // TODO: This needs to be changed to support multiple url declarations per font.
+ var fontUrlMatch = rule.cssText.match(fontUrlRegexp);
+
+ var externalFontUrl = (fontUrlMatch && fontUrlMatch[1]) || '';
+ var fontUrlIsDataURI = externalFontUrl.match(/^data:/);
+ if (fontUrlIsDataURI) {
+ // We should ignore data uri - they are already embedded
+ externalFontUrl = '';
+ }
+
+ if (externalFontUrl) {
+ // okay, we are lucky. We can fetch this font later
+ fontsQueue.push({
+ text: rule.cssText,
+ // Pass url regex, so that once font is downladed, we can run `replace()` on it
+ fontUrlRegexp: fontUrlRegexp,
+ format: getFontMimeTypeFromUrl(externalFontUrl),
+ url: externalFontUrl
+ });
+ } else {
+ // otherwise, use previous logic
+ css += rule.cssText + '\n';
+ }
}
}
}
}
}
- return css;
+
+ // Now all css is processed, it's time to handle scheduled fonts
+ processFontQueue(fontsQueue);
+
+ function getFontMimeTypeFromUrl(fontUrl) {
+ var supportedFormats = {
+ 'woff2': 'font/woff2',
+ 'woff': 'font/woff',
+ 'otf': 'application/x-font-opentype',
+ 'ttf': 'application/x-font-ttf',
+ 'eot': 'application/vnd.ms-fontobject',
+ 'sfnt': 'application/font-sfnt',
+ 'svg': 'image/svg+xml'
+ };
+ var extensions = Object.keys(supportedFormats);
+ for (var i = 0; i < extensions.length; ++i) {
+ var extension = extensions[i];
+ // TODO: This is not bullet proof, it needs to handle edge cases...
+ if (fontUrl.indexOf('.' + extension) > 0) {
+ return supportedFormats[extension];
+ }
+ }
+
+ // If you see this error message, you probably need to update code above.
+ console.error('Unknown font format for ' + fontUrl+ '; Fonts may not be working correctly');
+ return 'application/octet-stream';
+ }
+
+ function processFontQueue(queue) {
+ if (queue.length > 0) {
+ // load fonts one by one until we have anything in the queue:
+ var font = queue.pop();
+ processNext(font);
+ } else {
+ // no more fonts to load.
+ cssLoadedCallback(css);
+ }
+
+ function processNext(font) {
+ // TODO: This could benefit from caching.
+ var oReq = new XMLHttpRequest();
+ oReq.addEventListener('load', fontLoaded);
+ oReq.addEventListener('error', transferFailed);
+ oReq.addEventListener('abort', transferFailed);
+ oReq.responseType = 'arraybuffer';
+ oReq.open('GET', font.url);
+ oReq.send();
+
+ function fontLoaded() {
+ // TODO: it may be also worth to wait until fonts are fully loaded before
+ // attempting to rasterize them. (e.g. use https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet )
+ var fontBits = oReq.response;
+ var fontInBase64 = arrayBufferToBase64(fontBits);
+ updateFontStyle(font, fontInBase64);
+ }
+
+ function transferFailed(e) {
+ console.warn('Failed to load font from: ' + font.url);
+ console.warn(e)
+ css += font.text + '\n';
+ processFontQueue();
+ }
+
+ function updateFontStyle(font, fontInBase64) {
+ var dataUrl = 'url("data:' + font.format + ';base64,' + fontInBase64 + '")';
+ css += font.text.replace(font.fontUrlRegexp, dataUrl) + '\n';
+
+ // schedule next font download on next tick.
+ setTimeout(function() {
+ processFontQueue(queue)
+ }, 0);
+ }
+
+ }
+ }
+
+ function arrayBufferToBase64(buffer) {
+ var binary = '';
+ var bytes = new Uint8Array(buffer);
+ var len = bytes.byteLength;
+
+ for (var i = 0; i < len; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+
+ return window.btoa(binary);
+ }
}
function getDimension(el, clone, dim) {
@@ -151,8 +272,6 @@
clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, ''));
var svg = document.createElementNS('http://www.w3.org/2000/svg','svg')
- svg.appendChild(clone)
- clone = svg;
} else {
console.error('Attempted to render non-SVG element', el);
return;
@@ -191,18 +310,26 @@
outer.appendChild(clone);
- var css = styles(el, options.selectorRemap, options.modifyStyle);
- var s = document.createElement('style');
- s.setAttribute('type', 'text/css');
- s.innerHTML = "";
- var defs = document.createElement('defs');
- defs.appendChild(s);
- clone.insertBefore(defs, clone.firstChild);
-
- if (cb) {
- var outHtml = outer.innerHTML;
- outHtml = outHtml.replace(/NS\d+:href/gi, 'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href');
- cb(outHtml, width, height);
+ // In case of custom fonts we need to fetch font first, and then inline
+ // its url into data-uri format (encode as base64). That's why style
+ // processing is done asynchonously. Once all inlining is finshed
+ // cssLoadedCallback() is called.
+ styles(el, options, cssLoadedCallback);
+
+ function cssLoadedCallback(css) {
+ // here all fonts are inlined, so that we can render them properly.
+ var s = document.createElement('style');
+ s.setAttribute('type', 'text/css');
+ s.innerHTML = "";
+ var defs = document.createElement('defs');
+ defs.appendChild(s);
+ clone.insertBefore(defs, clone.firstChild);
+
+ if (cb) {
+ var outHtml = outer.innerHTML;
+ outHtml = outHtml.replace(/NS\d+:href/gi, 'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href');
+ cb(outHtml, width, height);
+ }
}
});
}
@@ -333,4 +460,5 @@
return out$;
});
}
+
})();