mrkeyoor.com_
Sat 08 Aug 21:58 UTC
npmWeb Frontendupdated 08 Aug 2026

fontace

fontace is a small, synchronous font metadata reader built for generating CSS @font-face rules. Give it a Node.js Buffer containing a TTF, WOFF, or WOFF2 file and it returns the embedded family name, CSS-ready format, normal or italic style, fixed or variable weight, whether the font is variable, and a compressed CSS unicode-range string. It does not render text, subset fonts, or rewrite font files; its job is to turn an existing font binary into the handful of descriptors a build tool needs.

Verdict

Install fontace when your exact task is turning font bytes into @font-face metadata; the focused API saves real parsing work. Pick a full font library if you need anything beyond those CSS descriptors, and account for its ESM-only, Buffer-based, pre-1.0 contract.

API stability4/5The public surface is a single named function with one Buffer argument and a seven-field return object, so there is little room for accidental complexity. The published declaration makes every field explicit, but version 0.4.1 remains below 1.0 and the ESM-only export leaves no compatibility entry point for CommonJS consumers.
Docs4/5The README explains every returned field and includes local-file, remote-file, variable-font, and complete @font-face generation examples. It is unusually clear for such a small package, though it does not document corrupt-file errors, memory behavior, collection rejection, or the exact supported format boundary exposed by the implementation.
Maintenance4/5Version 0.4.1 was published in February 2026 and the repository was pushed in August 2026, so this is active rather than an abandoned helper. The project has CI and a changesets release flow, but its small contributor footprint, pre-1.0 version, and dependence on one underlying parser still create some continuity risk.
Ecosystem3/5The package receives 3,742,748 weekly downloads and fits standard npm, ESM, Buffer, and CSS tooling without peers or native binaries. Its own GitHub community is tiny at 8 stars and no forks, and the narrow one-function API has little plugin ecosystem; most surrounding capability lives in larger font parsers instead.

Use it if

  • You are writing a build plugin that discovers @font-face descriptors directly from local TTF, WOFF, or WOFF2 files
  • You need a CSS unicode-range plus an iterable array of the same ranges without writing OpenType table parsing code
  • You need to distinguish a fixed weight such as 400 from a variable wght range such as 100 900
  • You already have font bytes in a Node.js Buffer and want a synchronous, one-function API
Skip it if

Setup reality

Installation is one command, npm install fontace, and there are no peer dependencies, credentials, config files, or native compilation steps. The package is ESM-only, so the first integration requirement is an ESM module or a dynamic import from CommonJS. Its single public function is synchronous and expects a Node.js Buffer. fs.readFileSync already returns one; fetch returns an ArrayBuffer, so convert it with Buffer.from(await response.arrayBuffer()) before calling fontace. The parser dependency, fontkitten, is installed transitively. Parsing is done in your process and there is no streaming API, which means a build that examines many files should limit concurrency, avoid rereading identical files, and decide how to report corrupt input. Do not pass a path or URL to fontace because it does no I/O. TTF, WOFF, and WOFF2 are the formats mapped to CSS format() values. A font collection causes a thrown error, so wrap files from untrusted uploads in try/catch. The family value comes from the file rather than its filename, unicode ranges can be long, and the generated CSS still needs your chosen URL, font-display policy, quoting, and escaping. Version 0.4.1 is still pre-1.0, so pinning the minor line is sensible for a build pipeline even though the current API is tiny.

Patterns

Read metadata from a local fontread-local-font

import { readFile } from 'node:fs/promises';
import { fontace } from 'fontace';

const bytes = await readFile('./public/fonts/Inter-Regular.woff2');
const metadata = fontace(bytes);
console.log(metadata);

readFile returns a Buffer, which is exactly the input accepted by fontace; pass bytes, not the file path.

Inspect a remote fontread-remote-font

import { fontace } from 'fontace';

const response = await fetch('https://cdn.example.com/fonts/Inter.woff2');
if (!response.ok) throw new Error(`font download failed: ${response.status}`);
const metadata = fontace(Buffer.from(await response.arrayBuffer()));

fetch produces an ArrayBuffer; convert it to a Node.js Buffer before parsing and check the HTTP status yourself.

Generate a complete @font-face rulegenerate-font-face

import { fontace } from 'fontace';

function toFontFace(url, bytes) {
  const m = fontace(bytes);
  const tech = m.isVariable ? ' tech(variations)' : '';
  return `@font-face {
  font-family: ${JSON.stringify(m.family)};
  font-style: ${m.style};
  font-weight: ${m.weight};
  font-display: swap;
  unicode-range: ${m.unicodeRange};
  src: url(${JSON.stringify(url)}) format('${m.format}')${tech};
}`;
}

fontace supplies descriptors, not the URL or font-display policy; JSON.stringify safely quotes typical family names and URLs.

Branch on variable-font metadatadetect-variable-font

const { isVariable, weight } = fontace(fontBytes);

if (isVariable) {
  console.log(`variable font weight range: ${weight}`);
} else {
  console.log(`fixed font weight: ${weight}`);
}

isVariable becomes true for any variation axis, while weight is a range only when the font has a wght axis.

Inspect individual Unicode rangesiterate-unicode-ranges

const { unicodeRangeArray } = fontace(fontBytes);

for (const range of unicodeRangeArray) {
  console.log(range);
}

The array is equivalent to splitting unicodeRange on comma-space, but avoids reparsing the CSS string.

Handle parser and collection errorshandle-invalid-font

function inspectFont(bytes) {
  try {
    return { ok: true, metadata: fontace(bytes) };
  } catch (error) {
    return {
      ok: false,
      message: error instanceof Error ? error.message : String(error),
    };
  }
}

Font collections are rejected explicitly, and malformed input can also throw from the underlying parser.

Scan a directory of font filesscan-font-directory

import { readdir, readFile } from 'node:fs/promises';
import { extname, join } from 'node:path';

const formats = new Set(['.ttf', '.woff', '.woff2']);
const rows = [];
for (const name of await readdir('./public/fonts')) {
  if (!formats.has(extname(name).toLowerCase())) continue;
  rows.push({ file: name, ...fontace(await readFile(join('./public/fonts', name))) });
}

Process deliberately chosen formats and catch errors per file if a single bad font should not abort the whole build.

Group files by embedded family namegroup-font-family

const byFamily = new Map();
for (const font of fontFiles) {
  const metadata = fontace(font.bytes);
  const variants = byFamily.get(metadata.family) ?? [];
  variants.push({ file: font.name, ...metadata });
  byFamily.set(metadata.family, variants);
}

Use the embedded family value rather than inferring a family from filenames, which often include weight and subset labels.

Allow only formats fontace mapsvalidate-font-format

const metadata = fontace(uploadedBytes);
const allowed = new Set(['truetype', 'woff', 'woff2']);
if (!allowed.has(metadata.format)) {
  throw new Error(`unsupported font format: ${metadata.format}`);
}

The current return type is already limited to these three values; an outer allowlist also makes an upload policy explicit.

Create a serializable font manifestbuild-css-manifest

const manifest = Object.fromEntries(
  fontFiles.map(({ publicUrl, bytes }) => {
    const metadata = fontace(bytes);
    return [publicUrl, metadata];
  }),
);

await writeFile('./dist/fonts.json', JSON.stringify(manifest, null, 2));

The returned object contains only strings, booleans, and a string array, so it can be serialized without cleanup.

Cache repeated font parsing by content hashcache-by-content

import { createHash } from 'node:crypto';

const metadataCache = new Map();
function inspectOnce(bytes) {
  const key = createHash('sha256').update(bytes).digest('hex');
  if (!metadataCache.has(key)) metadataCache.set(key, fontace(bytes));
  return metadataCache.get(key);
}

fontace is synchronous and has no cache; content hashing avoids reparsing an identical font copied under multiple names.

Sort fixed and variable font variantssort-font-variants

const variants = fontFiles
  .map(({ name, bytes }) => ({ name, ...fontace(bytes) }))
  .sort((a, b) => {
    const aWeight = Number.parseInt(a.weight, 10);
    const bWeight = Number.parseInt(b.weight, 10);
    return aWeight - bWeight || a.style.localeCompare(b.style);
  });

A variable weight such as '100 900' sorts by its minimum here; keep the original string when writing CSS.

Alternatives

PackageRegistryPick it when
fontkitnpmChoose it when you also need glyph access, layout, font collections, or font subsetting rather than CSS-facing metadata only
opentype.jsnpmChoose it for browser-capable font parsing plus paths, glyph metrics, kerning, and font construction
fontkittennpmChoose the underlying parser when you need OpenType table data and are willing to derive CSS descriptors yourself