How to font fingerprinting edge cases reference
- Step 1Confirm WOFF2 determinism for your encoder — Run the same TTF through your WOFF2 encoder twice and fingerprint both outputs. Matching hashes confirm the encoder is deterministic for your settings. Different hashes point to a non-deterministic step (version drift, timestamp embedding) to pin down.
- Step 2Check for a moving head timestamp — If re-exporting the same font flips the hash, the usual culprit is `head.created`/`head.modified` being rewritten on save. Inspect with [font-metadata-extractor](/font-tools/font-metadata-extractor); in a toolchain, zero or freeze the timestamp (e.g. fonttools `--no-recalc-timestamp`) to stabilise it.
- Step 3Always fingerprint the source binary — Never fingerprint a copy pulled back out of the OS font cache or an installer — macOS Font Book and Windows installers can strip/reorder tables. Hash the artifact straight from the foundry/CDN so the pin reflects the real source.
- Step 4Compare across formats only after converting — A TTF and its WOFF2 always have different hashes. To compare the same source across formats, convert both to one format first ([ttf-to-woff2](/font-tools/ttf-to-woff2) or [woff2-to-ttf](/font-tools/woff2-to-ttf)) and fingerprint the normalised pair.
- Step 5Cross-check against a CLI hasher — Because the tool hashes raw bytes with SHA-256, `shasum -a 256 font.woff2` returns exactly the `sha256` field. Use this to prove no transcode or normalisation happened between disk and hash.
- Step 6Use the hash as a byte-equality test — Two files with the same `sha256` are byte-identical; two with different hashes are not, however similar they look. Use this to dedupe, to detect drift, and to confirm a 'no-op' update really changed nothing.
Does the hash change? A quick reference
Whether each operation changes a font's SHA-256. 'Changes' means the bytes moved; 'Stable' means byte-identical output.
| Operation | Hash result | Why |
|---|---|---|
| Re-download the identical file | Stable | Same bytes → same hash; SHA-256 is deterministic |
| Re-export from a font editor (no glyph change) | Changes | New head timestamp + re-ordered/re-padded tables |
| Convert TTF → WOFF2 | Changes | Different byte stream entirely (brotli-compressed sfnt) |
| Re-run the same WOFF2 encoder, same settings | Stable | WOFF2 transform is deterministic for a fixed encoder/version |
| Re-run a different encoder version | Changes (often) | Padding/ordering differences between versions |
| Install via OS then re-extract | Changes | OS strips/reorders tables in its font cache |
| Rename the file | Stable | The hash is of bytes, not the filename |
| Subset / strip a table | Changes | Removing glyphs/tables changes the bytes |
Two encodings of the one hash
The Fingerprinter outputs the same digest in two forms; they never disagree because they come from one crypto.subtle.digest call.
| Output | Encoding | Used for |
|---|---|---|
sha256 | 64-char lowercase hex | Pins, manifests, shasum cross-check |
short_hash | first 8 hex chars of sha256 | Filename suffixes, human-readable ids |
sri_attribute base64 | base64 of the same 32 raw digest bytes | integrity="sha256-…" on preload links |
Cookbook
Concrete demonstrations of each surprise. The hash values are illustrative; the behaviours are exactly what the byte-honest fingerprinter reports.
Re-save flips the hash, render unchanged
ExampleOpen in a font editor, export again with no glyph edits — the rendering is identical but the bytes (and hash) move because the head timestamp was rewritten.
before-resave.woff2 → sha256 e3b0c442...7852b855 after-resave.woff2 → sha256 4a8e9f01...0099aabb diff: head.modified timestamp + table padding render: pixel-identical hash: DIFFERENT (the tool reports the byte truth)
TTF and its WOFF2 never match
ExampleSame source glyphs, two formats, two completely different byte streams and hashes. To compare, convert both to one format first.
Inter-Regular.ttf → sha256 11aa22bb...ccdd3344 Inter-Regular.woff2 → sha256 5566eeff...00112233 Same glyphs, different containers → different hashes (expected). To compare logical fonts: convert both to woff2, then fingerprint.
WOFF2 determinism check
ExampleEncode the same TTF to WOFF2 twice with the same tool/settings. Matching hashes prove the step is reproducible — required for stable CI fingerprints.
$ woff2_compress Inter.ttf # run 1 $ shasum -a 256 Inter.woff2 d41d8cd9...e9800998 Inter.woff2 $ woff2_compress Inter.ttf # run 2 (same tool/version) $ shasum -a 256 Inter.woff2 d41d8cd9...e9800998 Inter.woff2 ← identical → deterministic
Rename doesn't change the hash; cache-busted name does
ExampleThe hash is byte-based, so renaming is a no-op for sha256 — but the cache_busted_filename derives its suffix from that hash, not the name you gave the file.
brand.woff2 → sha256 e3b0c442... → cache_busted: brand.e3b0c442.woff2
logo.woff2 (same bytes, copied + renamed)
→ sha256 e3b0c442... → cache_busted: logo.e3b0c442.woff2
same sha256 (same bytes) — only the <stem> differs in the nameOS round-trip changes the bytes
ExampleInstall a font on macOS and re-extract it from the system location — tables get reordered, so the fingerprint differs from the original download.
downloaded/Inter-Regular.otf → sha256 aaaa1111...2222bbbb ~/Library/Fonts/Inter-Regular.otf → sha256 cccc3333...4444dddd OS re-wrote table order on install → different bytes → different hash. Rule: fingerprint the source download, never the installed copy.
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.
Editor re-export changes the hash
By designRe-exporting an unchanged font from FontForge / Glyphs / fonttools almost always flips the sha256. The bytes moved: a fresh head.modified timestamp, re-ordered tables, different padding. The renderer doesn't care, but the hash does — that's byte-honesty, not a bug. For stable hashes across builds, freeze the timestamp (fonttools --no-recalc-timestamp) and fingerprint the canonical source rather than each re-export.
Identical rendering, different bytes
ExpectedTwo files can render pixel-for-pixel the same yet have different sha256 values — different table ordering, different name records, different padding, or a metadata difference the renderer ignores. The Fingerprinter checks bytes, not pixels, so it will flag them as different. If you need visual equivalence, that's a rendering comparison, not a hash; the hash answers 'are these the same file?', which is a stricter question.
WOFF2 re-compression is deterministic — same encoder
SupportedThe WOFF2 transform is deterministic: the same TTF through the same encoder version with the same settings yields byte-identical WOFF2 and therefore the same hash. This is what makes reproducible font builds possible. Pin the encoder version in CI; an encoder upgrade can change padding/ordering and flip the hash even though the input didn't change.
TTF vs WOFF2 of the same source
ExpectedA TTF and the WOFF2 built from it are completely different byte streams (raw sfnt vs brotli-compressed sfnt), so they never share a fingerprint. The tool hashes the file as-is and never decodes, so it can't 'see through' the container to the shared glyphs. To compare logical fonts, normalise both to one format first with ttf-to-woff2.
OS installer round-trip
Changes the bytesInstalling a font via macOS Font Book or a Windows installer and pulling the copy back out of the system font location can reorder or strip tables, producing a different fingerprint from the original file. Always hash the source artifact (the download from the foundry/CDN), never an installed copy, or your pins will reflect the OS's edits rather than the real font.
Zero-byte or non-font input
Hashes anywayThe tool doesn't parse the font, so it will hash an empty file (yielding the well-known empty-input digest e3b0c442…b855) or any non-font bytes that happen to carry a font extension. A valid hash does not prove a valid font. If you need to confirm the bytes are a real TTF/OTF/WOFF/WOFF2, check with font-format-identifier first.
Wrong extension blocks the file before hashing
RejectedAcceptance is by extension only. A genuine WOFF2 saved as brand.dat is refused (not a supported font) before any hashing happens — so you can't fingerprint a mislabelled file without renaming it. Renaming to the correct extension doesn't change the bytes, so the resulting hash is still the true fingerprint of the content.
Over-limit file never reaches the hasher
413-style rejectA file above your tier cap (5 MB free / 50 MB pro / 1 GB developer) is blocked client-side before hashing, with exceeds the … tier per-job limit. Nothing is uploaded. If you need to fingerprint a font larger than your tier allows, upgrade or reduce it first with font-subsetter — but note the subset is a different file with a different hash.
git EOL normalisation corrupts the binary
Silent corruption — invalid fontIf a font is checked into git without a binary attribute, an EOL-normalisation rule can mangle it, changing the hash and the font itself. Mark fonts as binary (*.woff2 binary, etc.) in .gitattributes. A 'the hash changed but I didn't touch the font' mystery is frequently this — the bytes really did change, in the repo.
Variable font hashes as one value
ExpectedA variable font is a single file, so it has a single sha256 regardless of how many named instances or how wide the axis range is. Freezing an instance to a static file with variable-font-freezer creates a new file with a new hash. Fingerprint whichever artifact you actually deploy.
Frequently asked questions
Why do different builds produce different hashes for the same font?
Because the bytes differ even when the design doesn't. The most common cause is the font generator rewriting head.created/head.modified with a build timestamp; re-ordered tables and changed padding also flip the hash. fonttools' --no-recalc-timestamp (or equivalent) keeps timestamps stable across builds. The Fingerprinter hashes whatever bytes you give it, so a stable hash requires a deterministic upstream toolchain.
Can two fonts render identically but have different hashes?
Yes — different byte orderings of OpenType tables, different metadata records, different padding, all produce a different sha256 while the renderer shows identical pixels. The hash answers 'are these the same file?' (byte equality), not 'do these look the same?' (visual equality). The tool deliberately checks the stricter, byte-level question.
Is the SRI base64 a different hash from the hex?
No — one hash, two encodings. sha256 is the digest as lowercase hex; the base64 inside sri_attribute is the same 32 digest bytes encoded base64. They come from a single crypto.subtle.digest call, so they can never disagree. Don't mix them up: hex/short_hash go in filenames and pins; base64 goes in the integrity attribute.
Will a TTF and its WOFF2 ever have the same fingerprint?
No. They're different containers with different byte streams (raw sfnt vs brotli-compressed sfnt), so their hashes always differ even though the glyphs are the same. The tool hashes the file as-is without decoding. To compare logical fonts across formats, convert both to one format first (ttf-to-woff2) and fingerprint the matched pair.
Is WOFF2 compression deterministic?
For a fixed encoder and settings, yes — the same TTF produces byte-identical WOFF2 every time, so the fingerprint is stable. That's what enables reproducible font builds. Different encoder versions can change padding or table ordering and flip the hash, so pin your WOFF2 encoder version in CI if you depend on stable fingerprints.
Does renaming a font change its hash?
No — the SHA-256 is computed over the file's bytes, and the filename isn't part of the bytes. A copy under a different name has the identical sha256. Note, though, that the cache_busted_filename is <stem>.<short8>.<ext>, so the suggested name does include your stem — same hash, different suggested filename if you renamed the stem.
Why does the hash match shasum / sha256sum exactly?
Because the tool hashes the raw file bytes with SHA-256 and never decodes, transcodes, or normalises first. shasum -a 256 font.woff2 runs the same algorithm over the same input and returns the same string as the sha256 field. This is a deliberate property — it means the tool's output is portable and verifiable against any standard hasher.
What's the deterministic-build trap?
Assuming 'same source → same hash' without controlling the toolchain. If any step embeds a timestamp, uses a non-deterministic encoder, or git normalises the binary, the bytes change each build and your fingerprints (and any pins) churn. Make the pipeline deterministic — freeze timestamps, pin tool versions, mark fonts binary in .gitattributes — before relying on hash stability. See the build-script guide.
Can I fingerprint a non-font file?
Only if it has a recognised font extension — acceptance is by extension. If it does, the tool hashes the bytes regardless of whether they're a real font, because it never parses the content. A valid hash therefore doesn't certify a valid font. Confirm the format separately with font-format-identifier when that matters.
How do I get a stable hash for a font I keep re-exporting?
Stop fingerprinting the re-exports and fingerprint the canonical source binary, or make the export deterministic: freeze the head timestamp, pin the exporter version, and disable any randomised padding. Then re-exports of an unchanged design produce byte-identical output and a stable sha256. The drift you're seeing is real byte movement, which the hash faithfully reports.
Does the tool ever change the bytes it hashes?
No. It reads the file into an ArrayBuffer and hashes it directly — there is no normalisation, parsing, or re-encoding step in the fingerprint path. What you drop in is exactly what gets hashed, which is why the result is reproducible everywhere and matches CLI hashers. The font is also never uploaded (0 bytes uploaded).
How does the hash behave for variable fonts?
A variable font is one file, so it has one sha256 covering the whole thing — axes and all named instances included. Choosing a different instance at render time doesn't change the file, so the fingerprint is unchanged. If you freeze an instance to a static file with variable-font-freezer, that's a new file with a new hash; fingerprint whichever one you deploy.
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.