SVG to PNG with JavaScript
I wanted to take an SVG file generated with some data from Rails and convert
that into a png that could be exported and used as a thumnail on YouTube. One
of the odd requirements for the SVG is that I want to use Google
Fonts and many of the SVG to PNG options weren't
translating the font from a css @import
statement..
I tried a few things that didn't work well and wanted to document incase others
encounter the same.
First, I thought I could just use imagemagic with the
rmagick gem and that would work however I
ran into several issues loading fonts.
Eventually, using JavaScript on the client with an HTML canvas element worked.
I found and modified this code snippet on
bl.ocks.org.
<div id="svg-container">
<%= File.read(File.join(Rails.root, "public", "thumb-template.svg")).html_safe %>
</div>
<canvas id="canvas" width="1920" height="1080"></canvas>
<div id="png-container"></div>
<script>
var svgString = new XMLSerializer().serializeToString(document.querySelector('svg'));
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var DOMURL = self.URL || self.webkitURL || self;
var img = new Image();
var svg = new Blob([svgString], {type: "image/svg+xml;charset=utf-8"});
var url = DOMURL.createObjectURL(svg);
img.onload = function() {
ctx.drawImage(img, 0, 0);
var png = canvas.toDataURL("image/png");
document.querySelector('#png-container').innerHTML = '<img src="'+png+'"/>';
DOMURL.revokeObjectURL(png);
};
img.src = url;
</script>
My workflow starts with designing the thumbnail in Figma, then exporting as SVG
replacing the SVG fonts that were exported by Figma and converting those into
<text>
blocks with specific CSS classes to set the font family. Then,
rendering content into the text block with ERB.
<svg width="1920" height="1080" viewBox="0 0 1920 1080" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<style>
/* this bit doesn't work while converting to png: */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;900');
.subhead {
fill: #C2F7EB;
font-family: Roboto;
font-weight: 300;
font-size: 60px;
text-transform: uppercase;
}
.title {
fill: #F72585;
font-family: Roboto;
font-weight: 900;
font-size: 144px;
}
</style>
<g clip-path="url(#clip0)">
<rect width="1920" height="1080" fill="white"/>
<rect width="1920" height="1080" fill="#1B1725"/>
<text x="77" y="250" class="subhead">Ruby Metaprogramming</text>
<text x="77" y="420" class="title">Object#send</text>
</g>
<defs>
<clipPath id="clip0">
<rect width="1920" height="1080" fill="white"/>
</clipPath>
</defs>
</svg>
An SVG will load fine with the correct fonts when loaded in the browser using
an @import statement pointing at a font CDN like the one for Google fonts, however
when converting the SVG into a PNG those remote font faces are lost.
The next trick to get this bit working was to base64 encode the font file and
use a data url to embed the full content of the font into the CSS for styling
the text blocks.
If you download a font family from Google fonts, you'll get a set of .tff
files.
Using the built in base64
tool on Mac, you can convert the file into base64 and
paste that into the font family like so:
<svg width="1920" height="1080" viewBox="0 0 1920 1080" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<style>
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(data:font/truetype;charset=utf-8;base64,AAEAAAASAQAABAAgR0RFRnBqbY4AAaOkAAAB6kdQT1PZc2ujAAGlkAAATrpHU1VC0HjTzgAB9EwAAAoCT1MvMpcesZEA...)
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(data:font/truetype;charset=utf-8;base64,AAEAAAASAQAABAAgR0RFRnBqbY4AAaAMAAAB6kdQT1MfGyUBAAGh+AAAVhxHU1VC0HjTzgAB+BQAAAoCT1MvMpl2sdgA...)
}
.subhead {
fill: #C2F7EB;
font-family: Roboto;
font-weight: 300;
font-size: 60px;
text-transform: uppercase;
}
.title {
fill: #F72585;
font-family: Roboto;
font-weight: 900;
font-size: 144px;
}
</style>
<g clip-path="url(#clip0)">
<rect width="1920" height="1080" fill="white"/>
<rect width="1920" height="1080" fill="#1B1725"/>
<text x="77" y="250" class="subhead">Ruby Metaprogramming</text>
<text x="77" y="420" class="title">Object#send</text>
</g>
<defs>
<clipPath id="clip0">
<rect width="1920" height="1080" fill="white"/>
</clipPath>
</defs>
</svg>