How to svg font spec quirks and implementation reference
- Step 1Locate the font inside <defs> — Per spec, an SVG `<font>` element lives inside `<defs>` so it defines a resource rather than drawing anything. JAD's output follows this: `<svg xmlns="http://www.w3.org/2000/svg"><defs><font …>…</font></defs></svg>`. There is no visible rendering in the document itself — the `<font>` is a definition.
- Step 2Read the <font> element — `<font id="Family" horiz-adv-x="UPM">`. The `id` is the resolved family name (from the name table, or the filename stem). `horiz-adv-x` on `<font>` is the global default advance, set to the units-per-em. Per-glyph advances override it on each `<glyph>`.
- Step 3Read the <font-face> element — `<font-face font-family=… font-weight=… units-per-em=… ascent=… descent=…>`. `units-per-em` carries the source UPM; `ascent`/`descent` come from the font's ascender/descender (or the UPM-ratio fallback). `font-weight` is the coarse `bold`/`normal` derived from the subfamily name.
- Step 4Understand the coordinate system — Font outlines are Y-up; SVG is Y-down. The converter generates path data with opentype.js `getPath(0, 0, unitsPerEm)` then `toPathData(2)`, which yields SVG path commands that keep glyphs upright in the SVG/SVG-Fonts coordinate convention. You don't apply a flip yourself — it's in the path.
- Step 5Read each <glyph> — `<glyph unicode="X" glyph-name="x" horiz-adv-x="540" d="M…Z"/>`. `unicode` is the single character (XML-escaped). `glyph-name` appears only if the font names the glyph. `horiz-adv-x` is the per-glyph advance. `d` is the outline at 2-decimal precision in UPM units.
- Step 6Account for the missing-glyph and limits — Glyph index 0 becomes `<missing-glyph horiz-adv-x=… d=…>` (no `unicode`). Only the first 5,000 glyph indices are walked, and glyphs without a codepoint (other than index 0) are skipped. Use [Glyph Inspector](/font-tools/glyph-inspector) to compare a single glyph's `d` against the source.
Element and attribute reference (as emitted)
Exactly what JAD's converter writes. The spec defines more optional attributes; this table covers only the ones that appear in real output.
| Element | Attributes emitted | Source / value |
|---|---|---|
<font> | id, horiz-adv-x | Family name; global advance = UPM |
<font-face> | font-family, font-weight, units-per-em, ascent, descent | name table + metrics (with fallbacks) |
<missing-glyph> | horiz-adv-x, d | Glyph index 0 (.notdef); no unicode |
<glyph> | unicode, glyph-name, horiz-adv-x, d | Each codepointed glyph; glyph-name if present |
(not emitted) <hkern> | — | Kerning is omitted entirely |
(not emitted) <vkern> / vertical | — | No vertical metrics or kerning |
Metric fallbacks and derived values
How the converter fills attributes when the source font omits data. These defaults keep the file valid but may not match the font's true intent.
| Attribute | Primary source | Fallback when absent |
|---|---|---|
units-per-em | font.unitsPerEm | Always present in a valid font |
ascent | font.ascender | unitsPerEm × 0.8 |
descent | font.descender | unitsPerEm × -0.2 |
font-weight | Subfamily contains "bold" → bold | normal |
font-family / id | name table fontFamily | Filename stem |
glyph-name | Glyph's name | Attribute omitted |
Path and limit specifics
The numeric facts a parser or validator needs to handle JAD's SVG Font output correctly.
| Property | Value | Note |
|---|---|---|
| Path precision | 2 decimal places | toPathData(2) |
| Coordinate space | UPM units, Y handled for upright glyphs | via getPath(0,0,UPM) |
| Glyph cap | 5,000 indices | Math.min(glyphCount, 5000) |
| Codepoint requirement | unicode required (except index 0) | Non-codepointed glyphs skipped |
| XML escaping | & < > " ' → entities | Applied to unicode, names, family |
| Document root | <svg xmlns=…><defs><font> | Font is a <defs> resource |
Cookbook
Annotated fragments of real output. Use these as the canonical shape when writing a parser, a validator, or a downstream consumer for JAD's SVG Fonts.
The full document skeleton
ExampleThe exact wrapper every conversion produces. The <font> sits inside <defs>; everything is one self-contained document.
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<font id="MyFont" horiz-adv-x="1000">
<font-face font-family="MyFont"
font-weight="normal"
units-per-em="1000"
ascent="800"
descent="-200"/>
<missing-glyph horiz-adv-x="1000" d="…"/>
<glyph unicode="A" glyph-name="A" horiz-adv-x="680" d="M…Z"/>
</font>
</defs>
</svg>A single <glyph> dissected
ExampleEach attribute and where it comes from. unicode is the character itself, not a code like A — XML-escaped only when needed.
<glyph unicode="&" <!-- the '&' char, XML-escaped --> glyph-name="ampersand" <!-- only if the font names it --> horiz-adv-x="760" <!-- this glyph's advance, in UPM units --> d="M120.50 0.00 L…Z"/> <!-- outline, 2-decimal precision -->
The metric fallback in action
ExampleWhen a font omits ascender/descender, the converter derives them from UPM. The output is valid but the values are heuristic, not the designer's numbers.
Font A (metrics present, UPM 2048):
<font-face units-per-em="2048" ascent="1854" descent="-434"/>
Font B (ascender/descender absent, UPM 1000):
<font-face units-per-em="1000" ascent="800" descent="-200"/>
(800 = 1000 × 0.8 ; -200 = 1000 × -0.2)Why glyphs stay upright (the Y-axis note)
ExampleOpenType outlines are Y-up; SVG is Y-down. The path data is generated through opentype.js getPath(), which produces commands that render the glyph the right way up in the SVG coordinate system — no manual transform attribute is added.
Conceptually: OpenType glyph space: +Y is up SVG canvas space: +Y is down opentype.js getPath(0, 0, UPM).toPathData(2) → emits path commands already oriented for SVG → <glyph d="…"/> renders upright, no transform needed
What's deliberately absent
ExampleA parser must not expect kerning, vertical metrics, or OpenType features. The spec allows them; JAD's converter omits them. Treat their absence as the contract.
Will NOT appear in JAD output: <hkern …/> ← horizontal kerning pairs <vkern …/> ← vertical kerning vert-* attrs ← vertical metrics GSUB/GPOS data ← ligatures, contextual alternates, marks If your consumer needs these, the SVG Font format (as emitted here) cannot supply them — use the original sfnt.
Edge cases and what actually happens
Every row below was probed against the live API. Some documented requirements (alphabetical axis order, numerical tuple order) are not actually enforced in practice — useful to know if you've been blaming the wrong thing for a 400.
No <hkern> in the output
Not emittedThe SVG Fonts spec defines <hkern> for kerning pairs, but the converter never writes them — only outlines and advances. A validator or consumer that requires kerning data will find none. This is intentional: the surviving SVG-Font consumers generally read outlines, and emitting full kerning would bloat the already-verbose XML.
unicode attribute holds a literal character, not a code
By designPer the SVG Fonts spec, unicode contains the actual character string (unicode="A"), not a numeric reference like A. JAD XML-escapes only the five XML-special characters. Parsers expecting hex codes will misread the attribute — read it as a UTF-8 character value.
Glyphs without a codepoint are absent
SkippedEvery <glyph> except the index-0 <missing-glyph> requires a unicode. Glyphs reached only via substitution (ligatures, alternates, components) have no codepoint and are skipped. A consumer counting glyphs will see fewer than the source font's glyph count — that's expected, not corruption.
font-weight is only bold or normal
Coarse mappingfont-weight is bold solely when the subfamily name contains "bold"; every other subfamily (Light, Medium, Black, Semibold without an exact match) maps to normal. The spec allows numeric weights, but the converter emits only the two CSS keywords. Don't infer a precise weight from this attribute.
ascent/descent are heuristic when the font omits them
Preserved (fallback)If the font lacks ascender/descender, the converter writes UPM×0.8 and UPM×-0.2. These keep the file valid but won't match the font's true vertical metrics. A layout engine trusting them may set line height slightly wrong. Cross-check with the Font Metrics Analyzer.
Output truncated at 5,000 glyphs
Capped — silentThe glyph loop is Math.min(glyphCount, 5000). Beyond that, glyphs are silently absent; only the Glyphs-exported metric reveals it. A parser must not assume the SVG Font contains the source font's full repertoire. Subset large fonts before conversion with the Font Subsetter.
Path coordinates rounded to 2 decimals
By designtoPathData(2) rounds each coordinate to two decimal places in UPM space. This is sub-pixel-accurate at any realistic size but means coordinates are not bit-exact to the source outline. Tools doing exact outline comparison should account for the rounding rather than treating it as a mismatch.
Cross-engine SVG Fonts rendering was never consistent
Historic — removedThe reason SVG Fonts died as a rendering format: engines implemented different subsets, kerning and features were spottily supported, and there was no compression. No two browsers rendered a complex SVG Font identically, which is why all of them removed it. Use the format only for tools with a single, known parser — never for cross-engine rendering.
Frequently asked questions
Where does the <font> element live in the document?
Inside <defs>. An SVG <font> defines a resource rather than drawing, so the spec places it in <defs>. JAD's output is <svg xmlns=…><defs><font>…</font></defs></svg> — there's nothing rendered in the document itself; the font is a definition for a consumer to use.
How is the unicode attribute formatted?
As the literal character (unicode="A"), per the SVG Fonts spec — not a numeric reference like A. Only the five XML-special characters are escaped to entities. Read the attribute as a UTF-8 character value when parsing.
Does the output include kerning?
No. The spec defines <hkern> but the converter doesn't emit it. The output carries glyph outlines and per-glyph advance widths only. If you need kerning, the source sfnt's GPOS/kern is the place to get it — audit it with the Kerning Pair Auditor.
What are the ascent and descent values based on?
The font's ascender and descender. If those are absent, the converter falls back to units-per-em × 0.8 for ascent and units-per-em × -0.2 for descent. The fallback values are heuristic and may not match the font's intended vertical metrics.
How does the converter handle the Y-up vs Y-down mismatch?
OpenType outlines are Y-up; SVG is Y-down. The path data is produced via opentype.js getPath(0, 0, unitsPerEm).toPathData(2), which emits commands oriented so glyphs render upright in the SVG coordinate system. No separate transform attribute is added to the glyphs.
Why do some glyphs have a glyph-name and others don't?
The glyph-name attribute is written only when the source font assigns a name to that glyph. Fonts that drop or strip glyph names (some subset/optimised fonts) produce <glyph> elements without the attribute. It's informational; consumers key off unicode.
What precision are the path coordinates?
Two decimal places, via toPathData(2). Coordinates are in the units-per-em space, so they're sub-pixel-accurate at normal sizes. They are not bit-exact to the source outline because of the rounding — account for that if you compare outlines programmatically.
Is there a limit on how many glyphs appear?
Yes — 5,000. The converter walks min(glyphCount, 5000) glyph indices. A larger font is truncated silently; only the Glyphs-exported metric tells you. Don't assume the SVG Font holds the source font's entire glyph set.
Does the missing-glyph have a unicode attribute?
No. <missing-glyph> is produced from glyph index 0 (.notdef) and carries horiz-adv-x and d but no unicode — by spec, missing-glyph is the fallback shape, not a codepointed character. It's always present in the output.
Can I rely on font-weight to know the exact weight?
No. It's a two-state value: bold when the subfamily name contains "bold", normal otherwise. Numeric weight classes from the OS/2 table aren't parsed. For precise weight info, read the source font's metadata with the Font Metadata Extractor.
Why was the SVG Fonts spec never updated?
It was added in SVG 1.1 (2003), implemented inconsistently across engines, and superseded by WOFF/WOFF2 for the web. With no path to consistent cross-engine rendering and no compression, there was no appetite to revise it — SVG 2 removed it instead of updating it.
Is the output spec-valid?
It's valid against the parts of the SVG Fonts spec that matter to surviving consumers — proper <defs>/<font>/<font-face>/<missing-glyph>/<glyph> structure with required attributes. It deliberately omits optional features (<hkern>, vertical metrics, OpenType layout) the spec allows but consumers rarely need.
Privacy first
Every JAD Font tool runs entirely in your browser using opentype.js and the wawoff2 WASM Brotli encoder. Your fonts never leave your device — verified by zero outbound network requests during processing.