glyph_path 1.0.0
glyph_path: ^1.0.0 copied to clipboard
A pure Dart OpenType/TrueType parser and text-to-path converter.
Changelog #
All notable changes to this project will be documented in this file.
This project adheres to Semantic Versioning.
1.0.0 #
Added #
Core parsing
- TrueType glyph parsing —
glyf/locatable support for.ttffiles - CFF/OpenType glyph parsing — compact font format outline support for
.otffiles, including flex drawing operators (hflex,flex,hflex1,flex1) - CFF
hhcurveto/vvcurveto— multi-group sequences with a leading delta value are correctly rendered - cmap format 4 & 12 — Unicode character-to-glyph mapping with priority-based subtable selection
- OS/2 table parsing — typographic metrics (
sTypoAscender,sTypoDescender,sTypoLineGap) resolved from theUSE_TYPO_METRICSflag when set, falling back tohheavalues; exposed asFont.ascender,Font.descender, andFont.lineGap advanceWidthForGlyphout-of-range fallback — glyph IDs beyondnumberOfHMetrics, and negative glyph IDs, fall back to the.notdefadvance width per the OpenType spec instead of throwingRangeError- GSUB ligature set isolation — each glyph's ligature rule set is independently allocated, preventing cross-glyph interference
- GPOS PairPos Format 2
cellSize = 0— subtables where both value formats are empty are correctly skipped
Layout & measurement
TextMeasurementvalue class — immutable result ofmeasureText()withwidth,ascent, anddescent(all in scaled pixels at the requested font size)measureText— returns aTextMeasurementwithout generating path dataFont.metrics— returns aFontMetrics(unitsPerEm,ascender,descender) without generating paths- Kerning —
kerntable (format 0) and GPOS pair-adjustment support - Ligature substitution — GSUB
ligafeature (e.g.fi→ fi-ligature) - Multi-line layout with word-wrap —
TextLayoutwraps text at a configurablemaxWidth; word-wrap decisions use full-string kerning measurement - Text alignment —
TextAlign.left,TextAlign.center, andTextAlign.right TextLayoutmetric scaling —ascenderanddescenderare scaled to pixel units at the requested font size
Path data
- Sealed
PathCommandhierarchy —MoveTo,LineTo,QuadTo,CubicTo,ClosePath normalizeToCubic— converts allQuadTotoCubicTofor uniform TTF/OTF output; enables cross-format glyph morphing- Per-glyph access —
generateGlyphPaths()returns oneGlyphPathResultper rendered glyph with glyph-localcommands, tightbounds(computed from Bézier curve extrema),advanceWidth,positionX,contours, andsourceText Font.generateRawGlyphPaths— returnsTextPathResultin design-unit coordinates (scale = 1.0); intended for layout engines and morphing workflows- Contour decomposition —
GlyphPathResult.contoursis aList<Contour>where eachContourholds itscommands,WindingDirection, andsignedArea(shoelace / 2) splitIntoContours— top-level function that splits anyList<PathCommand>intoList<Contour>, supporting both closed and open sub-paths
Contour manipulation
Contour.flipY()— returns a newContourwith Y coordinates negated andwindingflipped; use instead ofPathCommandListTransform.flipY()to keep winding and signed area consistentContour.reversed()— returns a newContourtracing the same closed outline in the opposite direction; swapsCubicTocontrol points and flipswindingContour.rotated(int shift)— shifts the start vertex of a closed outline byshifton-curve vertices without changing shape or windingPathCommandListTransform— extension onList<PathCommand>exposingtranslate(dx, dy),scale(factor), andflipY()
Export
- SVG path data —
TextPathResult.toSvgPathData()returns an SVGdattribute string - PDF content stream —
TextPathResult.toPdfContentStream()emits PDF path operators (PDF 1.7 §8.5.2); quadratic curves are automatically elevated to cubic - JSON —
TextPathResult.toJson()serialises all commands toList<Map<String, dynamic>> - Flutter
ui.Path—TextPathResult.toUiPath()via conditional export; stub provided for Pure Dart environments
Caching
- Dual-cache with independent size controls — glyph-outline cache (
maxGlyphCacheSize, default 256) and shape cache (maxShapeCacheSize, default 256) backed bycacherinewith LRU eviction; configured viaFont.parse(maxGlyphCacheSize: N, maxShapeCacheSize: M)
Examples
- Six runnable examples covering basic usage, SVG, PDF, multi-line layout, JSON, and Flutter Canvas integration
Fuzz testing
test/fuzz/— self-contained random-mutation fuzz harness with no external dependencies; targets the whole-font entry point (Font.parse), twelve isolated table parsers (cff, cmap, glyf, gpos, gsub, head, hhea, hmtx, kern, loca, maxp, os2), and the text-shaping API (TextLayout.layout)test/fuzz/helpers.dart— sharedrunFuzzTargethelper withStopwatch-bounded loop,FontParseException-swallow policy, and automatic crash reproducer savingtest/fuzz/corpus/— seed corpus derived from existing binary test fixtures; crash reproducers are written here and re-checked on every run.github/workflows/fuzz.yml— CI job running all fuzz targets (60 s on PR/push, 600 s on scheduled runs); uploads the corpus as an artifact on failure
Security #
Font.parse()preflight — validates the sfnt magic bytes and all required tables (head,hhea,maxp,cmap,hmtx) before any further parsing; unsupported sfnt versions are rejected immediatelyBinaryReader.slice()— every table parser receives a reader scoped to the table's declared byte range, making cross-table reads structurally impossibleFontParseException— structural errors encountered while parsing the font binary itself (missing/truncated tables, out-of-range offsets, malformed structures) are raised as a single typed exception with a descriptive message and optional byte offset. This is distinct from invalid caller input (oversizedtext, non-finite/out-of-rangefontSize, non-positive cache sizes), which raisesArgumentErrordirectly and is never converted toFontParseException— see SECURITY.md for the full distinctionResourceLimits— abstract class ofstatic consthard caps enforced throughout parsing and text shaping:maxTables(64),maxGlyphs(65535)maxCompositeDepth(8),maxCompositeComponents(64) — composite glyph nesting and component countmaxCompositeOutputCommands(65535),maxPathCommandsPerGlyph(131072) — composite glyph output size, per level and cumulativemaxContoursPerGlyph(256),maxPointsPerGlyph(32768) — TrueType outline geometrymaxCmapIterations(65536) — format 4 segment expansionmaxCmapGroups(10000),maxCmapFormat12Span(1114112) — format 12 group count and cumulative code-point spanmaxKernPairs(65535),maxGposPairs(65535),maxGposMatrixCells(65535),maxGposFeatureLookupRefs(65535) — kern and GPOS pair/feature datamaxCoverageGlyphs(65535),maxClassDefEntries(65535) — GPOS Coverage/ClassDef expansionmaxLigaRules(10000),maxLigatureComponentCount(16),maxLigatureEvalOps(1000000) — GSUB ligature table size and per-call evaluation budgetmaxCoverageRangeRecords(20000),maxGsubCoverageScanSteps(1000000),maxGsubLigSetVisits(1000000),maxGsubSubtablesPerLookup(64),maxGsubFeatureLookupRefs(65535) — GSUB coverage-scan, LigatureSet-visit, and lookup/feature-reference budgetsmaxCffSubrDepth(10),maxCffTotalSubrCalls(10000),maxCffStackDepth(513),maxCffStemCount(96),maxCffOutputCommands(32768) — CFF Type 2 subroutine nesting/fanout, operand stack, and output sizemaxInputTextLength(4096 UTF-16 code units),maxFontSize(1e6) — public API argument validation; both throwArgumentErrorbefore any parsing or glyph-resolution work begins
- Composite glyph cycle detection —
Fontdetects circular component references during composite glyph resolution and throwsFontParseException; previously a crafted font could cause infinite recursion - cmap format 4 DoS guard —
segCountX2validated to be even;lengthchecked against the minimum size for the four segment arrays before any read; cumulative iteration count bounded bymaxCmapIterations - cmap format 12 DoS guard — group count bounded by
maxCmapGroupsbefore allocation; cumulative code-point span bounded bymaxCmapFormat12Span(prevents CPU exhaustion from overlapping groups); total character map entries bounded bymaxGlyphs - CFF INDEX validation —
offSizevalidated to be in [1, 4] before reading any INDEX data; offsets using the invalid 1-based encoding are rejected - CFF FDSelect Format 3 sentinel — range boundaries are validated for strict monotonic order; a non-monotonic sentinel triggers
FontParseException - CFF FDSelect Format 0 bounds check — the per-glyph FD-index byte count is validated against the remaining buffer before reading, raising
FontParseExceptiondirectly instead of a rawRangeError - CFF charstring interpreter — multi-byte number encodings (byte 28, byte 255, bytes 247–254) checked for sufficient remaining bytes; byte position verified to advance each iteration (stalled iteration →
FontParseException); subroutine call depth capped atmaxCffSubrDepth; operand stack depth capped atmaxCffStackDepth - GPOS Non-Fatal boundary — all GPOS parse errors are caught and swallowed; GPOS kerning is silently disabled on malformed data rather than aborting font load
- GSUB Non-Fatal boundary — GSUB parse failures disable ligatures without propagating to the caller;
FontParseException,RangeError, and internalArgumentErrors raised while parsing the table's own bytes are all caught (mirrors the GPOS Non-Fatal boundary) — unrelated to the public-API-boundaryArgumentError(see theFontParseExceptionentry above), which is never caught or converted. This covers every limit checked insideparseGsub()itself;maxLigatureEvalOpsis checked later, once per text-shaping call, and itsFontParseExceptionis not swallowed — see SECURITY.md - GSUB LigatureSet-visit budget — cumulative LigatureSet visits across all
ligalookups/subtables in a single parse are bounded bymaxGsubLigSetVisits, independent ofmaxGsubCoverageScanSteps; without it, a Coverage Format 1 table makes each visit O(1), so a font of only a few tens of kilobytes could reuse one physical subtable across every offset entry to declare hundreds of billions of visits - kern table bounds — at least 4 bytes required before reading the table header; each subtable requires at least 6 bytes; subtables with
length < 6are rejected; cumulative pair count bounded bymaxKernPairs - kern Apple-format header detection — a table version of
0x00010000(Apple's Fixed-point kern version, laid out as uint32 version + uint32 nTables) is detected explicitly and treated as unsupported (no kerning), rather than relying on the incidental fact that reinterpreting its header as the Microsoft/OpenType uint16+uint16 layout happens to yieldnTables = 0 - loca offset validation — every offset is checked to fall within the
glyftable's byte range; descending offsets (negative glyph size) are rejected - glyf SimpleGlyph flag consistency — flag array length and on-curve point consistency validated during outline parsing
- Dependency audit —
cacherine 2.4.0and its transitive dependencysynchronized 3.4.0+1have no known advisories as of 2026-06-24; see SECURITY.md SECURITY.mdadded — vulnerability reporting instructions and full dependency audit record