162 lines
4.8 KiB
Markdown
162 lines
4.8 KiB
Markdown
# 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)
|
|
|
|
```ts
|
|
/** 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
|
|
|
|
```ts
|
|
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
|
|
|
|
```text
|
|
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`).
|