Files
2026-05-27 23:29:53 +01:00

4.8 KiB

OpenSpec Change: enterprise-font-convert

  • Change ID: enterprise-font-convert
  • Package: @suaveplan/font-convert
  • Category: universal
  • Target path: universal/compression/font-convert
  • Status: PROPOSED
  • Date: 2026-04-25
  • Reference source: /mnt/s/web/suaveplan/packages/universal/font-compress/src/converter/

Summary

Deliver @suaveplan/font-convert on the genesis branch from scratch at enterprise quality. The package converts TTF/OTF font files to the typeface.js JSON format used by Three.js. It parses binary fonts via opentype.js, extracts glyph outline commands, corrects path winding order for TrueType fonts, and emits a TypefaceFont JSON structure. Also defines the shared TypefaceFont and PathCommand types consumed by @suaveplan/font-compress and @suaveplan/font-decompress.


Current-state audit (genesis)

The package does not exist on genesis yet. The original logic lived inside @suaveplan/font-compress on main; it is extracted here as a standalone package.


Available workspace deps on genesis

Package Role
@suaveplan/error (workspace:^) Base class for FontConvertError; FONT_CONVERT.* codes
@suaveplan/types (workspace:^) LoggerLike, Clock
opentype.js (catalog:fonts) Font binary parsing (TTF/OTF/WOFF/WOFF2)
@types/opentype.js (catalog:fonts) Type definitions for opentype.js
zod (catalog:forms) Zod schemas for public option types
@opentelemetry/api (catalog:observability) Spans + counters

Dev deps: vitest, @vitest/coverage-istanbul, fast-check, mitata (all catalog:testing).


Shared types (canonical source for downstream packages)

/** A single path command in typeface.js outline format */
export type PathCommandType = "m" | "l" | "q" | "b" | "z";

export interface PathCommand {
    type: PathCommandType;
    x?: number;
    y?: number;
    x1?: number;
    y1?: number;
    x2?: number;
    y2?: number;
}

/** Typeface.js glyph outline */
export interface TypefaceGlyph {
    ha: number;        // horizontal advance
    x_min: number;
    x_max: number;
    o: string;         // outline string: space-separated "type x y ..." tokens
}

/** Typeface.js font format — output of FontConverter, input to BinaryFontPacker */
export interface TypefaceFont {
    glyphs: Record<string, TypefaceGlyph>;
    familyName: string;
    ascender: number;
    descender: number;
    underlinePosition: number;
    underlineThickness: number;
    boundingBox: { yMin: number; xMin: number; yMax: number; xMax: number };
    resolution: number;
    original_font_information: unknown;
    cssFontWeight: string;
    cssFontStyle: string;
    reversed?: boolean;
}

Public API

class FontConvertError extends SuaveplanError<FontConvertErrorCode>

type FontConvertErrorCode =
  | 'FONT_CONVERT_PARSE_FAILED'
  | 'FONT_CONVERT_ENCODE_FAILED'
  | 'FONT_CONVERT_INVALID_OPTIONS'
  | 'FONT_CONVERT_UNSUPPORTED_FORMAT'

interface ConversionOptions {
    reverseTypeface?: boolean       // default: false — reverse winding for CFF fonts
    restrictCharacters?: boolean    // default: false
    restrictCharacterSet?: string   // characters to include when restrictCharacters=true
}

class FontConverter {
    constructor(options?: FontConverterOptions)
    convert(data: Uint8Array | ArrayBuffer, options?: ConversionOptions): TypefaceFont
    convertFromParsed(font: opentype.Font, options?: ConversionOptions): TypefaceFont
}

class BatchConverter {
    constructor(options?: FontConverterOptions)
    convertAll(items: Array<{ name: string; data: Uint8Array }>): Array<{ name: string; font: TypefaceFont }>
}

function createFontConverter(options?: FontConverterOptions): FontConverter

File layout

universal/compression/font-convert/
  package.json
  tsconfig.lib.json
  vitest.config.ts
  vite.config.ts
  src/
    index.ts
    error-codes.ts
    constants/
      index.ts
    types/
      typeface.ts          # TypefaceFont, TypefaceGlyph, PathCommand types
      typeface.md
    core/
      font-converter/
        font-converter.ts
        font-converter.md
        font-converter.types.ts
        font-converter.test.ts
        index.ts
      batch-converter/
        batch-converter.ts
        batch-converter.md
        batch-converter.test.ts
        index.ts
    utils/
      reverse-winding.ts
      reverse-winding.md
      reverse-winding.test.ts
      outline-to-string.ts
      outline-to-string.md
      outline-to-string.test.ts
    __benchmarks__/
      font-convert.bench.ts
  tests/
    integration.test.ts

Impact

Downstream: @suaveplan/font-compress (BTF packing), @suaveplan/font-decompress (BTF unpacking) both import TypefaceFont and PathCommand from this package. Breaking changes: none — fresh build. Prerequisites: none (leaf-level except opentype.js).