Initial Commit
This commit is contained in:
161
openspec/archive/2026-04-25-enterprise-font-convert/proposal.md
Normal file
161
openspec/archive/2026-04-25-enterprise-font-convert/proposal.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 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`).
|
||||
Reference in New Issue
Block a user