How to generate a sha-256 fingerprint for any font
- Step 1Drop the font onto the upload zone — Accepts `.ttf`, `.otf`, `.woff`, `.woff2` by extension. One file at a time — the tool is single-file (`multiFile: false`). The hash is taken over the bytes exactly as the file sits on disk; a WOFF2 is hashed in its compressed form, a TTF in its raw sfnt form. The drop zone shows your tier's size cap (`max 5 MB` on free).
- Step 2Click Process — There are no options to set — the panel goes straight from upload to the Process button. `crypto.subtle.digest("SHA-256", arrayBuffer)` runs locally and resolves in milliseconds. On paid tiers with the @jadapps/runner paired, the job auto-routes to the local runner instead, still on your machine.
- Step 3Read the metric chips — Two chips appear above the JSON: `SHA-256 (full)` shows the first 16 hex characters followed by `…` (a glanceable preview, not the whole hash), and `Short hash` shows the full 8-char `short_hash`. The complete 64-char value lives in the JSON body below.
- Step 4Copy or download the JSON — The output pane renders the pretty-printed JSON. Use Copy to clipboard for a paste, or Download to save `<stem>.fingerprint.json`. The four fields are `sha256`, `short_hash`, `cache_busted_filename`, and `sri_attribute`, plus `filename` and `file_size` echoed back.
- Step 5Apply each field where it belongs — Rename the deployed asset to `cache_busted_filename` (or drive your bundler's `[contenthash]` from it). Paste `sri_attribute` onto a `<link rel="preload" as="font">`. Pin `sha256` in your design-system docs to detect silent updates. Use `short_hash` only where a human-readable id is enough.
- Step 6Re-run on the next build to compare — Drop the rebuilt font in again. If the bytes are unchanged the hash is identical to four decimal places of certainty (SHA-256 has no known collisions). A changed hash means the bytes moved — often a font-editor re-save embedded a fresh `head` timestamp, which is exactly the drift you want surfaced.
The four output fields, exactly as emitted
Every run produces this JSON — there are no options, so the shape is fixed. <stem> is the filename without its extension; <short8> is the first 8 hex chars of the SHA-256.
| JSON field | Format | Example value | Where it goes |
|---|---|---|---|
sha256 | 64-char lowercase hex | e3b0c44298fc1c14…b855 (full 64 chars) | Build manifests, integrity logs, the canonical pin in design-system docs |
short_hash | 8-char hex (prefix of sha256) | e3b0c442 | Human-readable version ids, the suffix inside cache_busted_filename |
cache_busted_filename | <stem>.<short8>.<ext> | Inter-Regular.e3b0c442.woff2 | The deployed asset name; reference it in CSS src: url(...) |
sri_attribute | integrity="sha256-<base64>" | integrity="sha256-47DEQpj8HBSa…" | Pasted onto <link rel="preload" as="font"> |
filename / file_size | string / bytes | Inter-Regular.woff2 / 30696 | Echoed back for the record / manifest bookkeeping |
Tier limits for hashing
Per-file size cap by plan. The fingerprinter is single-file; batch limits (free 1, pro 20) apply to tools that accept multiple files, which this one does not.
| Tier | Max file size | Notes |
|---|---|---|
| Free | 5 MB (FREE_FONT_FILE_LIMIT_BYTES) | Covers nearly every Latin/Cyrillic/Greek WOFF2 and most subsetted faces |
| Pro | 50 MB | Full CJK families, icon megafonts, uncompressed TTC sources |
| Developer | 1 GB | Anything; the digest is streamed by the engine, so time scales with size not memory pressure |
Cookbook
Concrete before/after for each field. The JSON below is the real shape fingerprintFont emits — only the hash values are illustrative.
The full output for one WOFF2
ExampleDrop Inter-Regular.woff2, click Process, and this is the JSON pane. Note the extension is preserved in cache_busted_filename and the SRI base64 is the same digest as the hex, just encoded differently.
{
"filename": "Inter-Regular.woff2",
"file_size": 30696,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"short_hash": "e3b0c442",
"cache_busted_filename": "Inter-Regular.e3b0c442.woff2",
"sri_attribute": "integrity=\"sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=\""
}Drive a cache-busted CSS reference
ExampleTake cache_busted_filename, rename the deployed file to match, and point your @font-face at it. New bytes → new short hash → new filename → automatic cache invalidation.
/* deployed asset renamed to the cache_busted_filename */
@font-face {
font-family: "Inter";
font-weight: 400;
font-display: swap;
src: url("/fonts/Inter-Regular.e3b0c442.woff2") format("woff2");
}Paste the SRI attribute onto a preload
ExampleThe sri_attribute is already fully formatted — copy it verbatim onto a preload link. The browser hashes the downloaded file and refuses it if the hashes disagree.
<link rel="preload" as="font" type="font/woff2" crossorigin
href="/fonts/Inter-Regular.e3b0c442.woff2"
integrity="sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=">Verify the hash matches sha256sum on your machine
ExampleThe tool uses the same SHA-256 over the same raw bytes as a CLI hasher. Cross-check to prove no upload, transcode, or normalisation happened in between.
# macOS / Linux shasum -a 256 Inter-Regular.woff2 # e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 Inter-Regular.woff2 # matches the `sha256` field byte-for-byte — same algorithm, same input
Pin a known-good hash in design-system docs
ExampleStore the full sha256 next to the font in your component library README. On any future rebuild, a mismatch tells you the binary drifted — even when the filename and visual rendering look identical.
## Brand fonts (pinned 2026-06-06) | File | SHA-256 (pinned) | |----------------------|-----------------------------------| | Inter-Regular.woff2 | e3b0c44298fc1c14…7852b855 | | Inter-Bold.woff2 | 9f86d081884c7d65…1b4e0b27c | # re-fingerprint on each release; investigate any change
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.
Re-saving from a font editor flips the hash
By designOpen the font in FontForge / Glyphs and export again with no glyph changes — the sha256 almost always changes. Editors rewrite the head.modified timestamp, reorder tables, and re-pad. The bytes moved, so the hash moved. This is the drift-detection feature working, not a bug: fingerprint the canonical source binary, not a fresh re-export, when you want stable hashes.
Same glyphs, different format → different hash
ExpectedInter-Regular.ttf and the Inter-Regular.woff2 made from it render identically but have completely different sha256 values — WOFF2 is brotli-compressed sfnt, TTF is raw sfnt. The fingerprinter hashes the file as it sits, never decoding first. To compare two formats of the same source, fingerprint after converting both to one format with ttf-to-woff2.
WOFF2 is deterministic across re-compression
SupportedRunning the same TTF through the same WOFF2 encoder twice produces byte-identical output, so the fingerprint is stable. WOFF2's transform is deterministic. Different encoder versions or settings can change padding/ordering and therefore the hash — pin the encoder in CI if you need reproducible WOFF2 fingerprints.
An empty / zero-byte file
SupportedA 0-byte file still produces a valid SHA-256 (e3b0c442…b855, the well-known empty-input digest) — but it is not a font. The fingerprinter does not parse the font, so it will happily hash garbage. Pair with font-format-identifier first if you need to confirm the bytes are actually a TTF/OTF/WOFF/WOFF2.
File over the tier cap
413-style rejectA 60 MB CJK font on the free tier is blocked before hashing: File "…" exceeds the free tier per-job limit (5 MB). Limits are 5 MB free, 50 MB pro, 1 GB developer. The block happens client-side; nothing is uploaded. Upgrade or subset the font first with font-subsetter.
Wrong extension on a real font
RejectedAcceptance is by extension only — isSupportedFontFile checks for ttf/otf/woff/woff2. A valid WOFF2 saved as brand.bin is refused with is not a supported font (TTF/OTF/WOFF/WOFF2). Rename it to the correct extension and re-drop; the hash is taken over the bytes, which the rename does not change.
Extensionless filename in the cache-busted name
Falls back to woff2If the dropped file has no extension, cache_busted_filename defaults the extension to woff2 (file.name.split(".").pop() ?? "woff2"). Since acceptance requires a recognised extension this is rare, but if you somehow feed a dotless name the suggested filename will end .<short8>.woff2 regardless of the real format — rename to taste.
OS / installer round-trip changes the bytes
By designInstalling a font via Font Book or a Windows installer and re-extracting it can reorder or strip tables, so the extracted copy fingerprints differently from the original download. Always hash the source artifact from the foundry/CDN, not a copy pulled back out of the OS font cache.
Two files hash the same
ExpectedIdentical bytes always produce identical hashes — that is the whole point. If Inter-Regular.woff2 and a copy named body.woff2 share a sha256, they are the same font under two names. Dedupe with the hash as the key; SHA-256 has no known collisions, so equal hashes mean equal bytes in practice.
Frequently asked questions
Why SHA-256 specifically?
It is hardware-accelerated by the browser's Web Crypto (crypto.subtle.digest), has no known collisions, and is the algorithm Subresource Integrity and modern content-hashing both standardise on. SHA-1 has practical collisions and MD5 is broken — neither is safe for integrity. SHA-512 works but is overkill for fonts and produces longer, less convenient ids. The tool only emits SHA-256; there is no algorithm picker.
Does the hash change between builds?
Only if the font's bytes change. Re-running against a byte-identical file gives an identical hash every time — SHA-256 is deterministic. Re-exporting from a font editor usually flips the hash because editors embed a fresh head.modified timestamp and re-pad tables; that drift is useful signal, not noise. If you need stable hashes across builds, fingerprint the canonical source binary rather than a re-export.
How is the SRI attribute formatted?
integrity="sha256-<base64>", where <base64> is the base64 of the same raw 32-byte digest the hex shows. Put it on <link rel="preload" as="font" crossorigin> (SRI requires crossorigin for fonts). The browser hashes the downloaded file and fails the load if it doesn't match — tamper-resistant delivery, especially for fonts served from a third-party CDN.
Are the hex hash and the SRI base64 two different hashes?
No — one hash, two encodings. sha256 is the digest as lowercase hex; the base64 inside sri_attribute is the identical bytes encoded base64. Decode the base64 to bytes and hex-encode them and you get the sha256 string back. They never disagree because they come from the same crypto.subtle.digest result.
What exactly gets hashed — the font or some normalised form?
The raw file bytes, exactly as dropped. The tool does not parse, decode, re-compress, or normalise the font before hashing. A WOFF2 is hashed in its compressed form; a TTF in its raw sfnt form. That is why the result matches sha256sum / shasum -a 256 on the same file on your machine.
Does it work on TTF, OTF, WOFF, and WOFF2?
Yes — all four are accepted (by extension) and hashed identically, since hashing is format-agnostic. Note the same logical font in two formats produces two different hashes because the byte streams differ. To compare across formats, convert both to one format first using a converter like ttf-to-woff2 or woff2-to-ttf, then fingerprint.
Is there a size limit?
Per file: 5 MB on free, 50 MB on Pro, 1 GB on Developer. Oversize files are blocked client-side before any hashing — nothing is uploaded. SHA-256 is fast: even a 5 MB CJK WOFF2 hashes in tens of milliseconds because the digest is hardware-accelerated. If your source font exceeds the cap, subset it first with font-subsetter.
Can I fingerprint several fonts at once?
Not in this tool — it is single-file (multiFile: false), so you process one font per run. For a whole design system, script it: the algorithm is just crypto.createHash('sha256') (Node) or shasum -a 256 (shell) over each file. The build-pipeline guide walks through batching it in CI.
Where does the short_hash come from and is 8 chars enough?
short_hash is literally the first 8 hex characters of the full sha256. Eight hex chars is 32 bits — fine for human-readable filename suffixes and version labels within a single project (collision odds are negligible across a handful of font files). For cryptographic integrity always use the full sha256 or the SRI attribute; the short form is a convenience id, not a security primitive.
Why does the metric chip show only 16 characters with a '…'?
The SHA-256 (full) chip is a glanceable preview — it shows the first 16 hex characters plus … so the badge stays compact. The complete 64-character hash is in the JSON body under the sha256 key. Copy or download the JSON to get the full value; don't transcribe from the chip.
Are my fonts uploaded anywhere?
No. crypto.subtle.digest runs in your browser over an ArrayBuffer read locally via the File API. The result panel shows a 0 bytes uploaded badge. On paid tiers with the @jadapps/runner paired, hashing can route to the local runner on 127.0.0.1 — still on your machine, never JAD's servers. Safe for unreleased or licensed brand fonts.
Can I run this in CI instead of the browser?
Yes. GET /api/v1/tools/font-fingerprinter returns the (empty) option schema; pair the @jadapps/runner once and POST the font to 127.0.0.1:9789/v1/tools/font-fingerprinter/run. Or skip the tool entirely and call crypto.createHash('sha256') in a Node prebuild — the output is identical because both hash the same raw bytes. The build-script guide has a ready-made version.
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.