Skip to content
This repository has been archived by the owner on Jun 25, 2020. It is now read-only.

Primitive inlining of custom fonts #122

Merged
merged 4 commits into from
Dec 4, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why this is removed.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That logic is only hit if you pass a node within an SVG rather than the SVG itself. It probably shouldn't be removed for the changes you've made.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like the only blocker to merging this in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I pressed a wrong button in vim and the code was removed in mistake :).

Added it back.

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

})();