Munter/subfont

Feature request: Output to buffers

deckchairlabs opened this issue · 6 comments

What a fantastic project firstly! Love it.

I would love to be able to get the resulting outputs as buffers so I can do with them whatever I need.

When using as a module obviously. Is this something that is out of scope for the project?

Thanks for the nice words! 🤗

subfont does a lot of things, roughly:

  1. Loads a website into memory from disc or http (using assetgraph)
  2. Discovers the set of @font-face declarations on each page
  3. Traces each webpage to figure out which bits of text get rendered, along with the font-related parts of the computed style. This bit uses a separate module, font-tracer, that also works in a headless browser
  4. Snaps the traces to the available fonts on each page, also using a separate module: font-snapper
  5. Works out which font files need to be subset given the traces, and which characters to include in the subsets
  6. Creates the subset fonts using pyftsubset (from fonttools) or harfbuzzjs
  7. Adds the subset fonts and to the in-memory assetgraph instance, and update the CSS to reference them
  8. Does a bunch of extra stuff to the website such as self-hosting Google Web Fonts and making sure that the fonts get preloaded.
  9. Writes the website with all the above manipulations back to disc

You can add subfont as a dependency in your project and use it programmatically. It exports this function that does all of the above:

module.exports = async function subfont(

If you pass the dryRun:true option to the programmatic api, you'll opt out of the last step of writing the website back to disc. You will receive the assetgraph instance as the return value, from which you could extract the subset fonts:

(async () => {
  const assetGraph = await subfont({
    inputFiles: [ '/path/to/project/index.html' ],
    formats: ['woff2'],
    fallbacks: false,
    silent: true,
    dryRun: true,
  });
  const [subfontCss] = assetGraph.findAssets({
    type: 'Css',
    path: '/subfont/',
  });
  subfontCss.prettyPrint();
  console.log(subfontCss.text);
  for (const relation of assetGraph.findRelations({
    from: subfontCss,
    type: 'CssFontFaceSrc',
  })) {
    // relation.node is the postcss ast node of the @font-face rule:
    const fontFaceInfo = {};
    relation.node.walkDecls((declaration) => {
      fontFaceInfo[declaration.prop] = declaration.value;
    });
    if (!/__subset$/.test(fontFaceInfo['font-family'])) {
      // Not one of the subsets
      continue;
    }
    console.log(fontFaceInfo);

    const fontAsset = relation.to;
    const buffer = fontAsset.rawSrc;
    console.log(
      `Found font ${fontAsset.urlOrDescription}, size: ${buffer.length} bytes`
    );
  }
})();

Example output:

@font-face {
    font-display: swap;
    font-family: Open Sans__subset;
    font-stretch: normal;
    font-style: normal;
    font-weight: 400;
    src: url(/subfont/Open_Sans-400-619a000603.woff2) format("woff2");
    unicode-range: U+20-21, U+2c, U+48, U+64-65, U+6c, U+6f, U+72, U+77;
}
{
  'font-display': 'swap',
  'font-family': 'Open Sans__subset',
  'font-stretch': 'normal',
  'font-style': 'normal',
  'font-weight': '400',
  src: 'url(/subfont/Open_Sans-400-619a000603.woff2) format("woff2")',
  'unicode-range': 'U+20-21, U+2c, U+48, U+64-65, U+6c, U+6f, U+72, U+77'
}
Found font testdata/subsetFonts/css-source-map-inline/subfont/Open_Sans-400-619a000603.woff2, size: 2736 bytes

This is a bit rough and might not be exactly what you had in mind. We've be thinking about splitting the code up further and making a richer programmatic api.

Could you write a bit more about your use case and which of the above step(s) you are interested in?

Ideally I'd like to pass a string of HTML, which could be rendered from a CMS for instance, then queue up a task for generating the subfont for that rendered HTML string. Which would then be injected into the final output at a later stage.

I'll give your suggestions a go and see what I can come up with.

Would that HTML contain all the relevant CSS as inline stylesheets? Subfont needs that to figure out which font-related CSS properties get applied to each DOM element.
And what about the original fonts? They’re needed when creating the subsets (Not an issue if you use Google Fonts or another http-based service).
These are the primary reasons why we’ve chosen to launch the tool on a url or local file system path.
Is your challenge that your CMS uses a templating language so there’s no “HTML source” for subfont to read and patch up?

No the HTML would contain absolute urls to both stylesheets and fonts. I suppose I could save the output to a temporary file and set the root manually to the domain?

Ok, so the stylesheets and fonts are hosted on a http server? Then it would probably work.

That's correct. A quick test proved it was possible, I'll be fleshing out my service a bit more in the coming weeks so will report back any findings. Cheers!