foliojs / fontkit

An advanced font engine for Node and the browser
1.48k stars 219 forks source link

SVG Table Support #260

Open jlarmstrongiv opened 3 years ago

jlarmstrongiv commented 3 years ago

I am trying to render color fonts to an image (either svg or png). While I have been following the docs, I can’t seem to get it to work—rather than color, it’s black and white.

uppercase_G

Here are a few examples to help debug: color-fonts.zip

Pomax commented 3 years ago

You probably want to include the code you used, too

jlarmstrongiv commented 3 years ago

@Pomax the code is similar to the readme and tests.

That would involve loading the font and reading the glyph:

const fontBuffer = fs.readFileSync("./path/to/font.otf")
const font = fontkit.create(fontBuffer);

// failed attempts
let glyph;

glyph = font.glyphsForString("B")[0];

// glyph_id https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html
glyph = font.getGlyph(37);

glyph = font.layout("B").glyphs[0];

Anyway, none of these contained glyph.getImageForSize(size) or glyph.layers. The goal is to be able to render the color font to a png or svg image.

Pomax commented 3 years ago

As per the docs, Fontkit supports the SBIX and COLR tables, but all the fonts you're trying don't use those, they instead use the newer SVG table for color information, which Fontkit never got support for.

jlarmstrongiv commented 3 years ago

Ahh, I see, thank you! So I suppose this should be a feature request.

I did look open the raw files, and the SVGs are definitely contained inside:

image

These svgs can be extracted:

image

It’s possible to see which characters are contained too:

image

So it’s definitely possible to create a very rough parser in the meantime. The best solution would be adding support with fontkit.

Pomax commented 3 years ago

Yeah, the SVG table has a rather simple format: https://docs.microsoft.com/en-us/typography/opentype/spec/svg

Just the smallest header someone could come up with, then the number of SVG records, where each record specifies the range of codepoints it applies to (start-end are inclusive), then the byte offset to the SVG outlines, and the length, so you can read exactly and only the bytes you need for a (collection of) glyph(s).

I don't know if fontkit has a way to just get "a table's raw data", but that would be worth doing a quick hunt for in the codebase, to see if you can just work with that directly.

edit: in fact, looking at https://github.com/foliojs/fontkit/blob/417af0c79c5664271a07a783574ec7fac7ebad0c/src/TTFFont.js#L38-L46 it appears you should be able to get the table by using the data in font.directory.tables["SVG"].

Pomax commented 3 years ago

Hell let me just write this code for you, who knows, might be useful to other people in the future too. First, let's define the most basic data parser and an SVG document record class to make table parsing easier:

class DataParser {
  constructor(data, pos = 0) {
    this.data = data;
    this.seek(pos);
    this.start = this.offset;
  }
  reset() { this.seek(this.start); }
  seek(pos) { this.offset = pos; }
  read(bytes) {
    return Array.from(this.data.slice(this.offset, this.offset + bytes));
  }
  uint(n) {
    let sum = this.read(n)
                  .map((e, i) => e << (8 * (n - 1 - i)))
                  .reduce((t, e) => t + e, 0);
    this.offset += n;
    return sum;
  }
  uint16() { return this.uint(2); }
  uint32() { return this.uint(4); }
  readStructs(RecordType, n, ...args) {
    const records = [];
    while (n--) records.push(new RecordType(this, ...args));
    return records;
  }
}

class SVGDocumentRecord {
  constructor(parser, svgDocumentListOffset) {
    this.parser = parser;
    this.baseOffset = svgDocumentListOffset;
    this.startGlyphID = data.uint16(); // The first glyph ID for the range covered by this record.
    this.endGlyphID = data.uint16();   // The last glyph ID for the range covered by this record.
    this.svgDocOffset = data.uint32(); // Offset from the beginning of the SVGDocumentList to an SVG document.
    this.svgDocLength = data.uint32(); // Length of the SVG document data.
  }
  getSVG() {
    this.parser.seek(this.baseOffset + this.svgDocOffset);
    return this.parser.read(this.svgDocLength).map(b => String.fromCharCode(b)).join(``);
  }
}

And let's also write a convenience function to get the SVG associated with a glyph id:

function getSVGforGlyphId(id) {
  const record = SVGDocumentRecords.find(
    (record) => record.startGlyphID <= id && id <= record.endGlyphID
  );
  return record?.getSVG();
}

With that, we can write a table parser for the SVG table:

// Load the font:

const fontkit = require("fontkit");
const font = fontkit.openSync("./font.otf");

// get the SVG table data and wrap a byte parser around it:

const entry = font.directory.tables["SVG "];
const tableBuffer = font.stream.buffer.slice(entry.offset, entry.offset + entry.length);
const data = new DataParser(tableBuffer);

// Parse the SVG table's header:

const version = data.uint16();
const svgDocumentListOffset = data.uint32();
const reserved = data.uint16();

console.log(`SVG table version ${version}, data offset at ${svgDocumentListOffset}`);

// We can now move our read pointer to the correct offset, and read the SVG records:

data.seek(svgDocumentListOffset);
const numEntries = data.uint16();
const SVGDocumentRecords = data.readStructs(
  SVGDocumentRecord,
  numEntries,
  svgDocumentListOffset
);

console.log(`there are ${numEntries} svg records`);

// And that's it, we can now typeset things.
// For instance, what does the SVG sequence for the phrase "oh look, an SVG table parser!" look like?

const phrase = `oh look, an SVG table parser!`;
const glyphs = font.glyphsForString(phrase);
const svgDocuments = glyphs.map(({ id }) => getSVGforGlyphId(id));

console.log(`The phrase "${phrase}" consists of ${
  glyphs.filter((v,i) => glyphs.indexOf(v)===i).length
} glyphs, of which ${
  svgDocuments.filter(v => !v).length
} do not have SVG definitions`);

And done, we've successfully written an SVG table parser.

Pomax commented 3 years ago

However, remember that font coordinates by convention have the y coordinates flipped compared to SVG coordinates, so the SVG string you get will have a viewbox 0 0 1000 1000 (for otf, typically) or 0 0 2048 2048 (for ttf, typically) but coordinates with negative y coordinates, so you'll never actually see anything:

image

To fix that, find the top level <g> and add transform="scale(1,-1)" to flip the coordinates to something you can actually see:

image

jlarmstrongiv commented 3 years ago

Wow, thank you so much @Pomax ! Do you have a donation link or a favorite charitable organization?

For the orientation, I’ve found the glyph path in fontkit to be very helpful:

const run = font.layout(character);
const flipped = run.glyphs[0].path.scale(-1, 1).rotate(Math.PI);
const boundingBox = flipped.bbox;
const d = flipped.toSVG();
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${boundingBox.minX} ${
          boundingBox.minY
        } ${boundingBox.maxX - boundingBox.minX} ${
          boundingBox.maxY - boundingBox.minY
        }">
      <path fill="#000000" d="${d}" />
    </svg>`

I like your suggestion of finding and using the top level <g> and changing the viewbox 👍

Reference: https://web.archive.org/web/20210123120325/http://batik.2283329.n4.nabble.com/Converting-Font-to-SVG-all-glyphs-are-rotated-td3596984.html

Pomax commented 3 years ago

I have a paypal and patreon, usually they're because of https://pomax.github.io/bezierinfo but if you think this was worth a coffee: I do like coffee =P

jlarmstrongiv commented 3 years ago

@Pomax definitely, enjoy a few coffees 😄 thanks again!

jlarmstrongiv commented 3 years ago

@Pomax tangential to this issue of color font support, would you happen to know more about support for COLR tables?

Example font: image

BungeeColor-Regular_colr_Windows.ttf.zip

const fs = require('fs');
const fontkit = require('fontkit');

// rgb to hex https://stackoverflow.com/a/5624139
function rgbToHex({ blue, green, red }) {
  return (
    '#' + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1)
  );
}
function rgbaToFillOpacity({ alpha }) {
  return alpha / 255;
}

// rgba to svg color https://stackoverflow.com/a/6042577
function rgbaToSvgColor(rgba) {
  return {
    fill: rgbToHex(rgba),
    fillOpacity: rgbaToFillOpacity(rgba),
  };
}

const fontBuffer = fs.readFileSync('./BungeeColor-Regular_colr_Windows.ttf');
const font = fontkit.create(fontBuffer);
const run = font.layout('a');
const paths = run.glyphs[0].layers.map((layer) => {
  try {
    const d = font
      .getGlyph(layer.glyph.id)
      .path.scale(-1, 1)
      .rotate(Math.PI)
      .toSVG();
    const { fill, fillOpacity } = rgbaToSvgColor(layer.color);
    return `<path fill="${fill}" fill-opacity="${fillOpacity}" d="${d}" />`;
  } catch (error) {
    console.log(error);
  }
}).filter(Boolean);

const svg = `
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
    <g fill="none">
      ${paths.join('\n')}
    </g>
  </svg>
`;

However, I receive this error:

TypeError: this._font.getGlyph(...)._getContours is not a function
    at TTFGlyph._getContours

The strange thing is that is works some of the time. So, if I use that try/catch block, it will still output some paths. The result:

image

On the other hand, SBIX support via getImageForSize works great 👍

Pomax commented 3 years ago

Let me have a look, but as a note on that rgb to hex function: web colors are #rrggbbaa or #rrggbb, so that would be:

function rgb2hex({r, g, b, a}) {
  return `#${( (r<<16)+(g<<8)+b).toString(16).padStart(6,`0`)}`;
}

That 1<<24 is entirely unnecessary, given that padStart exists =)

However, much easier is to use the rgb() and rgba colors, if you already have rgb values:

function rgbaToSvgColor({red, green, blue, alpha=255}) {
  return {
    fill: `rgb(${red}, ${green}, ${blue})`,
    fillOpacity: (alpha/255).toFixed(2)
  };
}

And even easier is to use rgba() instead of rgb(), because there's no need to set the opacity separately:

function rgbaToSvgColor({red, green, blue, alpha=255}) {
  return `rgba(${red}, ${green}, ${blue}, ${(alpha/255).toFixed(2)})`;
}
jlarmstrongiv commented 3 years ago

Looking forward to it @Pomax ! I kept trying to debug why those paths crashed on the glyph layers, but I don’t feel any closer to a solution.

For the colors, I was under the old impression that svgs did not support transparency or alpha level on SVG fill colours ( https://stackoverflow.com/a/6042577 ), which is why you see that funky workaround. But, I should have read more, it seems modern browsers support it now and I’ll to check and see if other tools support it too. Thank you!

jlarmstrongiv commented 3 years ago

Following up, I wonder if it’s related to https://github.com/foliojs/fontkit/pull/116/files#diff-73702de003b5a6f2d69ac4b8664b227c89c89d93c61ec0d6b9f9e81b816cbe9fR274

Pomax commented 3 years ago

Looks like even if that change reverted, the error's still there though. The problem is that a resolves to glyph id 43, which has a numberOfContours of -1, so it's a compound glyph (meaning, a glyph made of other glyphs, rather than of one single (set of) path(s)).

However, if we look at the glyph information according to fontkit, we get this:

{
  numberOfContours: -1,
  xMin: 54,
  yMin: 0,
  xMax: 676,
  yMax: 720,
  components: [
    Component {
      glyphID: 43,
      dx: 0,
      dy: 0,
      pos: 12,
      scaleY: 1,
      scaleX: 1,
      scale10: 0,
      scale01: 0
    }
  ]
}

So this glyph with id 43 is a compound glyph, consisting of a single other glyph (that's... unusual, but a reasonable edge case) which is... itself. So something's going very wrong here.

Pomax commented 3 years ago

However, if we check the TTX dump for this font, we see:

    <TTGlyph name="A" xMin="54" yMin="0" xMax="676" yMax="720">
      <contour>
        <pt x="330" y="510" on="1"/>
        <pt x="283" y="358" on="1"/>
        <pt x="440" y="358" on="1"/>
        <pt x="393" y="510" on="1"/>
        <pt x="389" y="519" on="0"/>
        <pt x="380" y="527" on="0"/>
        <pt x="374" y="527" on="1"/>
        <pt x="349" y="527" on="1"/>
        <pt x="343" y="527" on="0"/>
        <pt x="334" y="519" on="0"/>
      </contour>
      <contour>
        <pt x="273" y="36" on="1"/>
        <pt x="273" y="17" on="0"/>
        <pt x="256" y="0" on="0"/>
        <pt x="237" y="0" on="1"/>
        <pt x="90" y="0" on="1"/>
        <pt x="71" y="0" on="0"/>
        <pt x="54" y="17" on="0"/>
        <pt x="54" y="36" on="1"/>
        <pt x="54" y="300" on="1"/>
        <pt x="54" y="330" on="0"/>
        <pt x="73" y="408" on="0"/>
        <pt x="93" y="460" on="1"/>
        <pt x="180" y="687" on="1"/>
        <pt x="186" y="704" on="0"/>
        <pt x="211" y="720" on="0"/>
        <pt x="231" y="720" on="1"/>
        <pt x="500" y="720" on="1"/>
        <pt x="519" y="720" on="0"/>
        <pt x="544" y="704" on="0"/>
        <pt x="550" y="687" on="1"/>
        <pt x="637" y="460" on="1"/>
        <pt x="657" y="408" on="0"/>
        <pt x="676" y="330" on="0"/>
        <pt x="676" y="300" on="1"/>
        <pt x="676" y="36" on="1"/>
        <pt x="676" y="17" on="0"/>
        <pt x="659" y="0" on="0"/>
        <pt x="640" y="0" on="1"/>
        <pt x="489" y="0" on="1"/>
        <pt x="469" y="0" on="0"/>
        <pt x="450" y="17" on="0"/>
        <pt x="450" y="36" on="1"/>
        <pt x="450" y="176" on="1"/>
        <pt x="273" y="176" on="1"/>
      </contour>
      <instructions/>
    </TTGlyph>

So this glyph has two contours. Not -1 (which is used to indicate "no contours: compound glyph").

Pomax commented 3 years ago

if we look at the COLR table, we see:

    <ColorGlyph name="A">
      <layer colorID="0" name="A.alt001"/>
      <layer colorID="1" name="A.alt002"/>
    </ColorGlyph>

so looking at those two glyphs in the glyf table again:

    <TTGlyph name="A.alt001" xMin="54" yMin="0" xMax="676" yMax="720">
      <component glyphName="A" x="0" y="0" flags="0x204"/>
      <instructions/>
    </TTGlyph>

    <TTGlyph name="A.alt002" xMin="160" yMin="90" xMax="570" yMax="630">
      <contour>
        <pt x="165" y="272" on="1"/>
        <pt x="567" y="272" on="1"/>
        <pt x="567" y="262" on="1"/>
        <pt x="165" y="262" on="1"/>
      </contour>
      <contour>
        <pt x="570" y="90" on="1"/>
        <pt x="560" y="90" on="1"/>
        <pt x="560" y="303" on="1"/>
        <pt x="560" y="330" on="0"/>
        <pt x="551" y="376" on="0"/>
        <pt x="544" y="396" on="1"/>
        <pt x="475" y="559" on="1"/>
        <pt x="464" y="585" on="0"/>
        <pt x="440" y="620" on="0"/>
        <pt x="412" y="620" on="1"/>
        <pt x="314" y="620" on="1"/>
        <pt x="284" y="620" on="0"/>
        <pt x="260" y="585" on="0"/>
        <pt x="250" y="560" on="1"/>
        <pt x="186" y="396" on="1"/>
        <pt x="179" y="376" on="0"/>
        <pt x="170" y="330" on="0"/>
        <pt x="170" y="303" on="1"/>
        <pt x="170" y="90" on="1"/>
        <pt x="160" y="90" on="1"/>
        <pt x="160" y="303" on="1"/>
        <pt x="160" y="331" on="0"/>
        <pt x="169" y="378" on="0"/>
        <pt x="177" y="400" on="1"/>
        <pt x="241" y="564" on="1"/>
        <pt x="252" y="592" on="0"/>
        <pt x="280" y="630" on="0"/>
        <pt x="314" y="630" on="1"/>
        <pt x="412" y="630" on="1"/>
        <pt x="445" y="630" on="0"/>
        <pt x="472" y="592" on="0"/>
        <pt x="484" y="563" on="1"/>
        <pt x="553" y="400" on="1"/>
        <pt x="561" y="378" on="0"/>
        <pt x="570" y="331" on="0"/>
        <pt x="570" y="303" on="1"/>
      </contour>
      <instructions/>
    </TTGlyph>

so A.alt001 is the compound glyph, with the outlines defined for A.

Pomax commented 3 years ago

So, quick check:

const fs = require("fs");
const fontkit = require("fontkit");
const fontBuffer = fs.readFileSync("./BungeeColor-Regular_colr_Windows.ttf");
const font = fontkit.create(fontBuffer);

const run = font.layout("a");
const glyph = run.glyphs[0];
const layers = glyph.layers;
console.log(layers);
process.exit();

yields

[
  COLRLayer {
    glyph: TTFGlyph {
      id: 292,
      codePoints: [],
      _font: [TTFFont],
      isMark: false,
      isLigature: false
    },
    color: { blue: 0, green: 9, red: 201, alpha: 255 }
  },
  COLRLayer {
    glyph: TTFGlyph {
      id: 293,
      codePoints: [],
      _font: [TTFFont],
      isMark: false,
      isLigature: false
    },
    color: { blue: 128, green: 149, red: 255, alpha: 255 }
  }
]

This is correct.

Pomax commented 3 years ago

Extending this a little:

const fs = require("fs");
const fontkit = require("fontkit");
const fontBuffer = fs.readFileSync("./BungeeColor-Regular_colr_Windows.ttf");
const font = fontkit.create(fontBuffer);
const run = font.layout("a");
const glyph = run.glyphs[0];
const layers = glyph.layers;

layers.forEach(({ glyph, color }) => console.log(glyph))
process.exit();

Yields:

<ref *1> TTFGlyph {
  id: 292,
  codePoints: [],
  _font: TTFFont {
    defaultLanguage: null,
    stream: DecodeStream {
      buffer: <Buffer 00 01 00 00 00 0f 00 80 00 03 00 70 43 4f 4c 52 f6 66 0d c4 00 00 f9 90 00 00 0f ce 43 50 41 4c c9 ff 80 b0 00 01 09 60 00 00 00 16 44 53 49 47 55 57 ... 75298 more bytes>,
      pos: 252,
      length: 75348
    },
    variationCoords: null,
    _directoryPos: 0,
    _tables: {
      GSUB: [Object],
      GPOS: [Object],
      cmap: [Object],
      hhea: [Object],
      maxp: [Object],
      hmtx: [Object],
      'OS/2': [Object],
      CPAL: [Object],
      COLR: [Object]
    },
    _glyphs: {
      '10': [COLRGlyph],
      '43': [COLRGlyph],
      '292': [Circular *1],
      '293': [TTFGlyph]
    },
    directory: {
      tag: '\x00\x01\x00\x00',
      numTables: 15,
      searchRange: 128,
      entrySelector: 3,
      rangeShift: 112,
      tables: [Object]
    }
  },
  isMark: false,
  isLigature: false
}
<ref *1> TTFGlyph {
  id: 293,
  codePoints: [],
  _font: TTFFont {
    defaultLanguage: null,
    stream: DecodeStream {
      buffer: <Buffer 00 01 00 00 00 0f 00 80 00 03 00 70 43 4f 4c 52 f6 66 0d c4 00 00 f9 90 00 00 0f ce 43 50 41 4c c9 ff 80 b0 00 01 09 60 00 00 00 16 44 53 49 47 55 57 ... 75298 more bytes>,
      pos: 252,
      length: 75348
    },
    variationCoords: null,
    _directoryPos: 0,
    _tables: {
      GSUB: [Object],
      GPOS: [Object],
      cmap: [Object],
      hhea: [Object],
      maxp: [Object],
      hmtx: [Object],
      'OS/2': [Object],
      CPAL: [Object],
      COLR: [Object]
    },
    _glyphs: {
      '10': [COLRGlyph],
      '43': [COLRGlyph],
      '292': [TTFGlyph],
      '293': [Circular *1]
    },
    directory: {
      tag: '\x00\x01\x00\x00',
      numTables: 15,
      searchRange: 128,
      entrySelector: 3,
      rangeShift: 112,
      tables: [Object]
    }
  },
  isMark: false,
  isLigature: false
}

This, too, is correct.

Pomax commented 3 years ago

Tracing this further:

const fs = require("fs");
const fontkit = require("fontkit");
const fontBuffer = fs.readFileSync("./BungeeColor-Regular_colr_Windows.ttf");
const font = fontkit.create(fontBuffer);
const run = font.layout("a");
const glyph = run.glyphs[0];
const layers = glyph.layers;

layers.forEach(({ glyph, color }) => {
  const decoded = glyph._decode();
  if (decoded.numberOfContours === -1) {
    decoded.components.map(({ glyphID }) => {
      const g = font.getGlyph(glyphID);
      console.log(glyphID, g.constructor.name, g._getContours, g.path);
    });
  }
  else {
    // const contours = glyph._getContours();
    // console.log(contours);
  }
})
process.exit();

Yields

43 COLRGlyph undefined Path {
  commands: [],
  _bbox: null,
  _cbox: BBox {
    minX: Infinity,
    minY: Infinity,
    maxX: -Infinity,
    maxY: -Infinity
  }
}

And that definitely looks like why things are going wrong: sure, we're looking up glyphid:43 because we found it in a layer, but it should not be a COLRGlyph, it should be a normal TTFGlyph.

Pomax commented 3 years ago

So the change to _getBaseGlyph is supposed to get around this, ensuring that it fetches the glyph as a TTFGlyph instead of a COLRGlyph. However, it fails, because Fontkit is caching glyphs based on their id, but without separate caches for "base glyphs" vs. COLR (etc.) glyphs, which is a bit of a problem when you need the base glyph.

So if we can somehow clear glyph caching, we can make this work. Sort of. It'll be hacky.

Pomax commented 3 years ago

And there we have it: this is not great code, but that's more on Fontkit than on you or me at this point, really... =/

const fs = require("fs");
const fontkit = require("fontkit");
const fontBuffer = fs.readFileSync("./BungeeColor-Regular_colr_Windows.ttf");
const font = fontkit.create(fontBuffer);

function rgbaToSvgColor({ red, green, blue, alpha = 255 }) {
  return {
    fill: `rgb(${red}, ${green}, ${blue})`,
    opacity: (alpha / 255).toFixed(2),
  };
}

function svgPath(glyph) {
  return glyph.path.toSVG();
}

const layout = font.layout("a");

const paths = layout.glyphs[0].layers.map(({ glyph, color }) => {
  const { fill, opacity } = rgbaToSvgColor(color);
  const decoded = glyph._decode();
  const d =
    decoded.numberOfContours === -1
      ? decoded.components
          .map(({ glyphID }) => {
            font._glyphs[glyphID] = false; // EXPLICIT CACHE CLEAR FOR THIS GLYPH
            return svgPath(font._getBaseGlyph(glyphID))
          })
          .join(` `)
      : svgPath(glyph);
  return `<path fill="${fill}" fill-opacity="${opacity}" d="${d}" />`;
});

const svg = `
  <svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 1000 1000">
    <g transform="translate(0,1000) scale(1,-1)">
      ${paths.join(`\n`)}
    </g>
  </svg>
`;

console.log(svg);
fs.writeFileSync(`test.svg`, svg, `utf-8`);

This code yields a file that looks like:

image

And you may have noticed some things. Or not, either way, let's point them out:

  1. this is effectively manually resolving compound glyphs. Which means this will almost certainly break for compounds of compounds. At that point, something like OpenType.js or something might be a better choice.
  2. this assumes the cache stays where it is. Given that Fontkit isn't being maintained, that's not unreasonable, but it's worth remembering.
  3. We're doing the flip-and-reposition using <g> attributes, but this assumes the em quad is actually 1000 units: you'll want to verify that by consulting the head table.
  4. I gave the SVG a width and height value, but these are 100% wrong, and you should pull those numbers from the actual layout result =D

So with all those caveats: have some working code that will at the very least unblock you in whatever you're trying to do that Fontkit is actively trying to fight you on =D

jlarmstrongiv commented 3 years ago

Wow, thank you for not only solving the difficult bug, but also showing your debugging steps 🙇‍♂️ another round of coffees on me

Appreciate the takeaways! I’ll definitely be able to play around with the viewBox, width, height, and em quad and fix the sizing. Pinning the version of fontkit (or writing tests) is definitely a good idea to make sure it continues to work.

I actually started with both opentypejs and fontkit, and eventually found that fontkit could do a lot of what opentypejs could (but work with more formats). I don’t think opentypejs has support either ( https://github.com/opentypejs/opentype.js/issues/193 ).

You mention compounds of compounds—I’ll definitely try to make sure those work. Would that mean adding extra checks to the svgPath function?

// excuse the pseudo code

function svgPath(glyph) {
  const decoded = glyph._decode();
  decoded.numberOfContours === -1
      ? decoded.components
          .map(({ glyphID }) => {
            font._glyphs[glyphID] = false; // EXPLICIT CACHE CLEAR
            return svgPath(font._getBaseGlyph(glyphID))
          })
          .join(` `)
      : glyph.path.toSVG();
}

const layout = font.layout("a");

const paths = layout.glyphs[0].layers.map(({ glyph, color }) => {
  const { fill, opacity } = rgbaToSvgColor(color);
  const d = svgPath(glyph)
  return `<path fill="${fill}" fill-opacity="${opacity}" d="${d}" />`;
});

I think that’s the only thing I had a question about. Amazing stuff.

Pomax commented 3 years ago

Yeah, you'd basically be checking the result of font._getBaseGlyph(glyphID) to see if it, too, is a compound glyph or not. If so, time for (a creatively implemented form of) recursion!

jlarmstrongiv commented 3 years ago

That trick for clearing the cache to get glyph.path.toSVG(); also worked for getting the bounding box (some reducing involved)😄 everything works now—thanks again!

Note to future self: restore the cache afterwards

Pomax commented 3 years ago

Since we're only clearing the cache on a glyph-by-glyph basis (rather than just deleting everything) and the code you're running refills the cache for that glyph, I'm pretty sure there's no need to restore anything.