Lesson 16 — Fonts & Typography
Typography automation means knowing what fonts are available, how to classify them, and how to apply them to text. This lesson covers the font inspection API: enumerating families, querying weight, width, and serif classification, using the PANOSE system for font matching, and combining font data with the text/story API to build practical typography tools.
Font tasks are common in professional workflows: checking a document uses only approved fonts, substituting a missing typeface with the closest alternative, generating a usage report, or applying consistent styling across a document.
The font API has two distinct layers. The /fonts module is the enumeration layer — FontFamily.all, metadata, classification, and PANOSE descriptors — for auditing, reporting, and font-matching. The /glyphatts and /paragraphatts modules are the application layer — setting specific fonts on text ranges via StoryDelta. Confusing the two (e.g. applying formatting through the enumeration API) produces no-ops or cryptic errors.
PANOSE is a ten-digit numeric classification describing a typeface's visual characteristics (serif style, weight, proportion, contrast, and more). It lets you find the closest available substitute for a missing font by similarity rather than name matching — usually a more visually appropriate result.
By the end you'll be able to enumerate installed fonts, classify them, find the closest substitute for any named typeface, and apply fonts to text ranges via the story API.
What this lesson covers
- Enumerating all installed fonts and font families
- Font classification: serif, sans-serif, monospaced, condensed
- The PANOSE system for font matching
- Querying font properties: weight, width, bold/italic availability
- Font collections
- Combining font data with the text/story API for practical scripts
- Finding the best available font for a given style need
- Document font usage reporting
The fonts module
The /fonts module adds a complete view of the system font library. Before this,
you could apply a named style via StoryDelta, but you couldn't ask "is this font
installed?" or "what serif fonts do I have?". Now you have the full picture.
'use strict';
const { Font, FontFamily, FontCollection, FontWeight, FontWidth } = require('/fonts.js');
Three levels of font data
FontCollection ← named groups (e.g. "Microsoft Office Fonts", "Adobe Fonts")
└── FontFamily ← a named family (e.g. "Helvetica Neue")
└── Font ← an individual variant (e.g. "Helvetica Neue Bold Italic")
You can enter the hierarchy at any level.
Enumerating font families
'use strict';
const { FontFamily } = require('/fonts.js');
const { EnumerationResult } = require('affinity:common');
// Array of all families (convenient but allocates a lot)
const families = FontFamily.all;
console.log(`${families.length} font families installed`);
// Callback-based (memory-efficient for large font libraries)
FontFamily.enumerate(family => {
console.log(family.name, '—', family.fontCount, 'font(s)');
return EnumerationResult.Continue;
});
Inspecting fonts within a family
'use strict';
const { FontFamily } = require('/fonts.js');
const family = FontFamily.all.find(f => f.name === 'Arial');
if (!family) {
console.log('Arial not installed.');
} else {
console.log(`${family.name}: ${family.fontCount} variant(s)`);
console.log(`Has variable fonts: ${family.hasVariations}`);
for (let i = 0; i < family.fontCount; i++) {
const font = family.getFont(i);
console.log(` [${i}] ${font.traitsName}`);
console.log(` Bold: ${font.isBold} Italic: ${font.isItalic}`);
console.log(` Weight: ${font.weight} Width: ${font.width}`);
console.log(` PostScript: ${font.postscriptName}`);
}
// Or iterate directly
for (const font of family.fonts) {
console.log(' ', font.postscriptName);
}
}
Getting a Font object
The correct way to get a Font object is via FontFamily.
'use strict';
const { FontFamily } = require('/fonts.js');
// Build a name-keyed map of all installed families
const familyMap = new Map();
for (const fam of FontFamily.all) {
familyMap.set(fam.name.toLowerCase(), fam);
}
// Get the regular weight font from a named family
function getFontByFamily(name, preferBold = false) {
const fam = familyMap.get(name.toLowerCase());
if (!fam) return null;
let best = null, bestDist = Infinity;
const targetWeight = preferBold ? 0.75 : 0;
for (const f of fam.fonts) {
if (!f.isValid) continue;
const d = Math.abs(f.normalizedWeight - targetWeight);
if (d < bestDist) { bestDist = d; best = f; }
}
return best;
}
const georgia = getFontByFamily('Georgia');
if (georgia) {
console.log('Valid: ', georgia.isValid);
console.log('Serif: ', georgia.isSerif);
console.log('PS name: ', georgia.postscriptName);
}
// Platform defaults (these always work with no arguments)
const { Font } = require('/fonts.js');
const def = Font.createDefault();
const mono = Font.createDefaultMonospaced();
const sym = Font.createDefaultSymbol();
Font classification via PANOSE
PANOSE is a 10-byte classification system for fonts. The SDK exposes the key
classification queries directly on the Font object:
'use strict';
const { Font } = require('/fonts.js');
for (const font of Font.all.slice(0, 50)) {
const family = font.familyName;
if (font.isSerif) console.log('Serif: ', family, font.traitsName);
if (font.isSansSerif) console.log('Sans: ', family, font.traitsName);
if (font.isMonospaced) console.log('Mono: ', family, font.traitsName);
}
For fine-grained classification, access the Panose object directly:
'use strict';
const { Font, FontFamily, PanoseType } = require('/fonts.js');
// Get a font via FontFamily — this is always safe
const tnr = FontFamily.all.find(f => f.name === 'Times New Roman');
const font = tnr ? tnr.getFont(0) : Font.createDefault();
const panose = font.panose;
console.log('Is serif: ', panose.isSerif);
console.log('Is sans-serif:', panose.isSansSerif);
console.log('Is monospaced:', panose.isMonospaced);
console.log('Is empty: ', panose.isEmpty);
console.log('PANOSE string:', panose.toString());
// Type-specific classification
console.log('Is serif type:', panose.is(PanoseType.Serif));
Font distance (finding the closest match)
Both Font and Panose expose a getDistance(other) method. Smaller distance = closer match.
Useful for finding a substitute when a font isn't installed:
'use strict';
const { Font, FontFamily } = require('/fonts.js');
// Try to find 'Futura' via FontFamily — no risk of INVALID_ARGS
const futuraFamily = FontFamily.all.find(f => f.name === 'Futura');
if (!futuraFamily) {
console.log('Futura not installed — finding closest match...');
// Use createEmpty() as a baseline for distance comparison
// Or better: use the default font as the reference point
const target = Font.createDefault();
let bestFont = null;
let bestDist = Infinity;
for (const font of Font.all) {
const dist = target.getDistance(font);
if (dist < bestDist) {
bestDist = dist;
bestFont = font;
}
}
if (bestFont) {
console.log('Best match:', bestFont.familyName, bestFont.traitsName);
console.log('Distance: ', bestDist);
}
} else {
console.log('Futura is installed:', futuraFamily.fontCount, 'variant(s)');
}
To find a substitute for a specific missing font, the most reliable approach is to compare PANOSE data: get the PANOSE of any installed font with a similar style, then measure font.panose.getDistance(candidate.panose) across all installed fonts.
Font collections
Collections are named groups of fonts (Adobe Fonts, Microsoft Office Fonts, etc.):
'use strict';
const { FontCollection } = require('/fonts.js');
for (const collection of FontCollection.all) {
console.log('Collection:', collection.displayName);
}
Practical: check which fonts a document uses
The Document object exposes enumerateFontNames — fonts referenced in text nodes:
'use strict';
const { Document } = require('/document.js');
const { Font, FontWeight, FontWidth } = require('/fonts.js');
const doc = Document.current;
if (!doc) { console.log('No document.'); }
else {
const fontNames = doc.getFontNames(); // returns [{ name, isInstalled }] pairs via enumeration
// Or use the raw enumeration
const missing = [];
doc.enumerateFontNames((name, isInstalled) => {
if (!isInstalled) missing.push(name);
return 0; // continue
});
console.log('Fonts used in document:');
doc.enumerateFontNames((name, isInstalled) => {
console.log(` ${isInstalled ? '✓' : '✗'} ${name}`);
return 0;
});
if (missing.length > 0) {
console.log(`\n⚠ ${missing.length} font(s) missing:`);
for (const name of missing) console.log(' -', name);
}
}
Practical: font picker dialog
Combine the fonts module with the Dialog API to build a font chooser:
'use strict';
const { Document } = require('/document.js');
const { Font, FontFamily } = require('/fonts.js');
const { Dialog, DialogResult } = require('/dialog.js');
const { DocumentCommand } = require('/commands.js');
const { StoryDelta } = require('/storydelta.js');
const { GlyphAttStringType } = require('/glyphatts.js');
const { Selection, TextSelection } = require('/selections.js');
const doc = Document.current;
if (!doc) { console.log('No document.'); }
else {
const families = FontFamily.all;
const names = families.map(f => f.name);
const dlg = Dialog.create('Choose Font');
const col = dlg.addColumn();
const grp = col.addGroup('Font family');
const combo = grp.addComboBox('Family', names, 0);
const result = dlg.runModal();
if (result != DialogResult.Ok) { console.log('Cancelled.'); }
else {
const chosenFamily = families[combo.selectedIndex];
const firstFont = chosenFamily.fontCount > 0 ? chosenFamily.getFont(0) : null;
if (!firstFont) { console.log('No fonts in family.'); }
else {
// Apply to selected text via StoryDelta
const textNode = doc.selection.nodes.filter(n => n.isTextNode).first;
if (!textNode) {
console.log('Select a text node too.');
} else {
doc.executeCommand(DocumentCommand.createSetCurrentSpread(doc.currentSpread));
const si = textNode.storyInterface;
const range = si.storyRange;
const textSel = TextSelection.create([{ begin: range.begin, end: range.end - 1 }]);
const sel = Selection.create(doc, textNode);
sel.addSubSelectionForNode(textNode, textSel);
const delta = StoryDelta.createGlyphString(GlyphAttStringType.FontPostscriptName, firstFont.postscriptName);
doc.executeCommand(DocumentCommand.createFormatText(sel, delta));
console.log(`Applied font: ${firstFont.postscriptName}`);
}
}
}
}
Font properties reference
| Property | Type | Description |
|---|---|---|
font.isValid | bool | Font exists and is usable |
font.isBold | bool | Bold weight |
font.isItalic | bool | Italic style |
font.isVariableBold | bool | Has variable bold axis |
font.isVariableItalic | bool | Has variable italic axis |
font.isBoldAvailable | bool | Bold variant exists in family |
font.isItalicAvailable | bool | Italic variant exists in family |
font.isCondensed | bool | Condensed width class |
font.isExpanded | bool | Expanded width class |
font.isSerif | bool | Serif (via PANOSE) |
font.isSansSerif | bool | Sans-serif (via PANOSE) |
font.isMonospaced | bool | Monospaced (via PANOSE) |
font.weight | number | Numeric weight (400 = Regular, 700 = Bold) |
font.normalizedWeight | number | Normalised 0–1 weight |
font.width | C++ enum | Width class — read from existing font; cannot be constructed directly |
font.familyName | string | Family name |
font.wwsFamily | string | WWS family name (weight, width, slant grouping) |
font.ribbiFamily | string | RIBBI family name (regular, italic, bold, bold-italic) |
font.traitsName | string | Traits string (Bold, Italic, etc.) |
font.postscriptName | string | PostScript name (unique identifier) |
font.postscriptPrefix | string | PS name prefix |
font.variableBold | number | Variable bold axis value |
font.variableItalic | number | Variable italic axis value |
font.panose | Panose | Full PANOSE classification |
FontFamily properties reference
| Property | Type | Description |
|---|---|---|
family.name | string | Family name |
family.fontCount | number | Number of fonts in family |
family.hasVariations | bool | Contains variable fonts |
family.hasFixed | bool | Contains fixed-pitch fonts |
family.fonts | Font[] | All fonts in family |
family.getFont(i) | Font | Font at index |
Key gotchas
font.postscriptName may be empty on some fonts. Even when isValid is true, some properties like postscriptName may be empty. Always prefer getting fonts via FontFamily.all and family.getFont(i) for reliable property access.
panose.is(PanoseType.Serif) and panose.isSerif can disagree. The convenience property uses a broader classification; panose.is() applies a stricter type test. Prefer font.isSerif for general classification checks.
Font.all enumerates every individual font — not families. A family with 8 variants appears as 8 entries. For family-level work, use FontFamily.all.
PANOSE is not always populated. Some fonts have an empty PANOSE record (panose.isEmpty === true). In this case isSerif, isSansSerif, isMonospaced will all return false, not because the font isn't serif, but because the data isn't there.
font.postscriptName is the stable identifier. Use this when applying fonts via StoryDelta — it's more reliable than the family name, which can have platform-specific variations.
doc.enumerateFontNames callback convention. The callback receives (name, isInstalled) and must return 0 to continue (not EnumerationResult.Continue as in other SDK callbacks). This is an inconsistency in the API.
Exercises
Exercise 16.1 — Font census with classification
List all installed font families. For each, classify it as Serif, Sans-serif, Monospaced, or Unknown. Print a summary count at the end.
Exercise 16.2 — Missing font reporter
Open a document with some text. Use doc.enumerateFontNames to find any fonts that are referenced but not installed. Log their names.
Exercise 16.3 — Font distance finder
Ask the user for a font name via a Dialog TextBox. If not installed, find and report the three closest installed alternatives using font.getDistance().
Exercise 16.4 — Font picker tool
Build a Dialog with a ComboBox of all installed family names. When confirmed, apply the first font in the chosen family to the current text selection using StoryDelta and GlyphAttStringType.FontPostscriptName.
Exercise 16.5 — Variable font detector
Find and list all font families that contain at least one variable font (family.hasVariations === true). For each, list the family name and how many fonts it has.
SDK modules used in this lesson
/fonts—Font,FontFamily,FontCollection,FontWeight,FontWidth,Panose,PanoseType/document—Document(forenumerateFontNames)/dialog—Dialog,DialogResult/storydelta—StoryDelta/glyphatts—GlyphAttStringType/selections—Selection,TextSelection/commands—DocumentCommand
See also
Example scripts
16a-font-census.js
'use strict';
// Example 15a — Font census
// Lesson 15: Fonts & Typography
//
// Lists all installed font families with classification and a summary.
const { Font, FontFamily } = require('/fonts.js');;
const families = FontFamily.all;
let serif = 0, sans = 0, mono = 0, variable = 0, unknown = 0;
console.log(`=== Font Census: ${families.length} families ===\n`);
for (const family of families) {
const first = family.fontCount > 0 ? family.getFont(0) : null;
let tag = '? ';
if (first && !first.panose.isEmpty) {
if (first.isMonospaced) { tag = 'MONO'; mono++; }
else if (first.isSerif) { tag = 'SERF'; serif++; }
else if (first.isSansSerif) { tag = 'SANS'; sans++; }
else { tag = '? '; unknown++; }
} else {
unknown++;
}
const varFlag = family.hasVariations ? ' [variable]' : '';
if (family.hasVariations) variable++;
console.log(`[${tag}] ${family.name} (${family.fontCount})${varFlag}`);
}
console.log(`\n=== Summary ===`);
console.log(`Serif: ${serif}`);
console.log(`Sans-serif: ${sans}`);
console.log(`Monospaced: ${mono}`);
console.log(`Unknown/empty: ${unknown}`);
console.log(`Variable: ${variable}`);
console.log(`Total: ${families.length}`);
16b-missing-font-reporter.js
Download 16b-missing-font-reporter.js
'use strict';
// Example 15b — Missing font reporter
// Lesson 15: Fonts & Typography
//
// Reports all fonts used in the document that are not installed.
// Uses doc.enumerateFontNames() — callback returns 0 to continue
// (different from the EnumerationResult pattern used elsewhere in the SDK).
const { Document } = require('/document.js');
const { FontFamily } = require('/fonts.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
const installed = [];
const missing = [];
doc.enumerateFontNames((name, isInstalled) => {
if (isInstalled) installed.push(name);
else missing.push(name);
return 0; // 0 = continue (not EnumerationResult.Continue)
});
const total = installed.length + missing.length;
console.log(`Document: ${doc.title}`);
console.log(`Fonts referenced: ${total}`);
console.log(`Installed: ${installed.length}`);
console.log(`Missing: ${missing.length}`);
if (installed.length > 0) {
console.log('\n✓ Installed:');
for (const name of installed) console.log(` ${name}`);
}
if (missing.length > 0) {
// Build a lookup of installed family names for substitute suggestions
const installedFamilies = [];
for (const fam of FontFamily.all) {
installedFamilies.push(fam.name.toLowerCase());
}
console.log('\n✗ Missing (not installed):');
for (const name of missing) {
console.log(` ${name}`);
// Suggest the closest name match among installed families
const target = name.toLowerCase();
let bestName = null, bestScore = -1;
for (const famName of installedFamilies) {
// Simple shared-prefix length as a rough similarity score
let score = 0;
const minLen = Math.min(target.length, famName.length);
while (score < minLen && target[score] === famName[score]) score++;
if (score > bestScore) { bestScore = score; bestName = famName; }
}
if (bestName && bestScore > 0) {
console.log(` → Closest installed name: ${bestName}`);
}
}
} else {
console.log('\nAll fonts are installed ✓');
}
}