glyph_path 1.0.0 copy "glyph_path: ^1.0.0" to clipboard
glyph_path: ^1.0.0 copied to clipboard

retracted[pending analysis]

A pure Dart OpenType/TrueType parser and text-to-path converter.

glyph_path #

A Pure Dart package that parses TrueType/OpenType fonts and converts text strings into vector path commands — no Flutter, no native code, no external dependencies.

pub package Dart SDK CI

Preview #

Basic Latin Kerning & Ligatures
Basic Latin text path rendering "Hello, World!" as vector outlines Kerning and ligature substitution applied to "Kerning & fi fl"
CJK / Japanese Multi-line Word-wrap
Japanese text "日本語テキスト" rendered as glyph outlines Three lines of word-wrapped text demonstrating multi-line layout

Preview SVGs are generated by dart run example/generate_preview_svgs.dart.

Features #

Core parsing

  • TrueType (glyf/loca) and CFF/OpenType outline parsing
  • cmap format 4 & 12 — full Unicode and CJK support
  • Typographic metrics resolved from OS/2 USE_TYPO_METRICS flag, falling back to hhea
  • normalizeToCubic — uniform cubic output across TTF and OTF fonts

Layout & measurement

  • Kerning — kern table (format 0) and GPOS pair adjustments
  • Ligature substitution — GSUB liga feature (fi → fi-ligature)
  • Multi-line layout with word-wrap — TextLayout wraps at a configurable maxWidth
  • Text alignment — left, center, right via TextAlign
  • measureText — returns a TextMeasurement (width, ascent, descent) without generating path data
  • Font.metricsFontMetrics (unitsPerEm, ascender, descender) without generating paths
  • Font.lineGap — typographic line gap in design units (from OS/2 or hhea)

Contour tools

  • generateGlyphPaths() — per-glyph GlyphPathResult with commands, tight bounds (Bézier extrema), advance width, and source text
  • GlyphPathResult.contoursList<Contour> with pre-computed WindingDirection and signed area
  • Contour.flipY() — Y-flip that keeps winding and signedArea consistent
  • Contour.reversed() — reverses the tracing direction of a closed outline
  • Contour.rotated(int shift) — rotates the start vertex of a closed outline
  • splitIntoContours — splits any List<PathCommand> into List<Contour>
  • PathCommandListTransformtranslate(), scale(), and flipY() on any command list

Export

  • SVG path data — toSvgPathData() emits an SVG d attribute string
  • PDF content stream — toPdfContentStream() emits PDF path operators (PDF 1.7 §8.5.2)
  • JSON — toJson() serialises all commands to List<Map<String, dynamic>>
  • Flutter ui.PathtoUiPath() via conditional export; Pure Dart stub included

Safety

  • ResourceLimits — hard caps on table count, glyph count, cmap entries, kern pairs, composite depth, CFF subroutine nesting, input text length, and font size (DoS prevention)
  • Scoped BinaryReader.slice() — cross-table reads are structurally impossible
  • FontParseException — font-structure parse errors surface as a single typed exception; invalid API arguments (oversized text, out-of-range font size) raise ArgumentError instead

Installation #

dart pub add glyph_path

Or add to pubspec.yaml manually:

dependencies:
  glyph_path: ^1.0.0

Usage #

Quick start #

import 'dart:io';
import 'dart:typed_data';
import 'package:glyph_path/glyph_path.dart';

void main() {
  // Load font bytes from any source (File, Flutter rootBundle, network, …)
  final Uint8List fontBytes = File('assets/MyFont.ttf').readAsBytesSync();

  // Parse once — expensive; reuse the instance for multiple calls
  final font = Font.parse(fontBytes);

  // Generate path commands for a string at a given font size
  final TextPathResult result = font.generateGlyphPaths('Hello Dart', fontSize: 48.0);

  // Iterate the flattened command sequence across all glyphs
  for (final cmd in result.commands) {
    switch (cmd) {
      case MoveTo(:final x, :final y):
        print('move to ($x, $y)');
      case LineTo(:final x, :final y):
        print('line to ($x, $y)');
      case QuadTo(:final controlX, :final controlY, :final x, :final y):
        print('quad to ($x, $y) via ($controlX, $controlY)');
      case CubicTo(:final control1X, :final control1Y,
                   :final control2X, :final control2Y,
                   :final x, :final y):
        print('cubic to ($x, $y)');
      case ClosePath():
        print('close');
    }
  }
}

Font.parse() throws FontParseException for malformed input or exceeded resource limits. Always wrap it in a try-catch when processing fonts from untrusted sources — and note that generateGlyphPaths, measureText, resolveGlyphIdsForLayout, and TextLayout.layout can also throw FontParseException, even for otherwise ordinary-length text, if the font's GSUB ligature table is crafted to exceed the per-call evaluation budget (ResourceLimits.maxLigatureEvalOps). Wrap those calls too whenever the font itself is not fully trusted:

try {
  final font = Font.parse(untrustedBytes);
  final result = font.generateGlyphPaths('Hello Dart', fontSize: 48.0);
  print('generated ${result.glyphs.length} glyphs');
} on FontParseException catch (e) {
  print('Font rejected: ${e.message}');
}

Text measurement #

measureText returns a TextMeasurement with the advance width and typographic metrics of a string at the requested font size — without generating any path data:

final TextMeasurement m = font.measureText('Hello', fontSize: 48.0);
print('width:   ${m.width}');   // advance width in scaled pixels
print('ascent:  ${m.ascent}');  // positive, above baseline
print('descent: ${m.descent}'); // negative, below baseline

To get typographic metrics for the font itself (without a specific string), use Font.metrics and Font.lineGap:

final FontMetrics metrics = font.metrics; // unitsPerEm, ascender, descender
final double gap     = font.lineGap;      // typographic line gap in design units

Per-glyph path generation #

generateGlyphPaths() decomposes a string into one GlyphPathResult per rendered glyph. Each entry carries drawing commands in glyph-local coordinates (origin at 0, 0), a tight bounding box computed from Bézier curve extrema, advance width, and source text.

"Hi!" → generateGlyphPaths()
        │
        ├─ GlyphPathResult { sourceText: "H", positionX: 0.0,
        │    advanceWidth: 34.6, bounds: GlyphBounds(0, 0, 33, 47),
        │    commands: [MoveTo, LineTo, …] }
        │
        ├─ GlyphPathResult { sourceText: "i", positionX: 34.6,
        │    advanceWidth: 14.2, bounds: GlyphBounds(0, 0, 13, 47),
        │    commands: [MoveTo, LineTo, …] }
        │
        └─ GlyphPathResult { sourceText: "!", positionX: 48.8,
             advanceWidth: 15.1, bounds: GlyphBounds(0, 0, 14, 47),
             commands: [MoveTo, LineTo, …] }

Use positionX to place a glyph's local commands at its absolute X offset in the line. normalizeToCubic (default true) converts all QuadTo to CubicTo — enabling cross-format glyph morphing between TTF and OTF fonts.

final TextPathResult result = font.generateGlyphPaths(
  'Hi!',
  fontSize: 48.0,   // pass null for raw design-unit coordinates (scale = 1.0)
  kerning: true,
  ligatures: true,
);

for (final GlyphPathResult glyph in result.glyphs) {
  print('glyph: "${glyph.sourceText}" at x=${glyph.positionX}');
  print('  advance: ${glyph.advanceWidth}');
  print('  bounds:  minX=${glyph.bounds.minX} maxX=${glyph.bounds.maxX}');
}

fontSize: null for raw font units: Returns coordinates in the font's design-unit space (scale = 1.0). Useful for layout engines, resolution-independent measurement, or glyph morphing before applying a scale at render time. The same result is available via generateRawGlyphPaths().

Contour decomposition

Each GlyphPathResult exposes a contours list where every Contour groups a closed sub-path with its pre-computed WindingDirection and signedArea (shoelace / 2). Use signedArea.abs() to distinguish outer outlines from inner counters (holes) by area:

for (final contour in glyph.contours) {
  print('  winding=${contour.winding}');      // .clockwise / .counterClockwise
  print('  area=${contour.signedArea.abs()}'); // larger = outer outline
}

Multi-line layout #

TextLayout handles explicit \n newlines and optional word-wrap:

final engine = TextLayout(font);

final TextLayoutResult result = engine.layout(
  'The quick brown fox jumps over the lazy dog.',
  fontSize: 36.0,
  maxWidth: 400.0,
  alignment: TextAlign.center,
);

for (final line in result.lines) {
  print('baseline y=${line.baselineY}, width=${line.lineWidth}');

  // Per-glyph access in glyph-local coordinates:
  // absolute x = glyph.positionX + line.alignmentOffset
  for (final glyph in line.glyphs) {
    final double absoluteX = glyph.positionX + line.alignmentOffset;
    print('  "${glyph.sourceText}" at x=$absoluteX');
  }
}

Available alignment values: TextAlign.left, TextAlign.center, TextAlign.right.

Path manipulation #

PathCommandListTransform provides geometric transforms on any List<PathCommand>:

final commands = glyph.commands;
final shifted  = commands.translate(10.0, -5.0); // shift by (dx, dy)
final scaled   = commands.scale(2.0);            // uniform scale
final flipped  = commands.flipY();               // negate all Y (font→screen)

When working with Contour objects, prefer the contour-aware variants so that winding and signedArea stay consistent with the transformed coordinates:

final flippedContour   = contour.flipY();          // flips winding too
final reversedContour  = contour.reversed();       // reverses tracing direction
final rotatedContour   = contour.rotated(1);       // shifts start vertex by 1

splitIntoContours splits any List<PathCommand> into List<Contour>, including open sub-paths (flushed at the next MoveTo or end of list):

final List<Contour> contours = splitIntoContours(<PathCommand>[
  MoveTo(0, 0), LineTo(100, 0), LineTo(50, 100), ClosePath(), // closed
  MoveTo(200, 0), LineTo(300, 0),                              // open
]);

Exporters #

SVG

final result = font.generateGlyphPaths('Hello SVG', fontSize: 48.0);
final String d = result.toSvgPathData(); // SVG <path d="…"> attribute value

Font coordinates are Y-up; SVG is Y-down. Wrap the <path> in a <g transform="scale(1,-1)"> to render correctly (see the example 02_svg_export.dart for a complete snippet).

PDF content stream

final String pdfOps = result.toPdfContentStream();
// e.g. "100 700 m 148 700 l … h"

Quadratic Bézier curves are automatically elevated to cubic (PDF 1.7 §8.5.2).

JSON

final String json = const JsonEncoder.withIndent('  ')
    .convert(result.toJson());
// [{ "type": "MoveTo", "x": 0.0, "y": 0.0 }, …]

Flutter (dart:ui)

Add glyph_path to your Flutter project's pubspec.yaml, then use toUiPath() to draw directly on a Canvas:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:glyph_path/glyph_path.dart';

class GlyphPainter extends CustomPainter {
  const GlyphPainter({required this.result});
  final TextPathResult result;

  @override
  void paint(Canvas canvas, Size size) {
    // Flip Y: font coordinates grow upward, Canvas grows downward.
    canvas.translate(0, size.height);
    canvas.scale(1, -1);
    canvas.drawPath(result.toUiPath(), Paint()..color = Colors.black);
  }

  @override
  bool shouldRepaint(GlyphPainter old) => old.result != result;
}

See example/06_flutter_canvas.dart for a complete StatefulWidget + FutureBuilder integration example.

API Reference #

Coordinate system #

All path commands use the font coordinate system: Y increases upward, baseline at Y = 0. Glyph outlines span from the descender line (negative Y) to the ascender line (positive Y).

To render in Y-down environments (Flutter Canvas, SVG, HTML canvas), apply a flip at render time:

canvas.translate(offsetX, offsetY + ascenderHeight);
canvas.scale(1, -1);
canvas.drawPath(result.toUiPath(), paint);

For glyph morphing, interpolate in font coordinates (before any scale or flip), then apply scale(1, -1) once at render time. This avoids re-inverting normals between keyframes:

  1. Obtain per-glyph commands via generateGlyphPaths(..., fontSize: null) (design units)
  2. Interpolate coordinates between keyframe fonts
  3. At render time, scale by fontSize / unitsPerEm and flip Y

Classes and extensions #

Type Description
Font Parses a Uint8List font (.ttf/.otf) via Font.parse(). Exposes generateGlyphPaths(), generateRawGlyphPaths(), measureText(), resolveGlyphIdsForLayout(), metrics, and lineGap.
FontMetrics Typographic values (unitsPerEm, ascender, descender) in design units; from Font.metrics.
TextMeasurement Result of measureText()width, ascent, and descent in scaled pixels.
TextLayout Multi-line layout engine; wraps a Font and exposes layout() with word-wrap and alignment.
TextPathResult Output of generateGlyphPaths()glyphs (List<GlyphPathResult>), lazily-cached commands, and scaled metrics.
GlyphPathResult Per-glyph result — commands, contours, advanceWidth, tight bounds, positionX, and sourceText.
GlyphBounds Tight axis-aligned bounding box (minX, minY, maxX, maxY) computed from Bézier curve extrema.
Contour A closed sub-path with commands, WindingDirection, and signedArea (shoelace / 2).
WindingDirection Enum: clockwise / counterClockwise in font coordinate space (Y-up).
computeWindingResult Low-level function returning WindingDirection and signedArea in one shoelace pass.
splitIntoContours Top-level function that splits any List<PathCommand> into List<Contour>.
PathCommandListTransform Extension on List<PathCommand>translate(), scale(), flipY().
TextLayoutResult Output of TextLayout.layout()lines and totalHeight.
TextLineResult One rendered line — commands, baselineY, lineWidth, glyphs, and alignmentOffset.
PathCommand Sealed base class; subtypes: MoveTo, LineTo, QuadTo, CubicTo, ClosePath.
FontParseException Thrown by Font.parse() for malformed input or exceeded limits, and by generateGlyphPaths()/measureText()/resolveGlyphIdsForLayout()/TextLayout.layout() if GSUB ligature evaluation exceeds its per-call budget.
ResourceLimits Abstract class of static const hard caps on all parsing operations.
SvgExporter Extension on TextPathResult; toSvgPathData() returns an SVG d string.
PdfContentStreamExporter Extension on TextPathResult; toPdfContentStream() returns PDF path operators.
JsonExporter Extension on TextPathResult; toJson() returns List<Map<String, dynamic>>.

Security #

Font.parse() validates the sfnt magic bytes and all required table headers before any further parsing. Font-structure parse errors are unified to FontParseException; invalid public API arguments (oversized text, out-of-range font size, non-positive cache sizes) raise ArgumentError instead — see SECURITY.md for the full distinction.

ResourceLimits enforces hard caps during both font parsing and text-shaping API calls:

  • Font structure — max tables (64), max glyphs (65535), max composite depth (8), max composite components (64)
  • Glyph geometry — max contours per glyph (256), max points per glyph (32768)
  • cmap — max format 4 iterations (65536), max format 12 groups (10000), max format 12 code-point span (1114112)
  • Kerning — max kern pairs (65535), max GPOS pairs (65535), max GPOS matrix cells (65535)
  • GSUB — max ligature rules (10000), max ligature component count (16), max ligature evaluation ops per call (1000000)
  • CFF — max subroutine call depth (10), max charstring stack depth (513)
  • Input — max input text length (4096 UTF-16 code units), max font size (1e6)

Each table parser receives a BinaryReader.slice() scoped to the table's declared byte range, making cross-table reads structurally impossible.

For full details on parser-level bounds checks, see SECURITY.md.

Reporting vulnerabilities #

Please report security concerns — including denial-of-service vectors or parser bypasses — via a GitHub Security Advisory. Do not open a public issue for security vulnerabilities.

Requirements #

  • Dart >= 3.8.0
  • Pure Dart — no Flutter dependency

License #

This package is released under the BSD 3-Clause license — see LICENSE.

Dependencies #

Package License Notes
cacherine MIT Runtime dependency
meta BSD-3-Clause Runtime dependency (annotations only)

test is a dev-only dependency and is not included in the published package.

Font licensing #

This package ships no fonts. Ensure that any font file you supply to Font.parse() is used in compliance with its license terms.

  • OFL (e.g., Noto) and Apache 2.0 (e.g., Roboto) explicitly permit outline embedding and export.
  • Commercial EULAs may restrict outline export or conversion. Check the Embedding and Conversion clauses before distributing path data derived from a commercial font.

Examples #

The example/ directory contains runnable examples (01–05 via dart run; 06 requires a Flutter project). Each requires a font file path as a command-line argument. Free fonts are available at Google Fonts and Noto Fonts.

File Run command Description
01_basic_usage.dart dart run example/01_basic_usage.dart path/to/font.ttf Core API: generateGlyphPaths() + PathCommand iteration + measureText()
02_svg_export.dart dart run example/02_svg_export.dart latin.ttf cjk.ttf SVG output via toSvgPathData() — Latin and CJK text
03_pdf_content_stream.dart dart run example/03_pdf_content_stream.dart path/to/font.ttf PDF operator export via toPdfContentStream()
04_multiline_layout.dart dart run example/04_multiline_layout.dart path/to/font.ttf Multi-line layout with left / center / right alignment
05_json_export.dart dart run example/05_json_export.dart path/to/font.ttf Serialising path commands to JSON via toJson()
06_flutter_canvas.dart (copy into a Flutter project) Flutter CustomPainter integration using toUiPath()

Contributing #

Contributions are welcome! To maintain code quality, this project uses a strict CI pipeline.

Local setup #

We recommend setting up the pre-commit hook to automate formatting and lint fixes:

sh scripts/setup-hooks.sh

This links .git/hooks/pre-commit to scripts/pre-commit, ensuring your changes match the project's style guide automatically.

Development workflow #

After cloning, dart pub get is sufficient to set up all dependencies including the test runner — no global tool installation required.

# 1. Apply automated fixes (trailing commas, type annotations, etc.)
dart fix --apply

# 2. Format the code
dart format .

# 3. Run static analysis
dart analyze

# 4. Run the test suite (with coverage)
dart test --coverage=coverage

# 5. Verify pub.dev publishing checklist
dart pub publish --dry-run

Note: If you've modified layout or rendering logic, regenerate the preview SVGs:

dart run example/generate_preview_svgs.dart

Feel free to open an issue or submit a pull request for any suggestions or bug fixes.

0
likes
0
points
77
downloads

Publisher

verified publishercrossapplication.members.co.jp

Weekly Downloads

A pure Dart OpenType/TrueType parser and text-to-path converter.

Repository (GitHub)
View/report issues

Topics

#font #text #svg #opentype #truetype

License

(pending) (license)

Dependencies

cacherine, meta

More

Packages that depend on glyph_path