I'm using FabricJS to allow a user to design an SVG in the browser. When I'm looking to save I'm trying to use OpenType JS to convert the textbox (Fabric) into an SVG Path using OpenType.
Problem I'm seeing is the location of my textbox is not translating through to the new path addition to the canvas.
AND
When I add the new path to the canvas, then call toSVG() it disappears in the resulting SVG I save.
Code:
async function convertTextToPaths() {
ungroup();
var _all = canvas.getObjects();
for(i=0;i<_all.length;i++) {
var activeObject = _all[i];
if(activeObject.type=="textbox") {
const font = await opentype.load('fonts/'+activeObject.fontFamily+'.ttf');
debugger;
console.log(activeObject.type, activeObject.left, activeObject.top+activeObject.height, activeObject.fontSize);
const path = font.getPath(activeObject.text, activeObject.left, activeObject.top+activeObject.height, activeObject.fontSize);
const outlinetextpath = new fabric.Path(path.toPathData(3));
activeObject.dirty=true;
canvas.remove(activeObject);
canvas.insertAt(outlinetextpath,2);
canvas.renderAll();
}
}
}
Make any sense or can someone share some thoughts?
thank you
You need to calculate some scaling factors/ratios according to your font's metrics for appropriate vertical alignments.
Calling font.getPath(string, x, y, fontSize)
will "draw" a path from bottom to top:
Example: draw text element at x=500, y=250
(canvas size: 1000×500px; font-family: Fira Sans; font-size: 100px;)
fabric.js
let activeObject = new fabric.Textbox('Hamburg', {
left: 500,
top: 250,
fontFamily: 'Fira Sans',
fontSize: 100
});
opentype.js
font.getPath('Hamburg', 500, 250, 100)
Red: opentype.js path; Black: fabric generated textBox
Left: top: 250
Right: object.top+object.height
The opentype.js generated element is vertically aligned to 250 px using the font's baseline as a reference point.
Whereas fabric.js aligns the textBox
element according to it's top (boundary box) y coordinate.
(Download function won't work on SO due to content security policies)
const canvas = new fabric.Canvas("canvas");
const fontFileUrl = 'https://fonts.gstatic.com/s/firasans/v16/va9E4kDNxMZdWfMOD5Vvl4jO.ttf';
const [textBoxX, textBoxY] = [500, 250];
const fontName = "Fira Sans";
const fontWeight = 400;
const fontSizeCanvas = 100;
const textboxString = "Hamburg";
const btnDownload = document.querySelector('.btn-download')
convertTextToPaths();
async function convertTextToPaths() {
//parse font file with opentype.js
const font = await opentype.load(fontFileUrl);
//draw textbox on canvas
let activeObject = new fabric.Textbox(textboxString, {
left: textBoxX,
top: textBoxY,
fontFamily: fontName,
fontWeight: fontWeight,
fontSize: fontSizeCanvas
});
canvas.add(activeObject);
// get properties of fabric.js object
let [type, string, fontSize] = [activeObject.type, activeObject.text, activeObject.fontSize];
let [left, top, height, width] = [activeObject.left, activeObject.top, activeObject.height, activeObject
.width
];
/**
* Get metrics and ratios from font
* to calculate absolute offset values according to font size
*/
let unitsPerEm = font.unitsPerEm;
let ratio = fontSize / unitsPerEm;
// font.descender is a negative value - hence Math.abs()
let [ascender, descender] = [font.ascender, Math.abs(font.descender)];
let ascenderAbs = Math.ceil(ascender * ratio);
let descenderAbs = Math.ceil(descender * ratio);
let lineHeight = (ascender + descender) * ratio;
/**
* calculate difference between font path bounding box and
* canvas bbox (including line height)
*/
let font2CanvasRatio = 1 / lineHeight * height
let baselineY = top + ascenderAbs * font2CanvasRatio;
// Create path object from font
path = font.getPath(string, left, baselineY, fontSize);
//path = font.getPath(string, left, top, fontSize);
let pathData = path.toPathData();
// render on canvas
const outlinetextpath = new fabric.Path(pathData, {
fill: 'red'
});
canvas.add(outlinetextpath);
//optional: just for illustration: render center and baseline
canvas.add(new fabric.Line([0, 250, 1000, 250], {
stroke: 'red'
}));
canvas.add(new fabric.Line([0, baselineY, 1000, baselineY], {
stroke: 'purple'
}));
// Download/export svg
upDateSVGExport(canvas);
canvas.on('object:modified', function(e) {
//console.log('changed')
upDateSVGExport(canvas);
});
}
function upDateSVGExport(canvas) {
let svgOut = canvas.toSVG();
let svgbase64 = 'data:image/svg+xml;base64,' + btoa(svgOut);
btnDownload.href = svgbase64;
}
@font-face {
font-family: "Fira Sans";
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/firasans/v16/va9E4kDNxMZdWfMOD5Vvl4jO.ttf) format("truetype");
}
* {
box-sizing: border-box;
}
body {
font-family: "Fira Sans";
font-weight: 400;
background: #000;
margin: 0;
padding: 1em;
}
.fabricContainer {
display: block;
width: 100%;
height: auto;
max-width: 100%!important;
position: relative;
background: #fff;
aspect-ratio: 2/1;
}
.canvas-container {
width: 100%!important;
height: auto!important;
}
.canvas-container>canvas {
height: auto !important;
max-width: 100%;
}
canvas {
display: block;
width: 100% !important;
font-family: inherit;
}
h3 {
font-weight: 400
}
.btn-download {
text-decoration: none;
background: #777;
color: #fff;
padding: 0.3em;
position: absolute;
width: auto !important;
bottom: 0.5em;
right: 0.5em;
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.5.0/fabric.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/opentype.js@latest/dist/opentype.min.js"></script>
<div class="fabricContainer" id="fabricContainer">
<canvas id="canvas" width="1000" height="500"></canvas>
<a class="btn-download" href="#" download="fabric2Svg.svg">Download svg</a>
</div>
Essentially, we need to compare heights as rendered by fabrics.js testBox
objects with ideally rendered boundaries based on font metrics.
First we need to get some ratios to translate font units to pixels.
Most importantly we need to calculate a ratio/factor to translate relative font metric values to absolute font size related pixel values:
1. Font metrics: font size to font unit ratio
let unitsPerEm = font.unitsPerEm;
let ratio = fontSize / unitsPerEm;
Most webfonts have a 1000 unitsPerEm value.
However, traditional truetype fonts (so not particularly optimised for web usage) usually use 2048 units per em.
2. Font metrics: ascender and descender
// font.descender is a negative value - hence Math.abs()
let [ascender, descender] = [font.ascender, Math.abs(font.descender)];
let ascenderAbs = Math.ceil(ascender * ratio);
let descenderAbs = Math.ceil(descender * ratio);
let lineHeight = (ascender + descender) * ratio;
With regards to the font's metrics an ideal bBox would have the height of
(ascender + descender) * FontSize2unitsPerEmRatio
.
3. Font metrics to canvas coordinates
Fabric.js bBox is slightly bigger – so we need to compare their heights to get the perfect scaling factor.
let font2CanvasRatio = 1 / lineHeight * height
Now we get the right y offset when using getPath()
let baselineY = top + ascenderAbs * font2CanvasRatio;
path = font.getPath(string, left, baselineY, fontSize);