Skip to content

Commit

Permalink
Added support for custom fonts
Browse files Browse the repository at this point in the history
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.

exupero#24
  • Loading branch information
anvaka committed Dec 3, 2016
1 parent 3478b86 commit 8d1f42a
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 18 deletions.
21 changes: 20 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,29 @@ <h3>Preview <button class="save btn">Save as PNG</button></h3>
</li>

<li id=custom-font>
<div style="color:red">Custom fonts are not currently supported.</div>
<svg width=200 height=200>
<text x=100 y=100 text-anchor=middle dy=14 style="font-family:'Stalemate';font-size:36pt;">Custom Fonts</text>
</svg>
<div style="color:red;">
<p>
Custom fonts are supported but in a very rudimentary way.
</p>
<p>
<ul>
<li>
Make sure that the custom font is applied to a non-svg element first:
<span style='font-family: "Stalemate"'>hello</span>
This will help browser to rasterize SVG correctly onto canvas.
</li>
<li>
@font-face declartion has to be inside document stylesheets (not in the external `link` tag)
</li>
<li>
Only first `url()` is inlined into svg (don't have multiple urls in the font-face).
</li>
</ul>
</p>
</div>
</li>
</ul>
</div>
Expand Down
162 changes: 145 additions & 17 deletions saveSvgAsPng.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = "<![CDATA[\n" + css + "\n]]>";
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 = "<![CDATA[\n" + css + "\n]]>";
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);
}
}
});
}
Expand Down Expand Up @@ -333,4 +460,5 @@
return out$;
});
}

})();

0 comments on commit 8d1f42a

Please sign in to comment.