Every CJK font Windows ships in the box — msgothic.ttc, meiryo.ttc, simsun.ttc, msjh.ttc — is a TrueType Collection. OxiFont’s subsetter refused to open a single one of them until today.
Today we released OxiFont 0.2.2 — adding oxifont-subset::instance() to static-instance a variable font at a pinned design location, a drop_variations subset option, TrueType Collection (.ttc) face-index support across oxifont-core and oxifont-subset, a public SubsetGidMap old↔new glyph-ID mapping for PDF CIDFont embedding, and full GSUB/GPOS contextual-lookup remapping in all three lookup formats — plus a .notdef-retention correctness fix, a DSIG stripping fix, and an sfnt-magic fix for CFF-flavoured subsets.
No FreeType. No fontconfig. No hand-rolled variable-font math trusting a gvar tuple stream it hasn’t bounds-checked. oxifont-subset runs entirely under #![forbid(unsafe_code)] — the same guarantee oxifont-core already carries — and the new instancing pipeline bounds every allocation by an already-validated length, because it parses attacker-supplied web-font bytes on some call paths. It compiles to a single static binary and runs anywhere Rust does — no C toolchain, no -sys crate, no build script vendoring a copy of FreeType.
Why OxiFont 0.2.2 is a game changer
A font subsetter that’s correct on a plain static TTF but stops there still leaves real fonts unusable:
- Every variable font a subsetter touches either stays variable — carrying
fvar/avar/gvar/HVAR/VVAR/STATmachinery nobody asked to keep, measured at 2.6× the program size on a stock two-axis UI font — or gets flattened by hand outside the library, with no guarantee the coordinate math matches the spec. - Windows’ own CJK system fonts —
msgothic.ttc,meiryo.ttc,YuGothM.ttc,msyh.ttc,msjh.ttc,simsun.ttc— ship only as TrueType Collections. A subsetter that can’t parse thettcfcontainer simply can’t touch the fonts most Windows deployments actually have installed. - A PDF CIDFont needs the subset’s old→new glyph-ID renumbering to emit correct CIDs (
Identity-H+/CIDToGIDMap /Identity). Without that map coming back from the subsetter, nothing downstream can build a CID font from an OxiFont subset — which is exactly what the crate’s ownpdf_subsetmodule exists for. - Advanced GSUB/GPOS contextual lookups (formats 1/2/3 — glyph rule sets, class rule sets, and per-position coverages) were dropped from every subset instead of remapped, silently degrading shaping for scripts that lean on them.
- A
.notdef-numbering inconsistency meant the same requested glyph set produced two different glyph 0 assignments depending on which entry point you called — and a PDF CIDFont built on the wrong one silently promoted the lowest requested glyph to CID 0.
OxiFont 0.2.2 ends all of that:
instance()static-instances a variable face at one pinned design location —(tag, value)coordinates infvaruser units, clamped to each axis’s range, glyph IDs untouched socmap/GSUB/GPOS/GDEF/kern/COLR/MATH/sbixcarry over verbatim into the ordinary subsetting entry points.SubsetOptions::drop_variationsemits a genuinely static subset — the flag is only meaningful afterinstance()has pinned the location you actually want; called alone it drops the variation tables but keeps the default master.- TrueType Collections are now first-class.
face_count, the_at_faceentry points, andPdfFontSubsetter::new_at_facelet you pick the right face out ofmsgothic.ttcor any other.ttcinstead of failing on the container magic. SubsetGidMapis public, with_mappedvariants on every subsetting entry point returning it as a third tuple element — the exact old↔new renumbering a PDF CIDFont needs, without changing the bytes or stats the non-mapped siblings already produce.- GSUB 5/6/8 and GPOS 3/5/7/8 are remapped in all three formats, Extension-wrapped or not, instead of being dropped — a new
SubsetStats::dropped_context_subtablescounter reports only the subtables that are genuinely malformed or unmatchable. .notdefretention is now consistent across every entry point,DSIGis never copied (a subset’s signature is invalid over rewritten bytes by construction), and CFF-flavoured subsets stamp theOTTOsfnt magic instead of lying about being TrueType.
Technical Deep Dive: instancing, collections, and the CID map
- Static instancing (
oxifont-subset::instance, five sub-modules:coords.rs,tuples.rs,ivs.rs,metrics.rs,outline.rs). The whole coordinate pipeline runs in 16.16 fixed point with FreeType’s rounded division and converts to F2Dot14 once at the end; outlines come from a fullgvartuple walk (both offset formats, shared and embedded peak tuples, intermediate regions, packed point numbers, packed deltas,IUPagainst the default outline), and advances/side bearings are rebuilt from the four phantom points rather than inherited, so even empty glyphs with varying advances are covered. - TrueType Collection support (
oxifont-core::sfnt).TTC_MAGIC,face_count,face_offset, andSfntTableMap::parse_facevalidate the collection header — major version, non-zeronumFonts, an offset table that actually fits — before trusting any offset in it, then validate the SFNT header at the selected offset exactly as offset 0 is validated. An out-of-range index isSfntError::FaceIndexOutOfRange, never a panic. - The glyph-ID map (
oxifont-subset::gid_map).SubsetGidMaprenumbers retained glyphs densely from 0 in ascending old-GID order after composite-component closure —new_gid,old_gid,contains_old_gid,new_to_old, anditer()expose the mappingPdfFontSubsetter::finalize_mappedneeds to back a CID font. - Contextual-lookup rewriting (
otl_context). Parsed contextual subtables stay in an intermediate form until the final old→new lookup-index map is known, so everyseqLookupRecordis written with its renumberedlookupListIndex— records whose target lookup was dropped are pruned, so there is no longer any path that can emit a stale index.
Getting Started
cargo add oxifont --features subset,bundled-noto
Pin a variable font to a specific weight, then subset the pinned instance to a static, variation-free font:
use std::collections::BTreeSet;
use oxifont::subset::{instance, subset_font_with_options, SubsetError, SubsetOptions};
fn build_static_bold_subset(variable_font_bytes: &[u8]) -> Result<Vec<u8>, SubsetError> {
// Pin the variable font to wght=700 at face 0 -- glyph IDs are untouched,
// so cmap/GSUB/GPOS/GDEF/kern/COLR/MATH/sbix all carry over verbatim.
let pinned = instance(variable_font_bytes, 0, &[(*b"wght", 700.0)])?;
// Now subset the pinned instance and drop the vestigial variation tables.
let codepoints: BTreeSet<char> = "Hello, OxiFont!".chars().collect();
let opts = SubsetOptions::default().drop_variations(true);
let (subsetted, _stats) = subset_font_with_options(&pinned, &codepoints, &opts)?;
Ok(subsetted)
}
Subset one face out of a Windows .ttc collection:
use std::collections::BTreeSet;
use oxifont::subset::{face_count, subset_font_at_face, SubsetError};
fn subset_msgothic(ttc_bytes: &[u8]) -> Result<Vec<u8>, SubsetError> {
let faces = face_count(ttc_bytes)?; // msgothic.ttc reports more than one face
println!("{faces} face(s) in this collection");
let codepoints: BTreeSet<char> = "こんにちは".chars().collect();
subset_font_at_face(ttc_bytes, 0, &codepoints) // face 0 == MS Gothic
}
crates/oxifont/examples/ also gained four runnable walkthroughs this release — discover_query_match (default features), parse_metrics_outline, subset_woff2_roundtrip, and hinting_at_ppem (the latter two need bundled-noto plus their respective feature):
cargo run -p oxifont --example subset_woff2_roundtrip --features subset,woff2,bundled-noto
cargo run -p oxifont --example hinting_at_ppem --features hinting,bundled-noto
What’s New in 0.2.2
- Added:
oxifont-subset::instance()for static-instancing a variable font at a pinnedfvardesign location;SubsetOptions::drop_variations(the struct is now#[non_exhaustive]); TrueType Collection face-index support inoxifont-core(face_count,face_offset,SfntTableMap::parse_face) andoxifont-subset(face_count, the_at_faceentry points,PdfFontSubsetter::new_at_face); a publicSubsetGidMapplus_mappedvariants on every subsetting entry point;SubsetStats::cff_charstrings_verbatimandSubsetStats::dropped_context_subtables; anoxifontfacadehintingfeature re-exportingoxifont-hinting, plus anoxifont::hinted_outline()convenience wrapper; twooxifont-hintingfuzz targets; anoxifont-adapter-nativedbfeature bridging intooxifont_db::FontDatabase; four runnableoxifontexamples; workspace-rootrustfmt.toml/clippy.toml. - Changed (breaking):
PdfSubsetResultgained agid_map: SubsetGidMapfield;BundledFont::parsed_face()no longer panics on a decompression/parse failure — it now returns and caches aResult. GSUB/GPOS contextual lookups (formats 1/2/3, including Extension-wrapped and GSUB 8/GPOS 3/GPOS 5) are now remapped instead of dropped. - Fixed:
.notdefis now retained consistently by every subsetting entry point;DSIGis never copied into a subset; CFF-flavoured subsets get theOTTOsfnt magic instead of the TrueType one; the offset-tablesearchRange/entrySelector/rangeShiftfields now follow the correct OpenType formulas; a DirectWrite compile-blocking missing import; several workspace-wide clippy lint fixes on Windows. - Dependency bumps:
oxiarc-deflate/oxiarc-brotli0.4.0 → 0.4.1,quick-xml(oxixml-quickxml-compat) 0.1.0 → 0.1.1,oxicode0.2.5 → 0.2.6. - 1165 tests passing, 0 failed with
--all-features(1102 passing under default features, 23#[ignore]d for Windows-only/external-fixture cases), plus 123 doc tests; 11 crates in the workspace, ~23,200 Rust SLOC undersrc/(~43,400 including test code).
Tips
- Instance before you drop variations, not instead of it.
SubsetOptions::drop_variations(true)alone just discards the variation tables and keeps the default master outline — callinstance()first if you actually need a different pinned location than the font’s default. - An axis tag that doesn’t exist on the font is now a hard error, not a silent no-op.
instance()returnsSubsetError::UnknownAxisfor a typo’d tag instead of quietly falling back to the default weight while reporting success — check the font’s realfvaraxis tags before pinning. - Reach for
_at_faceentry points on any.ttc. The historical offset-0 entry points deliberately keep refusing a collection rather than guessing which face you meant — callface_countfirst, thensubset_font_at_face/subset_font_with_options_at_face/PdfFontSubsetter::new_at_facewith the index you want. - If you’re building a PDF CIDFont, switch to the
_mappedentry points.subset_font_with_options_mapped,subset_by_gids_mapped, andPdfFontSubsetter::finalize_mappedreturn aSubsetGidMapalongside the same bytes and stats — that’s the old↔new renumberingIdentity-H+/CIDToGIDMap /Identityembedding needs. - Update your
PdfSubsetResultconstruction/destructuring for the new field. The struct gainedgid_map: SubsetGidMap; code that only reads fields is unaffected, but a struct literal or exhaustive destructure needs..or the new field. BundledFont::parsed_face()no longer panics — handle theResult. A decompression/parse failure now returns (and caches)Err(FontError)instead of unwinding the caller’s stack; the bundled Latin/CJK constants this crate ships are unaffected since their bytes are always valid.
This is the foundation
OxiFont is the font foundation under OxiText — which pulls in oxifont, oxifont-core, oxifont-parser, oxifont-bundled (with the bundled-noto feature), oxifont-subset, and oxifont-adapter-native for glyph metrics and layout — oxigaf (PDF CFF/Type-0 font embedding, the direct beneficiary of this release’s SubsetGidMap), oximedia (subtitle and OSD rendering), oxigdal-symbology (map labels), oxiphoton (image text overlay), and OxiUI (GUI text rendering, via oxifont). Outside the COOLJAPAN workspace, the tiktok/kitasan-shorts project depends on oxifont directly. None of them need a code change to pick up this release beyond a version bump — except a caller that exhaustively destructures or struct-literal-constructs PdfSubsetResult (it gained the gid_map field) or builds SubsetOptions from a struct literal instead of SubsetOptions::default() plus the builder methods (it is now #[non_exhaustive]).
Repository: https://github.com/cool-japan/oxifont
Star the repo if you want a subsetter that can actually open the CJK fonts already sitting on the Windows machines your users run.
The era of a Pure Rust font stack that can parse a .ttc header but not subset a single face out of it is over. Pure Rust typography — sovereign, safe, and FFI-free.
— KitaSan at COOLJAPAN OÜ August 6, 2026