From 8d1f42a9a001c106c68f5b0b17019633690cf81e Mon Sep 17 00:00:00 2001 From: anvaka Date: Fri, 2 Dec 2016 23:05:40 -0800 Subject: [PATCH] Added support for custom fonts The support is done by parsing all font-face urls, fetching them as binary files, and inlining them as data uri (https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) Note: it's very rudimentary and fragile. But it works when applied correctly, and I thought it could be useful for more people. https://github.com/exupero/saveSvgAsPng/issues/24 --- index.html | 21 ++++++- saveSvgAsPng.js | 162 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 165 insertions(+), 18 deletions(-) diff --git a/index.html b/index.html index 2996bd5..3d545bc 100644 --- a/index.html +++ b/index.html @@ -267,10 +267,29 @@

Preview

  • -
    Custom fonts are not currently supported.
    Custom Fonts +
    +

    + 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$; }); } + })();