fontseditorcad

Open ttf/otf in readable text


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.


Solution

  • Svg font format as intermediate

    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.

    1. Convert your ttf (or otf) to a svg font with a converter like transfonter.
    2. Open the downloaded font in a code editor.

    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.

    1. When done: convert your svg fontfile back to .ttf

    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.

    Preview helper

    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>

    Normalize d attributes to readable format

    Usally 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: