How to woff & woff2 internals: the spec, byte by byte
- Step 1Read the 4-byte signature — The first four bytes identify the wrapper: `77 4F 46 46` ('wOFF') for WOFF 1.0, `77 4F 46 32` ('wOF2') for WOFF2. The next field, `flavor`, carries the original SFNT version (`0x00010000` for TrueType outlines, `OTTO` for CFF).
- Step 2Parse the header — Both headers record `numTables`, `totalSfntSize` (the uncompressed size to reconstruct), and metadata/private-block offsets. WOFF2 adds one extra field — `totalCompressedSize` — because its tables share a single compressed stream.
- Step 3Walk the table directory — WOFF 1.0 lists each table with explicit `offset`, `compLength`, `origLength`, and `origChecksum`. WOFF2 drops the offsets entirely: tables are stored consecutively in directory order, with sizes encoded as variable-length `UIntBase128` and a flags byte naming known tags.
- Step 4Decompress and reconstruct — WOFF 1.0 inflates each table's zlib stream independently. WOFF2 Brotli-decodes the whole block, then reverses the `glyf`/`loca` transform to rebuild the SFNT. The result is byte-equivalent to the original font's tables.
WOFF 1.0 header (W3C WOFF)
All multi-byte fields are big-endian. Total header size is 44 bytes.
| Field | Type | Meaning |
|---|---|---|
signature | UInt32 | 0x774F4646 ('wOFF') — must match exactly |
flavor | UInt32 | Original SFNT version (0x00010000 TrueType, OTTO CFF) |
length | UInt32 | Total WOFF file size in bytes |
numTables | UInt16 | Number of font tables |
reserved | UInt16 | Must be 0 |
totalSfntSize | UInt32 | Size of the uncompressed font once reconstructed |
majorVersion / minorVersion | UInt16 ×2 | WOFF file version |
metaOffset / metaLength / metaOrigLength | UInt32 ×3 | Optional XML metadata block (compressed + original size) |
privOffset / privLength | UInt32 ×2 | Optional private data block |
WOFF 2.0 header (W3C WOFF2)
Same shape as WOFF 1.0 plus one field — totalCompressedSize — for the shared Brotli stream.
| Field | Type | Meaning |
|---|---|---|
signature | UInt32 | 0x774F4632 ('wOF2') |
flavor | UInt32 | Original SFNT version |
length | UInt32 | Total WOFF2 file size |
numTables | UInt16 | Number of font tables |
reserved | UInt16 | Must be 0 |
totalSfntSize | UInt32 | Uncompressed reconstructed size |
totalCompressedSize | UInt32 | New in WOFF2 — length of the Brotli-compressed data block |
majorVersion / minorVersion | UInt16 ×2 | WOFF2 file version |
metaOffset / metaLength / metaOrigLength | UInt32 ×3 | Optional metadata block |
privOffset / privLength | UInt32 ×2 | Optional private data block |
Table directory: WOFF 1.0 vs WOFF 2.0
| Aspect | WOFF 1.0 entry | WOFF 2.0 entry |
|---|---|---|
| Tag | tag (UInt32, 4 bytes) | 5-bit known-tag index in a flags byte (or full tag if 63) |
| Sizes | compLength + origLength (UInt32) | origLength (and transformLength) as UIntBase128 |
| Offset | Explicit offset (UInt32) | None — tables stored consecutively in order |
| Checksum | origChecksum (UInt32) | Not stored per-table |
| Transform | None | Flags byte selects glyf/loca transform or null transform |
WOFF vs WOFF2 vs raw SFNT
| Property | TTF/OTF (SFNT) | WOFF 1.0 | WOFF 2.0 |
|---|---|---|---|
| Compression | None | zlib/DEFLATE per table | Brotli over whole font |
| Magic | 0x00010000 / OTTO | wOFF | wOF2 |
| Table transforms | — | No | Yes (glyf/loca) |
| Typical web size | Largest | ~40% smaller than TTF | ~50% smaller than TTF (~30% smaller than WOFF) |
| Browser support | Old / desktop | IE9+ and all modern | All modern (no IE) |
Cookbook
Hands-on recipes: read the magic bytes yourself, ship the right @font-face sources, and convert between formats.
Identify the format from the first 4 bytes
ExampleThe wrapper is unambiguous from byte 0. xxd (or the Font Format Identifier) shows it instantly.
$ xxd -l 4 font.woff2 00000000: 774f 4632 wOF2 $ xxd -l 4 font.woff 00000000: 774f 4646 wOFF $ xxd -l 4 font.ttf 00000000: 0001 0000 .... # TrueType SFNT
Modern @font-face: WOFF2 first, WOFF fallback
ExampleBrowsers pick the first format they support. List WOFF2 first; add WOFF only if you must support very old engines.
@font-face {
font-family: "Inter";
font-weight: 400;
font-display: swap;
src: url("/fonts/inter.woff2") format("woff2"),
url("/fonts/inter.woff") format("woff");
}Convert TTF/OTF to WOFF2 for production
ExampleWOFF2 is the right shipping format for the web. The TTF to WOFF2 tool applies the same Brotli + glyf transform the spec defines; WOFF2 to TTF reverses it for desktop editing.
Drop a .ttf/.otf into /font-tools/ttf-to-woff2 → download .woff2 (Brotli-compressed, glyf/loca transform applied, byte-equivalent tables)
Inspect the SFNT version (flavor) field
ExampleBytes 4–7 reveal whether the wrapped font is TrueType-outline or CFF — useful when a 'font won't load' bug is really a flavor mismatch.
$ xxd -s 4 -l 4 font.woff 00000004: 0001 0000 .... # TrueType outlines # 4f54 544f = 'OTTO' → CFF/PostScript outlines
Benchmarks
Numbers we measured for this page. Every byte count below comes from a real Chrome User-Agent request to fonts.googleapis.com on , with the response WOFF2 fetched and counted directly.
Typical transfer sizes for the same font
A representative Latin text font; exact numbers vary by glyph count and outlines.
| Format | Relative size | Notes |
|---|---|---|
| TTF (SFNT) | 100% | Uncompressed baseline |
| WOFF 1.0 | ~60% | Per-table zlib |
| WOFF 2.0 | ~50% | Brotli + glyf/loca transform |
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.
The first 4 bytes are not `wOFF` or `wOF2`
errorIt is not a WOFF file. 0x00010000 is raw TrueType, OTTO is CFF/OTF, ttcf is a font collection. A decoder must reject a bad signature rather than guess.
A WOFF 1.0 table would compress larger than the original
okThe spec stores that table uncompressed (its compLength equals origLength). This is normal for tiny or already-dense tables — not an error.
You serve WOFF2 to IE11
warnIE11 cannot decode WOFF2 and ignores that src entry. Provide a WOFF fallback in the same @font-face if legacy support matters.
WOFF2 of a variable font
okFully supported — WOFF2 wraps the SFNT including fvar/gvar. The transform applies to glyf/loca; variation tables ride along in the Brotli stream.
The optional metadata block is present
okWOFF can carry an XML metadata block (licence, vendor) and a private block. Both are optional and do not affect rendering; some converters drop them.
`totalSfntSize` does not match the reconstructed font
errorThe file is corrupt or was tampered with — a conformant decoder validates the reconstructed size against this header field and fails closed.
Round-tripping WOFF2 → TTF → WOFF2
okLossless for the table data: the outlines and metrics are byte-equivalent. Only the optional metadata/private blocks may differ if a tool omits them.
Frequently asked questions
What's the difference between WOFF and WOFF2?
WOFF 1.0 compresses each font table separately with zlib/DEFLATE. WOFF2 compresses the whole font as a single Brotli stream and adds a glyf/loca transform, making it about 30% smaller than WOFF on average. Both unwrap to the same SFNT the browser renders.
What are the WOFF and WOFF2 magic numbers?
WOFF 1.0 starts with 0x774F4646 — the ASCII bytes 'wOFF'. WOFF2 starts with 0x774F4632 — 'wOF2'. They are the first four bytes of the file, so you can identify the format without parsing anything else.
Is WOFF2 a different font format from TTF?
No. WOFF2 is a compression and packaging wrapper around the same SFNT container TTF and OTF use. After decompression and reversing the transform, the browser holds an in-memory SFNT equivalent to the original — the glyph outlines and tables are unchanged.
What is the glyf/loca transform in WOFF2?
Before Brotli runs, WOFF2 reorganises the glyf (outlines) and loca (offsets) tables into a form with more redundancy — splitting coordinate streams and recomputing offsets — which Brotli then compresses far more effectively. The decoder reverses it to reconstruct the exact original tables.
Why does WOFF2 drop the per-table offset field?
Because WOFF2 stores tables consecutively in directory order inside one compressed stream, their positions are implied — there is nothing to point at until decompression. Removing the explicit offsets (and using UIntBase128 for sizes) shrinks the directory itself.
Is WOFF2 supported everywhere?
WOFF2 works in every modern browser — Chrome, Edge, Firefox, Safari, and mobile equivalents. The only notable exception is Internet Explorer 11, which needs a WOFF 1.0 fallback. For the vast majority of sites, WOFF2-only is now fine.
Do I still need a WOFF fallback alongside WOFF2?
Only if your analytics show meaningful IE11 (or very old Android) traffic. Otherwise WOFF2-only saves a request and bytes. When in doubt, list WOFF2 first and WOFF second — browsers use the first format they understand. See when to ship both.
Can I convert TTF directly to WOFF2?
Yes. The TTF to WOFF2 converter applies the Brotli compression and glyf/loca transform the spec defines; the result is a standards-compliant WOFF2 you can ship immediately. Use WOFF2 to TTF to go back for desktop editing.
Is the conversion lossless?
For the font's table data, yes — outlines, metrics, and layout tables are byte-equivalent after a round trip. The only things that can change are the optional metadata and private blocks, which some tools omit.
What compression does WOFF2 actually use?
Brotli, the algorithm defined in RFC 7932 (also developed by Google for the web). It generally beats zlib/DEFLATE — which WOFF 1.0 uses — particularly on the structured, repetitive data inside font tables.
Does WOFF2 help more for some fonts than others?
Yes. The savings are largest for fonts with many glyphs and complex outlines — CJK fonts especially — because the glyf transform and Brotli have more redundancy to exploit. Small Latin fonts still benefit but by a smaller margin. See when each format wins for the trade-offs.
How can I see a font's format and structure myself?
Read the first bytes with xxd as shown above, or run the file through the Font Format Identifier for a one-click answer, and the Font Metadata Extractor to dump the name and version tables.
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.