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.
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.
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
- You need CommonJS: version 0.4.1 declares type module and exports only ./dist/index.js, so require('fontace') is not a supported entry point
- You need browser-native input: the published type definition accepts Buffer, and the README's remote example converts an ArrayBuffer with Buffer.from
- You need TTC or another font collection format: the implementation explicitly throws when fontkitten reports isCollection
- You need glyph outlines, shaping, layout, subsetting, or font rewriting: the public API returns seven metadata fields and nothing for manipulating the font
- You need detailed variation-axis metadata: isVariable checks whether any axis exists, but the returned weight range only reads the wght axis and style is reduced to normal or italic
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
| Package | Registry | Pick it when |
|---|---|---|
| fontkit | npm | Choose it when you also need glyph access, layout, font collections, or font subsetting rather than CSS-facing metadata only |
| opentype.js | npm | Choose it for browser-capable font parsing plus paths, glyph metrics, kerning, and font construction |
| fontkitten | npm | Choose the underlying parser when you need OpenType table data and are willing to derive CSS descriptors yourself |