Skip to main content

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​

PropertyTypeDescription
font.isValidboolFont exists and is usable
font.isBoldboolBold weight
font.isItalicboolItalic style
font.isVariableBoldboolHas variable bold axis
font.isVariableItalicboolHas variable italic axis
font.isBoldAvailableboolBold variant exists in family
font.isItalicAvailableboolItalic variant exists in family
font.isCondensedboolCondensed width class
font.isExpandedboolExpanded width class
font.isSerifboolSerif (via PANOSE)
font.isSansSerifboolSans-serif (via PANOSE)
font.isMonospacedboolMonospaced (via PANOSE)
font.weightnumberNumeric weight (400 = Regular, 700 = Bold)
font.normalizedWeightnumberNormalised 0–1 weight
font.widthC++ enumWidth class — read from existing font; cannot be constructed directly
font.familyNamestringFamily name
font.wwsFamilystringWWS family name (weight, width, slant grouping)
font.ribbiFamilystringRIBBI family name (regular, italic, bold, bold-italic)
font.traitsNamestringTraits string (Bold, Italic, etc.)
font.postscriptNamestringPostScript name (unique identifier)
font.postscriptPrefixstringPS name prefix
font.variableBoldnumberVariable bold axis value
font.variableItalicnumberVariable italic axis value
font.panosePanoseFull PANOSE classification

FontFamily properties reference​

PropertyTypeDescription
family.namestringFamily name
family.fontCountnumberNumber of fonts in family
family.hasVariationsboolContains variable fonts
family.hasFixedboolContains fixed-pitch fonts
family.fontsFont[]All fonts in family
family.getFont(i)FontFont 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 (for enumerateFontNames)
  • /dialog — Dialog, DialogResult
  • /storydelta — StoryDelta
  • /glyphatts — GlyphAttStringType
  • /selections — Selection, TextSelection
  • /commands — DocumentCommand

See also​


Example scripts​

16a-font-census.js​

Download 16a-font-census.js

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

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 ✓');
}
}