Skip to content
GlyphPact
Menu

The problem, in detail

Why icon-font codepoints change, and how to stop it

A generated icon font is an application-facing API. The codepoint behindAppIcons.back is as much a contract as a method signature, and it breaks the same way: quietly, at a distance, in code nobody touched.

The mechanism

Icon fonts place glyphs in a Unicode private use area, most often starting at U+E000. Somebody has to decide which icon gets which value, and most generators decide it the simplest way available: walk the current list of source files, hand out consecutive codepoints, stop.

That makes the assignment a function of the file list rather than a stored decision. Nothing anywhere records thatback.svg was U+E000 last Tuesday. Re-derive the mapping from a different file list and you get a different mapping.

Start with two icons:

Assignment derived from the file list
arrows/back.svg      -> 0xE000
status/verified.svg  -> 0xE001

Now add one icon, actions/add.svg. It sorts before everything else, so under a list-derived scheme it takes the first codepoint and pushes both existing icons along:

After adding one icon that sorts firstevery assignment moved
actions/add.svg      -> 0xE000   <- was back.svg
arrows/back.svg      -> 0xE001   <- was verified.svg
status/verified.svg  -> 0xE002   <- moved

The new font is perfectly valid and internally consistent. Itscmap agrees with its glyph table, it passes validation, and it renders. The damage is entirely outside the font, in the code that still says 0xE000 and now gets a plus sign where a back arrow used to be.

This is why the failure is expensive. There is no crash, no exception, and no failing test. A reviewer reading the diff sees a regenerated binary font and a changed constant, both of which look like the expected result of adding an icon. The wrong glyph ships, and it gets found in a screenshot.

The same shift happens when a file is renamed, when a directory is reorganised, when an icon is deleted from the middle of a set, or when a designer reorders a selection in a GUI. None of those are unusual events. All of them silently rewrite the mapping.

The fix is to store the decision

If instability comes from re-deriving the mapping, stability comes from recording it. GlyphPact writes every assignment toiconfont.lock.json in the generated output directory and reads that file back on every subsequent build. An assignment already present is reused exactly; only sources with no recorded assignment draw a new codepoint.

The lock is a plain, documented JSON file. It is meant to be committed, read, and reviewed in a pull request.

iconfont.lock.jsonschema version 1
{
  "schemaVersion": 1,
  "generator": "glyphpact",
  "generatorVersion": "1.0.0",
  "fontFamily": "AppIcons",
  "className": "AppIcons",
  "fontPackage": null,
  "startCodepoint": "0xE000",
  "unitsPerEm": 1000,
  "glyphs": [
    {
      "source": "arrows/back.svg",
      "name": "back",
      "codepoint": "0xE000",
      "sourceSha256": "38121f4ed8a1e2ab...",
      "matchTextDirection": true,
      "geometrySha256": "a86146b122485cd9...",
      "metadata": {
        "author": "GlyphPact contributors",
        "license": "MIT"
      }
    }
  ],
  "retired": []
}

Each entry carries more than a number. sourceSha256 andgeometrySha256 let a later build tell the difference between a file that was renamed and a file whose artwork actually changed, which is what makes a content-preserving rename keep its codepoint instead of being treated as a deletion plus an addition.

The lock file JSON schema is published alongside the compiler, so other tooling can read it without guessing.

The same change, with a lock

What actually happens

This is a real build. A two-icon pack gained four icons, one of which sorts before everything that was already there.

Before: two icons

  1. U+E000arrows/back.svg
  2. U+E001status/verified.svg

After: four added, in discovery order

  1. U+E002actions/add.svg
  2. U+E004arrows/arrow_down.svg
  3. U+E000arrows/back.svgunchanged
  4. U+E001status/verified.svgunchanged
  5. U+E005status/warning.svg

actions/add.svg is now the first source alphabetically, and it holds U+E002. arrows/back.svg is now third, and it still holds the U+E000 it was given when it was first. Position in the pack stopped mattering the moment the assignment was written down.

iconfont.lock.jsongit diff, after adding four icons
--- a/lib/generated/app_icons/iconfont.lock.json+++ b/lib/generated/app_icons/iconfont.lock.json@@ -9,6 +9,30 @@   "unitsPerEm": 1000,   "glyphs": [     {+      "source": "actions/add.svg",+      "name": "actionsAdd",+      "codepoint": "0xE002",+      "sourceSha256": "a169819fd451...",+      "matchTextDirection": false,+      "geometrySha256": "2025c5f8dff2..."+    },+    {+      "source": "arrows/arrow_down.svg",+      "name": "arrowsArrowDown",+      "codepoint": "0xE004",+      "sourceSha256": "b6e87581e2f2...",+      "matchTextDirection": false,+      "geometrySha256": "48a927141186..."+    },+    {       "source": "arrows/back.svg",       "name": "back",       "codepoint": "0xE000",       "sourceSha256": "38121f4ed8a1...",       "matchTextDirection": true,     },     {       "source": "status/verified.svg",       "name": "verified",       "codepoint": "0xE001",     },+    {+      "source": "status/warning.svg",+      "name": "statusWarning",+      "codepoint": "0xE005",+    }   ],   "retired": []
+ addedunchanged assignment
The diff contains additions and nothing else. No existing assignment was modified, so no shipped IconData constant changed meaning.

Deleting an icon does not free its codepoint

Reuse is the second way this breaks, and it is worse than shifting, because a recycled codepoint is one that already shipped. If0xE003 pointed at an archive box in a released app, handing it to a different icon means old builds, cached fonts, and any code still referencing the constant now render the new picture.

So GlyphPact tombstones instead. A removed source moves to theretired array with its codepoint intact, and the value is never allocated again. The active sequence is left with a permanent gap, which is the correct outcome.

After deleting actions/archive.svg0xE003 retired, not reused
"glyphs": [
  { "source": "actions/add.svg",        "codepoint": "0xE002" },
  { "source": "arrows/arrow_down.svg",  "codepoint": "0xE004" },
  { "source": "arrows/back.svg",        "codepoint": "0xE000" },
  { "source": "status/verified.svg",    "codepoint": "0xE001" },
  { "source": "status/warning.svg",     "codepoint": "0xE005" }
],
"retired": [
  { "source": "actions/archive.svg",    "codepoint": "0xE003" }
]

Restoring the file later reactivates the same slot rather than allocating a fresh one, so a revert is a real revert.

Tombstones consume allocation slots. Both active icons and retired ones draw from the range, because that is what permanence costs. The default BMP private use area,U+E000 - U+F8FF, holds 6,400 lifetime slots. For a bigger catalogue, start in a supplementary area with--start-codepoint 0xF0000, which provides 65,534 slots.

A lock only helps if something enforces it

A stored mapping still fails if the committed font stops matching the committed sources. Someone edits an SVG without regenerating, or regenerates without committing, and the repository now holds a font built from an older pack. Nothing in a normal test suite notices.

--check closes that gap. It rebuilds a candidate artifact set from current sources and compares it against what is committed, without writing to the owned output directory. Exit code3 means stale.

.github/workflows/ci.yml
- name: Verify generated icon output is current
  run: glyphpact --config icon_font.json --check

With that step in place, the three failure modes are all covered by something mechanical: the lock prevents reassignment, tombstones prevent reuse, and the check prevents drift. None of them depend on anybody remembering a procedure.

Try it

Start with your own pack

uv tool install glyphpact

Installs the latest stable release from PyPI, currently v1.0.1. Already installed? Run uv tool upgrade glyphpact. Release notes.

Compiling is read-only with respect to your sources, and the strict default policy will tell you about anything in the pack an icon font cannot hold before it writes a font.

Wire the output into a Flutter app

FAQ

Questions about codepoint stability

Why do icon-font codepoints change when icons are added?

Most icon-font generators assign codepoints by walking the current set of source files and handing out values in order, starting from the beginning of a private use area such as U+E000. The assignment is a function of the file list, not a stored decision. Insert an icon whose name sorts early, rename a file, or reorder a selection, and every icon after that point shifts by one.

The font itself is then internally consistent, so nothing fails at build time. The breakage appears in application code that still refers to the old value: a constant for a back arrow keeps pointing at U+E001 while U+E001 now contains a different glyph. The icon renders, it is simply the wrong picture.

How does GlyphPact keep existing codepoints stable?

GlyphPact writes a lock file, iconfont.lock.json, into the generated output directory and reads it back on every later build. Each entry records the source path, the Dart name, the assigned codepoint, and content hashes. An assignment already in the lock is reused as-is; only genuinely new sources draw new codepoints from the end of the allocated range.

Removing an icon does not free its codepoint. The entry moves to a "retired" list in the same file, leaving a permanent tombstone so the value is never handed to a different picture. Restoring the source later reactivates the original slot, and a unique content-preserving rename keeps both its codepoint and its generated Dart name.

How the lock file works

Should iconfont.lock.json be committed to Git?

Yes. The lock file is the codepoint ABI for the generated font, and it only protects existing assignments if the next build can read the same file the previous build wrote. Committing it alongside the generated font, Dart provider, report, and attribution file is the intended workflow.

If the lock is deleted or lost, the next build has no record of prior assignments and will allocate codepoints from scratch, which can reassign icons even when no source file changed.