Short version: Hi, how do i open ttf or otf (whichever is easier) in text like manner so i can manually edit/delete vectors for each letter inside the font?
Long version: I made font in FontForge (single stroke) for CAD/CAM from SVG files. Its good and looks like single line font as it should until i convert the letters into entities inside CAD, each and every line inside the font has duplicates stacked on eachother (my suspision is that font generator creates these duplicates to trick windows into readable text). So i would like to open font i made in like xml manner or whatever and delete the duplicated vectors generated by FontForge.
I do have true single stroke font (in ttf format) that when converted into entities in CAD it doesnt have any duplicates, so i opened the font in FontForge and generated new version of it. When new version converted into entities inside CAD it does have duplicates (but original doesnt). I tried also FontCreator which yielded same results. I also opened the font in 010 Editor but even if i knew what to look for i doubt it would work anyway. I understand that font is some kind of table format but if FontForge can read any font you throw at it, knows vectors for each letter and shows it in graphical setup i kinda dont understand why i cant seem to find a way to edit the vectors manually in text editor of some sort. (I need new single stroke font as customer doesnt like the one i already got). Also i need to convert the font into entities inside CAD so i can move the letters separatly on 3D curve where equal spacing of letters next to eachother or putting {space} between them yields somewhat unusable results.
Although svg fonts are deprecated and no longer supported by most browsers, they are still used as an interchange format by some applications such as icomoon.
The markup looks something like this:
<svg>
<defs>
<font id="font-family-name" horiz-adv-x="678" >
<font-face
font-family="font-family-name"
font-weight="500"
font-stretch="normal"
units-per-em="1000"
/>
<glyph glyph-name="H" unicode="H" horiz-adv-x="705"
d="M630 695v-695h-114v304h-327v-304h-114v695h114v-298h327v298h114z" />
<glyph glyph-name="I" unicode="I" horiz-adv-x="264"
d="M189 695v-695h-114v695h114z" />
</font>
</defs>
</svg>
You can edit the d
attributes to reduce the glyph's shapes to single strokes.
Attention: unlike regular svg <paths>
svg font files use a cartesian coordinate system – so if you edit your paths in an editor like inkscape – you'll see flipped glyphs.
Since <glyph>
elements are invisible by default you need to convert them to <path>
elements for previewing.
let svgFont = document.querySelector("svg");
let ns = "http://www.w3.org/2000/svg";
let previewSvg = document.getElementById("previewSvg");
// create preview svg
if (!previewSvg) {
previewSvg = document.createElementNS(ns, "svg");
previewSvg.id = "previewSvg";
document.body.appendChild(previewSvg);
}
let fontFace = document.querySelector("font-face");
let unitsPerEm = +fontFace.getAttribute("units-per-em");
let row = 0;
let column = 0;
let colsInRow = 24;
let paddingL = 400;
let paddingB = 750;
let glyphs = svgFont.querySelectorAll("glyph");
glyphs.forEach((glyph, i) => {
let d = glyph.getAttribute("d");
if (d) {
let glyphName = glyph.getAttribute('glyph-name');
let xOff = (unitsPerEm) * column + paddingL;
let yOff = (unitsPerEm+paddingB) * row;
let g = document.createElementNS(ns, "g");
g.id = glyphName;
let label = document.createElementNS(ns, "text");
label.textContent=glyphName;
label.setAttribute('x', (unitsPerEm)/2);
label.setAttribute('y', unitsPerEm*0.5);
label.setAttribute('font-size', unitsPerEm/6.6);
label.setAttribute('font-family', 'sans-serif');
label.setAttribute('text-anchor', 'middle');
let path = document.createElementNS(ns, "path");
path.setAttribute("d", d);
g.appendChild(label);
g.appendChild(path);
g.setAttribute("transform", `translate(${xOff} ${yOff}) scale(1 1)`);
previewSvg.appendChild(g);
// center glyphs
let bb = path.getBBox();
let sB = Math.abs(unitsPerEm - bb.width)/2;
// svg fonts use cartesian coordinate system, hence we need to flip the display
path.setAttribute("transform", `translate(${sB} 0) scale(1 -1)`);
if (column < colsInRow) {
column++;
} else {
column = 0;
row++;
}
}
});
adjustViewBox(previewSvg);
function adjustViewBox(svg) {
let bb = svg.getBBox();
let bbVals = [bb.x, bb.y, bb.width, bb.height].map((val) => {
return +val.toFixed(2);
});
let maxBB = Math.max(...bbVals);
let [x, y, width, height] = bbVals;
svg.setAttribute("viewBox", [x, y, width, height].join(" "));
}
svg:first-of-type {
width: 0;
height: 0;
}
svg {
overflow: visible;
margin: 1em;
}
#previewSvg path {
stroke: red;
stroke-width: 100px;
paint-order: stroke
}
<svg>
<defs>
<font id="font-family-name" horiz-adv-x="678" >
<font-face
font-family="font-family-name"
font-weight="500"
font-stretch="normal"
units-per-em="1000"
/>
<glyph glyph-name="H" unicode="H" horiz-adv-x="705"
d="M630 695v-695h-114v304h-327v-304h-114v695h114v-298h327v298h114z" />
<glyph glyph-name="I" unicode="I" horiz-adv-x="264"
d="M189 695v-695h-114v695h114z" />
<glyph glyph-name="J" unicode="J" horiz-adv-x="564"
d="M454 695v-501q0 -93 -56.5 -147t-148.5 -54t-148.5 54t-56.5 147h115q1 -46 23.5 -73t66.5 -27t67 27.5t23 72.5v501h115z" />
<glyph glyph-name="K" unicode="K" horiz-adv-x="633"
d="M458 0l-269 311v-311h-114v695h114v-317l270 317h143l-302 -348l307 -347h-149z" />
<glyph glyph-name="L" unicode="L" horiz-adv-x="444"
d="M189 92h235v-92h-349v695h114v-603z" />
</font>
</defs>
</svg>
d
attributes to readable formatUsally svg font contain optimized data e.g relative commands and shorthands. So you might need to convert the commands.
svgFontData.addEventListener("input", (e) => {
upDateSVG();
});
upDateSVG();
function upDateSVG() {
let markup = svgFontData.value;
let parser = new DOMParser();
let doc = parser.parseFromString(markup, "application/xml");
let font = doc.querySelector("svg");
svgFontWrap.appendChild(font);
let glyphs = font.querySelectorAll("glyph");
glyphs.forEach((glyph, i) => {
let d = glyph.getAttribute("d");
if (d) {
// convert to absolute commands - remove shorthands
let pathData = pathDataToLonghands(
pathDataToAbsolute(dStringToPathData(d))
);
setPathData(glyph, pathData);
//console.log(pathData)
}
});
let serializer = new XMLSerializer();
let markupNew = serializer.serializeToString(font);
svgFontNew.value = markupNew;
}
function setPathData(path, pathData) {
let d = "";
pathData.forEach((com) => {
d += `${com.type} ${com.values.join(" ")} `;
});
path.setAttribute("d", d);
}
/**
* create pathData from d attribute
**/
function dStringToPathData(d) {
// sanitize d string
let commands = d
.replace(/[\n\r\t]/g, "")
.replace(/,/g, " ")
.replace(/-/g, " -")
.replace(/(\.)(\d+)(\.)(\d+)/g, "$1$2 $3$4")
.replace(/( )(0)(\d+)/g, "$1 $2 $3")
.replace(/([a-z])/gi, "|$1 ")
.replace(/\s{2,}/g, " ")
.trim()
.split("|")
.filter(Boolean)
.map((val) => {
return val.trim();
});
// compile pathData
let pathData = [];
for (let i = 0; i < commands.length; i++) {
let com = commands[i].split(" ");
let type = com.shift();
let typeLc = type.toLowerCase();
let isRelative = type === typeLc ? true : false;
let values = com.map((val) => {
return parseFloat(val);
});
// analyze repeated (shorthanded) commands
let chunks = [];
let repeatedType = type;
// maximum values for a specific command type
let maxValues = 2;
switch (typeLc) {
case "v":
case "h":
maxValues = 1;
if (typeLc === "h") {
repeatedType = isRelative ? "h" : "H";
} else {
repeatedType = isRelative ? "v" : "V";
}
break;
case "m":
case "l":
case "t":
maxValues = 2;
repeatedType =
typeLc !== "t" ? (isRelative ? "l" : "L") : isRelative ? "t" : "T";
/**
* first starting point should be absolute/uppercase -
* unless it adds relative linetos
* (facilitates d concatenating)
*/
if (typeLc === "m") {
if (i == 0) {
type = "M";
}
}
break;
case "s":
case "q":
maxValues = 4;
repeatedType =
typeLc !== "q" ? (isRelative ? "s" : "S") : isRelative ? "q" : "Q";
break;
case "c":
maxValues = 6;
repeatedType = isRelative ? "c" : "C";
break;
case "a":
maxValues = 7;
repeatedType = isRelative ? "a" : "A";
break;
// z closepath
default:
maxValues = 0;
}
// if string contains repeated shorthand commands - split them
const arrayChunks = (array, chunkSize = 2) => {
let chunks = [];
for (let i = 0; i < array.length; i += chunkSize) {
let chunk = array.slice(i, i + chunkSize);
chunks.push(chunk);
}
return chunks;
};
chunks = arrayChunks(values, maxValues);
// add 1st/regular command
let chunk0 = chunks.length ? chunks[0] : [];
pathData.push({
type: type,
values: chunk0
});
// add repeated commands
if (chunks.length > 1) {
for (let c = 1; c < chunks.length; c++) {
pathData.push({
type: repeatedType,
values: chunks[c]
});
}
}
}
return pathData;
}
/**
* decompose shorthands to "longhand" commands:
* H, V, S, T => L, L, C, Q
* reversed method: pathDataToShorthands()
*/
function pathDataToLonghands(pathData) {
pathData = JSON.parse(JSON.stringify(pathDataToAbsolute(pathData)));
let pathDataLonghand = [];
let comPrev = {
type: "M",
values: pathData[0].values
};
pathDataLonghand.push(comPrev);
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i];
let type = com.type;
let values = com.values;
let valuesL = values.length;
let valuesPrev = comPrev.values;
let valuesPrevL = valuesPrev.length;
let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
let cp1X, cp1Y, cp2X, cp2Y;
let [prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
switch (type) {
case "H":
comPrev = {
type: "L",
values: [values[0], prevY]
};
break;
case "V":
comPrev = {
type: "L",
values: [prevX, values[0]]
};
break;
case "T":
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
// new control point
cpN1X = prevX + (prevX - cp1X);
cpN1Y = prevY + (prevY - cp1Y);
comPrev = {
type: "Q",
values: [cpN1X, cpN1Y, x, y]
};
break;
case "S":
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[cp2X, cp2Y] =
valuesPrevL > 2 ?
[valuesPrev[2], valuesPrev[3]] :
[valuesPrev[0], valuesPrev[1]];
[prevX, prevY] = [
valuesPrev[valuesPrevL - 2],
valuesPrev[valuesPrevL - 1]
];
// new control points
cpN1X = 2 * prevX - cp2X;
cpN1Y = 2 * prevY - cp2Y;
cpN2X = values[0];
cpN2Y = values[1];
comPrev = {
type: "C",
values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
};
break;
default:
comPrev = {
type: type,
values: values
};
}
pathDataLonghand.push(comPrev);
}
return pathDataLonghand;
}
/**
* path data to absolute
**/
function pathDataToAbsolute(pathData, decimals = -1, unlink = false) {
// remove object reference
pathData = unlink ? JSON.parse(JSON.stringify(pathData)) : pathData;
let M = pathData[0].values;
let x = M[0],
y = M[1],
mx = x,
my = y;
// loop through commands
for (let i = 1; i < pathData.length; i++) {
let cmd = pathData[i];
let type = cmd.type;
let typeAbs = type.toUpperCase();
let values = cmd.values;
if (type != typeAbs) {
type = typeAbs;
cmd.type = type;
// check current command types
switch (typeAbs) {
case "A":
values[5] = +(values[5] + x);
values[6] = +(values[6] + y);
break;
case "V":
values[0] = +(values[0] + y);
break;
case "H":
values[0] = +(values[0] + x);
break;
case "M":
mx = +values[0] + x;
my = +values[1] + y;
default:
// other commands
if (values.length) {
for (let v = 0; v < values.length; v++) {
// even value indices are y coordinates
values[v] = values[v] + (v % 2 ? y : x);
}
}
}
}
// is already absolute
let vLen = values.length;
switch (type) {
case "Z":
x = +mx;
y = +my;
break;
case "H":
x = values[0];
break;
case "V":
y = values[0];
break;
case "M":
mx = values[vLen - 2];
my = values[vLen - 1];
default:
x = values[vLen - 2];
y = values[vLen - 1];
}
// round coordinates
if (decimals >= 0) {
cmd.values = values.map((val) => {
return +val.toFixed(decimals);
});
}
}
// round M (starting point)
if (decimals >= 0) {
[M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];
}
return pathData;
}
body{
font-family:sans-serif
}
textarea {
width: 100%;
min-height: 30em;
}
.flex {
display: flex;
gap: 1em;
}
.col {
flex: 1;
}
<div class="flex">
<div class="col">
<h3>Original svg font</h3>
<!-- svg font input -->
<textarea id="svgFontData">
<svg>
<defs>
<font id="font-family-name" horiz-adv-x="678" >
<font-face
font-family="font-family-name"
font-weight="500"
font-stretch="normal"
units-per-em="1000"
/>
<glyph glyph-name="H" unicode="H" horiz-adv-x="705"
d="M630 695 v-695 h-114 v304 h-327 v-304 h-114 v695 h114 v-298 h327 v298 h114z" />
<glyph glyph-name="I" unicode="I" horiz-adv-x="264"
d="M189 695 v-695" />
<glyph glyph-name="J" unicode="J" horiz-adv-x="564"
d="M454 695v-501q0 -93 -56.5 -147t-148.5 -54t-148.5 54t-56.5 147h115q1 -46 23.5 -73t66.5 -27t67 27.5t23 72.5v501h115z" />
<glyph glyph-name="K" unicode="K" horiz-adv-x="633"
d="M458 0l-269 311v-311h-114v695h114v-317l270 317h143l-302 -348l307 -347h-149z" />
<glyph glyph-name="L" unicode="L" horiz-adv-x="444"
d="M189 92h235v-92h-349v695h114v-603z" />
</font>
</defs>
</svg>
</textarea>
</div>
<div class="col">
<h3>Edited svg font: all absolute no shorthands</h3>
<!-- new font output -->
<textarea id="svgFontNew">
</textarea>
</div>
</div>
<div id="svgFontWrap">
</div>
Codepen helpers: