text-shaper 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +413 -39
- package/dist/buffer/glyph-buffer.d.ts +2 -2
- package/dist/font/brotli/decode.d.ts +42 -0
- package/dist/font/tables/cff2.d.ts +63 -0
- package/dist/font/tables/gpos.d.ts +15 -0
- package/dist/font/tables/gsub.d.ts +9 -0
- package/dist/index.js +6 -8
- package/dist/raster/msdf.d.ts +12 -0
- package/dist/shaper/shaper.d.ts +128 -1
- package/package.json +2 -2
- package/dist/index.js.map +0 -134
- package/dist/index.test.d.ts +0 -1
- package/dist/perf/benchmark-runner-eksh2b26.js +0 -0
- package/dist/perf/benchmark-runner.html +0 -517
- package/dist/perf/comparison-tests.js +0 -22676
- package/dist/perf/comparison-tests.js.map +0 -111
- package/dist/perf/cpu/cpu-perf.js +0 -21920
- package/dist/perf/cpu/cpu-perf.js.map +0 -108
- package/dist/perf/gpu/webgl-perf.js +0 -470
- package/dist/perf/gpu/webgl-perf.js.map +0 -11
- package/dist/perf/gpu/webgpu-perf.js +0 -442
- package/dist/perf/gpu/webgpu-perf.js.map +0 -11
- package/dist/perf/index.html +0 -258
- package/dist/perf/module-loader.js +0 -18
- package/dist/perf/utils/perf-utils.js +0 -170
- package/dist/perf/utils/perf-utils.js.map +0 -10
- package/dist/typeshaper.js +0 -7
- package/dist/unicode/bidi/brackets.data.d.ts +0 -5
- package/dist/unicode/bidi/char-types.data.d.ts +0 -25
- package/dist/unicode/bidi/mirroring.data.d.ts +0 -2
package/README.md
CHANGED
|
@@ -5,17 +5,33 @@
|
|
|
5
5
|
|
|
6
6
|
Pure TypeScript text shaping engine with OpenType layout, TrueType hinting, and FreeType-style rasterization. Works in browsers and Bun/Node.js with zero dependencies.
|
|
7
7
|
|
|
8
|
+
## Performance
|
|
9
|
+
|
|
10
|
+
text-shaper outperforms harfbuzzjs (WebAssembly) and opentype.js across all benchmarks:
|
|
11
|
+
|
|
12
|
+
| Category | vs harfbuzzjs | vs opentype.js |
|
|
13
|
+
|----------|---------------|----------------|
|
|
14
|
+
| Path Extraction | 16x faster | 10x faster |
|
|
15
|
+
| Text to SVG | 1.2-1.5x faster | 4-6x faster |
|
|
16
|
+
| Latin Shaping | 1.5x faster | 22x faster |
|
|
17
|
+
| Arabic Shaping | 1.2x faster | 86x faster |
|
|
18
|
+
| Hebrew Shaping | 1.6x faster | 33x faster |
|
|
19
|
+
| Hindi Shaping | 3.6x faster | 11x faster |
|
|
20
|
+
| Myanmar Shaping | 10.5x faster | 17x faster |
|
|
21
|
+
| CJK Shaping | 1.3-1.5x faster | 11-13x faster |
|
|
22
|
+
|
|
8
23
|
## Features
|
|
9
24
|
|
|
10
25
|
- **OpenType Layout**: Full GSUB (substitution) and GPOS (positioning) support
|
|
11
26
|
- **Complex Scripts**: Arabic, Indic, USE (Universal Shaping Engine) shapers
|
|
12
27
|
- **Variable Fonts**: fvar, gvar, avar, HVAR, VVAR, MVAR tables
|
|
13
28
|
- **AAT Support**: morx, kerx, trak tables for Apple fonts
|
|
14
|
-
- **Color Fonts**: SVG, sbix, CBDT/CBLC tables
|
|
29
|
+
- **Color Fonts**: SVG, sbix, CBDT/CBLC, COLR/CPAL tables
|
|
15
30
|
- **BiDi**: UAX #9 bidirectional text algorithm
|
|
16
31
|
- **Rasterization**: FreeType-style grayscale, LCD subpixel, and monochrome rendering
|
|
17
32
|
- **TrueType Hinting**: Full bytecode interpreter (150+ opcodes)
|
|
18
33
|
- **Texture Atlas**: GPU-ready glyph atlas generation with shelf packing
|
|
34
|
+
- **SDF/MSDF**: Signed distance field rendering for scalable text
|
|
19
35
|
- **Zero Dependencies**: Pure TypeScript, works in browser and Node.js
|
|
20
36
|
|
|
21
37
|
## Installation
|
|
@@ -28,16 +44,18 @@ bun add text-shaper
|
|
|
28
44
|
|
|
29
45
|
## Usage
|
|
30
46
|
|
|
47
|
+
### Basic Shaping
|
|
48
|
+
|
|
31
49
|
```typescript
|
|
32
50
|
import { Font, shape, UnicodeBuffer } from "text-shaper";
|
|
33
51
|
|
|
34
52
|
// Load a font
|
|
35
|
-
const fontData = await
|
|
36
|
-
const font =
|
|
53
|
+
const fontData = await fetch("path/to/font.ttf").then(r => r.arrayBuffer());
|
|
54
|
+
const font = Font.load(fontData);
|
|
37
55
|
|
|
38
56
|
// Create a buffer with text
|
|
39
57
|
const buffer = new UnicodeBuffer();
|
|
40
|
-
buffer.
|
|
58
|
+
buffer.addStr("Hello, World!");
|
|
41
59
|
|
|
42
60
|
// Shape the text
|
|
43
61
|
const glyphBuffer = shape(font, buffer);
|
|
@@ -46,21 +64,49 @@ const glyphBuffer = shape(font, buffer);
|
|
|
46
64
|
for (let i = 0; i < glyphBuffer.length; i++) {
|
|
47
65
|
const info = glyphBuffer.info[i];
|
|
48
66
|
const pos = glyphBuffer.pos[i];
|
|
49
|
-
console.log(`Glyph ${info.glyphId}: advance=${pos.xAdvance}
|
|
67
|
+
console.log(`Glyph ${info.glyphId}: advance=${pos.xAdvance}`);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### High-Performance Shaping
|
|
72
|
+
|
|
73
|
+
For best performance, reuse buffers with `shapeInto`:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { Font, shapeInto, UnicodeBuffer, GlyphBuffer } from "text-shaper";
|
|
77
|
+
|
|
78
|
+
const font = Font.load(fontData);
|
|
79
|
+
const uBuffer = new UnicodeBuffer();
|
|
80
|
+
const gBuffer = GlyphBuffer.withCapacity(128);
|
|
81
|
+
|
|
82
|
+
// Shape multiple strings efficiently
|
|
83
|
+
for (const text of texts) {
|
|
84
|
+
uBuffer.clear();
|
|
85
|
+
uBuffer.addStr(text);
|
|
86
|
+
gBuffer.reset();
|
|
87
|
+
shapeInto(font, uBuffer, gBuffer);
|
|
88
|
+
// Process gBuffer...
|
|
50
89
|
}
|
|
51
90
|
```
|
|
52
91
|
|
|
53
92
|
### With Features
|
|
54
93
|
|
|
55
94
|
```typescript
|
|
56
|
-
import { Font, shape, UnicodeBuffer, feature
|
|
95
|
+
import { Font, shape, UnicodeBuffer, feature } from "text-shaper";
|
|
96
|
+
|
|
97
|
+
const glyphBuffer = shape(font, buffer, {
|
|
98
|
+
features: [
|
|
99
|
+
feature("smcp"), // Small caps
|
|
100
|
+
feature("liga"), // Ligatures
|
|
101
|
+
feature("kern"), // Kerning
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Or use convenience helpers
|
|
106
|
+
import { smallCaps, standardLigatures, kerning, combineFeatures } from "text-shaper";
|
|
57
107
|
|
|
58
108
|
const glyphBuffer = shape(font, buffer, {
|
|
59
|
-
features:
|
|
60
|
-
feature("smcp", 1), // Small caps
|
|
61
|
-
feature("liga", 1), // Ligatures
|
|
62
|
-
feature("kern", 1), // Kerning
|
|
63
|
-
),
|
|
109
|
+
features: combineFeatures(smallCaps(), standardLigatures(), kerning()),
|
|
64
110
|
});
|
|
65
111
|
```
|
|
66
112
|
|
|
@@ -80,42 +126,26 @@ const glyphBuffer = shape(font, buffer, {
|
|
|
80
126
|
### Rendering to SVG
|
|
81
127
|
|
|
82
128
|
```typescript
|
|
83
|
-
import {
|
|
129
|
+
import {
|
|
130
|
+
Font, shape, UnicodeBuffer,
|
|
131
|
+
glyphBufferToShapedGlyphs, shapedTextToSVG
|
|
132
|
+
} from "text-shaper";
|
|
84
133
|
|
|
85
134
|
const buffer = new UnicodeBuffer();
|
|
86
|
-
buffer.
|
|
135
|
+
buffer.addStr("Hello");
|
|
87
136
|
|
|
88
137
|
const glyphBuffer = shape(font, buffer);
|
|
89
|
-
const
|
|
138
|
+
const shapedGlyphs = glyphBufferToShapedGlyphs(glyphBuffer);
|
|
139
|
+
const svg = shapedTextToSVG(font, shapedGlyphs, { fontSize: 48 });
|
|
90
140
|
```
|
|
91
141
|
|
|
92
|
-
## API
|
|
93
|
-
|
|
94
|
-
### Core Classes
|
|
95
|
-
|
|
96
|
-
- `Font` - Load and parse OpenType/TrueType fonts
|
|
97
|
-
- `Face` - Font face with variation coordinates applied
|
|
98
|
-
- `UnicodeBuffer` - Input buffer for text to shape
|
|
99
|
-
- `GlyphBuffer` - Output buffer containing shaped glyphs
|
|
100
|
-
|
|
101
|
-
### Shaping
|
|
102
|
-
|
|
103
|
-
- `shape(font, buffer, options?)` - Shape text in a buffer
|
|
104
|
-
- `createShapePlan(font, options)` - Create a reusable shape plan
|
|
105
|
-
|
|
106
|
-
### Rendering
|
|
107
|
-
|
|
108
|
-
- `getGlyphPath(font, glyphId)` - Get glyph outline as path commands
|
|
109
|
-
- `shapedTextToSVG(font, buffer, options)` - Render shaped text to SVG
|
|
110
|
-
- `renderShapedText(ctx, font, buffer, options)` - Render to Canvas 2D context
|
|
111
|
-
|
|
112
142
|
### Rasterization
|
|
113
143
|
|
|
114
144
|
```typescript
|
|
115
145
|
import { Font, rasterizeGlyph, buildAtlas, PixelMode } from "text-shaper";
|
|
116
146
|
|
|
117
147
|
// Rasterize a single glyph
|
|
118
|
-
const
|
|
148
|
+
const bitmap = rasterizeGlyph(font, glyphId, 48, {
|
|
119
149
|
pixelMode: PixelMode.Gray, // Gray, Mono, or LCD
|
|
120
150
|
hinting: true, // Enable TrueType hinting
|
|
121
151
|
});
|
|
@@ -129,6 +159,342 @@ const atlas = buildAtlas(font, glyphIds, {
|
|
|
129
159
|
});
|
|
130
160
|
```
|
|
131
161
|
|
|
162
|
+
### SDF/MSDF Rendering
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import {
|
|
166
|
+
Font, getGlyphPath, renderSdf, renderMsdf, buildMsdfAtlas
|
|
167
|
+
} from "text-shaper";
|
|
168
|
+
|
|
169
|
+
// Single glyph SDF
|
|
170
|
+
const path = getGlyphPath(font, glyphId);
|
|
171
|
+
if (path) {
|
|
172
|
+
const sdf = renderSdf(path, {
|
|
173
|
+
width: 64,
|
|
174
|
+
height: 64,
|
|
175
|
+
scale: 1,
|
|
176
|
+
spread: 8,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// MSDF atlas for GPU text rendering (handles font internally)
|
|
181
|
+
const msdfAtlas = buildMsdfAtlas(font, glyphIds, {
|
|
182
|
+
fontSize: 32,
|
|
183
|
+
spread: 4,
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Fluent API
|
|
188
|
+
|
|
189
|
+
Two composition styles for glyph manipulation and rendering:
|
|
190
|
+
|
|
191
|
+
#### Builder Pattern (Method Chaining)
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { Font, glyph, char, glyphVar, combine, PixelMode } from "text-shaper";
|
|
195
|
+
|
|
196
|
+
// From glyph ID
|
|
197
|
+
const rgba = glyph(font, glyphId)
|
|
198
|
+
?.scale(2)
|
|
199
|
+
.rotateDeg(15)
|
|
200
|
+
.rasterizeAuto({ padding: 2 })
|
|
201
|
+
.blur(5)
|
|
202
|
+
.toRGBA();
|
|
203
|
+
|
|
204
|
+
// From character
|
|
205
|
+
const svg = char(font, "A")
|
|
206
|
+
?.scale(3)
|
|
207
|
+
.italic(12)
|
|
208
|
+
.toSVG({ width: 100, height: 100 });
|
|
209
|
+
|
|
210
|
+
// Variable fonts
|
|
211
|
+
const bitmap = glyphVar(font, glyphId, [700, 100]) // wght=700, wdth=100
|
|
212
|
+
?.embolden(50)
|
|
213
|
+
.rasterize({ pixelMode: PixelMode.Gray, scale: 2 })
|
|
214
|
+
.toBitmap();
|
|
215
|
+
|
|
216
|
+
// Combine multiple glyphs
|
|
217
|
+
const h = glyph(font, hGlyphId)?.translate(0, 0);
|
|
218
|
+
const i = glyph(font, iGlyphId)?.translate(100, 0);
|
|
219
|
+
if (h && i) {
|
|
220
|
+
const combined = combine(h, i).scale(2).rasterizeAuto().toRGBA();
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### PathBuilder Methods
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Transforms (lazy - accumulated as matrix)
|
|
228
|
+
.scale(sx, sy?) // Scale uniformly or non-uniformly
|
|
229
|
+
.translate(dx, dy) // Translate by offset
|
|
230
|
+
.rotate(radians) // Rotate by angle in radians
|
|
231
|
+
.rotateDeg(degrees) // Rotate by angle in degrees
|
|
232
|
+
.shear(shearX, shearY) // Shear transform
|
|
233
|
+
.italic(degrees) // Italic slant (convenience for shear)
|
|
234
|
+
.matrix(m) // Apply custom 2D matrix
|
|
235
|
+
.perspective(m) // Apply 3D perspective matrix
|
|
236
|
+
.resetTransform() // Reset accumulated transforms
|
|
237
|
+
.apply() // Apply accumulated transforms to path
|
|
238
|
+
|
|
239
|
+
// Path effects (immediate - modifies path)
|
|
240
|
+
.embolden(strength) // Make strokes thicker
|
|
241
|
+
.condense(factor) // Horizontal compression
|
|
242
|
+
.oblique(slant) // Oblique slant effect
|
|
243
|
+
.stroke(width, cap?, join?) // Convert to stroked outline
|
|
244
|
+
.strokeAsymmetric(opts) // Independent x/y stroke widths
|
|
245
|
+
|
|
246
|
+
// Output
|
|
247
|
+
.rasterize(options) // Rasterize to BitmapBuilder
|
|
248
|
+
.rasterizeAuto(options?) // Auto-sized rasterization
|
|
249
|
+
.toSdf(options) // Render to SDF bitmap
|
|
250
|
+
.toMsdf(options) // Render to MSDF bitmap
|
|
251
|
+
.toSVG(options?) // Export as SVG string
|
|
252
|
+
.toSVGElement(options?) // Export as SVG path element
|
|
253
|
+
.toCanvas(ctx, options?) // Draw to Canvas 2D context
|
|
254
|
+
.toPath() // Get raw GlyphPath
|
|
255
|
+
.clone() // Clone the builder
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### BitmapBuilder Methods
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// Blur effects
|
|
262
|
+
.blur(radius) // Gaussian blur
|
|
263
|
+
.boxBlur(radius) // Box blur (faster)
|
|
264
|
+
.fastBlur(radius) // Fast approximated blur
|
|
265
|
+
.cascadeBlur(rx, ry?) // Cascade blur for large radii
|
|
266
|
+
.adaptiveBlur(rx, ry?) // Adaptive quality blur
|
|
267
|
+
|
|
268
|
+
// Modifications
|
|
269
|
+
.embolden(xStrength, yStrength?) // Expand bitmap
|
|
270
|
+
.shift(dx, dy) // Shift bitmap contents
|
|
271
|
+
.resize(width, height) // Resize (nearest neighbor)
|
|
272
|
+
.resizeBilinear(w, h) // Resize with bilinear filtering
|
|
273
|
+
.pad(left, top, right, bottom) // Add padding
|
|
274
|
+
.convert(pixelMode) // Convert pixel format
|
|
275
|
+
|
|
276
|
+
// Output
|
|
277
|
+
.toRGBA() // Export as RGBA Uint8Array
|
|
278
|
+
.toGray() // Export as grayscale Uint8Array
|
|
279
|
+
.toBitmap() // Get Bitmap object
|
|
280
|
+
.toRasterizedGlyph() // Get RasterizedGlyph with metrics
|
|
281
|
+
.clone() // Clone the builder
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### Pipe Pattern (Functional)
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import {
|
|
288
|
+
pipe, glyph,
|
|
289
|
+
$scale, $rotate, $embolden,
|
|
290
|
+
$rasterize, $blur, $toRGBA
|
|
291
|
+
} from "text-shaper";
|
|
292
|
+
|
|
293
|
+
// Compose operations functionally
|
|
294
|
+
const rgba = pipe(
|
|
295
|
+
glyph(font, glyphId),
|
|
296
|
+
$scale(2),
|
|
297
|
+
$rotate(Math.PI / 12),
|
|
298
|
+
$embolden(30),
|
|
299
|
+
$rasterize({ pixelMode: PixelMode.Gray }),
|
|
300
|
+
$blur(3),
|
|
301
|
+
$toRGBA()
|
|
302
|
+
);
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Line Breaking & Justification
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
import { breakIntoLines, justify, JustifyMode } from "text-shaper";
|
|
309
|
+
|
|
310
|
+
// Break text into lines
|
|
311
|
+
const lines = breakIntoLines(glyphBuffer, font, maxWidth);
|
|
312
|
+
|
|
313
|
+
// Justify a line
|
|
314
|
+
const justified = justify(line, targetWidth, {
|
|
315
|
+
mode: JustifyMode.Distribute,
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Text Segmentation
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { countGraphemes, splitGraphemes, splitWords } from "text-shaper";
|
|
323
|
+
|
|
324
|
+
// Count grapheme clusters (visual characters)
|
|
325
|
+
const count = countGraphemes("👨👩👧👦Hello"); // 6 (family emoji = 1)
|
|
326
|
+
|
|
327
|
+
// Split into graphemes
|
|
328
|
+
const graphemes = splitGraphemes("नमस्ते"); // Devanagari clusters
|
|
329
|
+
|
|
330
|
+
// Split into words
|
|
331
|
+
const words = splitWords("Hello World"); // ["Hello", " ", "World"]
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Canvas Rendering
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import {
|
|
338
|
+
Font, shape, UnicodeBuffer,
|
|
339
|
+
glyphBufferToShapedGlyphs, renderShapedText
|
|
340
|
+
} from "text-shaper";
|
|
341
|
+
|
|
342
|
+
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
|
|
343
|
+
const ctx = canvas.getContext("2d")!;
|
|
344
|
+
|
|
345
|
+
const buffer = new UnicodeBuffer();
|
|
346
|
+
buffer.addStr("Hello Canvas!");
|
|
347
|
+
|
|
348
|
+
const glyphBuffer = shape(font, buffer);
|
|
349
|
+
const shapedGlyphs = glyphBufferToShapedGlyphs(glyphBuffer);
|
|
350
|
+
|
|
351
|
+
renderShapedText(ctx, font, shapedGlyphs, {
|
|
352
|
+
x: 50,
|
|
353
|
+
y: 100,
|
|
354
|
+
fontSize: 48,
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### BiDi & RTL Text
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import {
|
|
362
|
+
Font, shape, UnicodeBuffer,
|
|
363
|
+
processBidi, reorderGlyphs, detectDirection
|
|
364
|
+
} from "text-shaper";
|
|
365
|
+
|
|
366
|
+
// Automatic RTL detection and reordering
|
|
367
|
+
const buffer = new UnicodeBuffer();
|
|
368
|
+
buffer.addStr("Hello שלום World"); // Mixed LTR/RTL
|
|
369
|
+
|
|
370
|
+
const glyphBuffer = shape(font, buffer);
|
|
371
|
+
// Glyphs are automatically reordered for visual display
|
|
372
|
+
|
|
373
|
+
// Manual BiDi processing
|
|
374
|
+
const bidiResult = processBidi("مرحبا Hello");
|
|
375
|
+
console.log(bidiResult.direction); // "rtl"
|
|
376
|
+
console.log(bidiResult.levels); // Embedding levels per character
|
|
377
|
+
|
|
378
|
+
// Detect text direction
|
|
379
|
+
const dir = detectDirection("שלום"); // "rtl"
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Color Fonts
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import {
|
|
386
|
+
Font,
|
|
387
|
+
hasColorGlyph, getColorPaint, getColorLayers, // COLR
|
|
388
|
+
hasSvgGlyph, getSvgDocument, // SVG
|
|
389
|
+
hasColorBitmap, getBitmapGlyph, // CBDT/sbix
|
|
390
|
+
} from "text-shaper";
|
|
391
|
+
|
|
392
|
+
// Check for color glyph support
|
|
393
|
+
const glyphId = font.glyphId("😀".codePointAt(0)!);
|
|
394
|
+
|
|
395
|
+
// COLR/CPAL (vector color)
|
|
396
|
+
if (hasColorGlyph(font, glyphId)) {
|
|
397
|
+
const paint = getColorPaint(font, glyphId);
|
|
398
|
+
// Render paint tree...
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// SVG color glyphs
|
|
402
|
+
if (hasSvgGlyph(font, glyphId)) {
|
|
403
|
+
const svgDoc = getSvgDocument(font, glyphId);
|
|
404
|
+
// Use SVG document directly
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Bitmap color glyphs (sbix, CBDT)
|
|
408
|
+
if (hasColorBitmap(font, glyphId)) {
|
|
409
|
+
const bitmap = getBitmapGlyph(font, glyphId, 128); // ppem=128
|
|
410
|
+
// bitmap.data contains PNG/JPEG data
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Texture Atlas (WebGL/GPU)
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
import {
|
|
418
|
+
Font, buildAtlas, buildStringAtlas, buildMsdfAtlas,
|
|
419
|
+
atlasToRGBA, getGlyphUV, PixelMode
|
|
420
|
+
} from "text-shaper";
|
|
421
|
+
|
|
422
|
+
// Build atlas from glyph IDs
|
|
423
|
+
const atlas = buildAtlas(font, glyphIds, {
|
|
424
|
+
fontSize: 32,
|
|
425
|
+
padding: 2,
|
|
426
|
+
pixelMode: PixelMode.Gray,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Build atlas from string (auto-extracts unique glyphs)
|
|
430
|
+
const textAtlas = buildStringAtlas(font, "Hello World!", {
|
|
431
|
+
fontSize: 48,
|
|
432
|
+
padding: 1,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Convert to RGBA for WebGL texture
|
|
436
|
+
const rgba = atlasToRGBA(atlas);
|
|
437
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, atlas.width, atlas.height,
|
|
438
|
+
0, gl.RGBA, gl.UNSIGNED_BYTE, rgba);
|
|
439
|
+
|
|
440
|
+
// Get UV coordinates for rendering
|
|
441
|
+
const uv = getGlyphUV(atlas, glyphId);
|
|
442
|
+
// uv = { u0, v0, u1, v1, ... }
|
|
443
|
+
|
|
444
|
+
// MSDF atlas for scalable GPU text
|
|
445
|
+
const msdfAtlas = buildMsdfAtlas(font, glyphIds, {
|
|
446
|
+
fontSize: 32,
|
|
447
|
+
spread: 4,
|
|
448
|
+
});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Browser Usage
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
// Fetch font from URL
|
|
455
|
+
const fontData = await fetch("/fonts/MyFont.ttf").then(r => r.arrayBuffer());
|
|
456
|
+
const font = Font.load(fontData);
|
|
457
|
+
|
|
458
|
+
// Or from File input
|
|
459
|
+
const file = input.files[0];
|
|
460
|
+
const buffer = await file.arrayBuffer();
|
|
461
|
+
const font = Font.load(buffer);
|
|
462
|
+
|
|
463
|
+
// Works with any ArrayBuffer source
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## API Reference
|
|
467
|
+
|
|
468
|
+
### Core Classes
|
|
469
|
+
|
|
470
|
+
| Class | Description |
|
|
471
|
+
|-------|-------------|
|
|
472
|
+
| `Font` | Load and parse OpenType/TrueType fonts |
|
|
473
|
+
| `Face` | Font face with variation coordinates applied |
|
|
474
|
+
| `UnicodeBuffer` | Input buffer for text to shape |
|
|
475
|
+
| `GlyphBuffer` | Output buffer containing shaped glyphs |
|
|
476
|
+
|
|
477
|
+
### Shaping Functions
|
|
478
|
+
|
|
479
|
+
| Function | Description |
|
|
480
|
+
|----------|-------------|
|
|
481
|
+
| `shape(font, buffer, options?)` | Shape text, returns new GlyphBuffer |
|
|
482
|
+
| `shapeInto(font, buffer, glyphBuffer, options?)` | Shape into existing buffer (faster) |
|
|
483
|
+
| `createShapePlan(font, options)` | Create reusable shape plan |
|
|
484
|
+
| `getOrCreateShapePlan(font, options)` | Get cached or create shape plan |
|
|
485
|
+
|
|
486
|
+
### Rendering Functions
|
|
487
|
+
|
|
488
|
+
| Function | Description |
|
|
489
|
+
|----------|-------------|
|
|
490
|
+
| `getGlyphPath(font, glyphId)` | Get glyph outline as path commands |
|
|
491
|
+
| `shapedTextToSVG(font, shapedGlyphs, options)` | Render shaped text to SVG string |
|
|
492
|
+
| `renderShapedText(ctx, font, shapedGlyphs, options)` | Render to Canvas 2D context |
|
|
493
|
+
| `glyphBufferToShapedGlyphs(buffer)` | Convert GlyphBuffer to ShapedGlyph[] |
|
|
494
|
+
| `rasterizeGlyph(font, glyphId, size, options)` | Rasterize glyph to bitmap |
|
|
495
|
+
| `rasterizePath(path, options)` | Rasterize path commands to bitmap |
|
|
496
|
+
| `buildAtlas(font, glyphIds, options)` | Build texture atlas |
|
|
497
|
+
|
|
132
498
|
### Feature Helpers
|
|
133
499
|
|
|
134
500
|
```typescript
|
|
@@ -152,10 +518,19 @@ proportionalFigures() // pnum
|
|
|
152
518
|
stylisticSet(n) // ss01-ss20
|
|
153
519
|
characterVariant(n) // cv01-cv99
|
|
154
520
|
swash() // swsh
|
|
155
|
-
|
|
156
|
-
// And many more...
|
|
157
521
|
```
|
|
158
522
|
|
|
523
|
+
### Unicode Utilities
|
|
524
|
+
|
|
525
|
+
| Function | Description |
|
|
526
|
+
|----------|-------------|
|
|
527
|
+
| `processBidi(text)` | Process bidirectional text (UAX #9) |
|
|
528
|
+
| `getScript(codepoint)` | Get Unicode script for codepoint |
|
|
529
|
+
| `getScriptRuns(text)` | Split text into script runs |
|
|
530
|
+
| `countGraphemes(text)` | Count grapheme clusters |
|
|
531
|
+
| `splitGraphemes(text)` | Split into grapheme clusters |
|
|
532
|
+
| `analyzeLineBreaks(text)` | Find line break opportunities (UAX #14) |
|
|
533
|
+
|
|
159
534
|
## Supported Tables
|
|
160
535
|
|
|
161
536
|
### Required
|
|
@@ -171,7 +546,7 @@ CFF, CFF2
|
|
|
171
546
|
fvar, gvar, avar, HVAR, VVAR, MVAR, STAT
|
|
172
547
|
|
|
173
548
|
### AAT (Apple)
|
|
174
|
-
morx, kerx, kern, trak
|
|
549
|
+
morx, kerx, kern, trak, feat
|
|
175
550
|
|
|
176
551
|
### Color
|
|
177
552
|
COLR, CPAL, SVG, sbix, CBDT, CBLC
|
|
@@ -185,4 +560,3 @@ fpgm, prep, cvt, gasp
|
|
|
185
560
|
## License
|
|
186
561
|
|
|
187
562
|
[MIT](LICENSE)
|
|
188
|
-
|
|
@@ -23,10 +23,10 @@ export declare class GlyphBuffer {
|
|
|
23
23
|
private _infoPool;
|
|
24
24
|
/** Pre-allocated position pool for reuse */
|
|
25
25
|
private _posPool;
|
|
26
|
+
/** Pre-allocated capacity hint */
|
|
27
|
+
private _capacity;
|
|
26
28
|
/** Create buffer with pre-allocated capacity (lazy object creation) */
|
|
27
29
|
static withCapacity(capacity: number): GlyphBuffer;
|
|
28
|
-
/** Hint for initial capacity (used for lazy allocation) */
|
|
29
|
-
private _capacity;
|
|
30
30
|
/** Number of glyphs */
|
|
31
31
|
get length(): number;
|
|
32
32
|
/** Reset buffer for reuse - reuses existing object pool */
|
|
@@ -3,7 +3,49 @@
|
|
|
3
3
|
* Based on the brotli.js reference implementation (MIT License)
|
|
4
4
|
* https://github.com/devongovett/brotli.js
|
|
5
5
|
*/
|
|
6
|
+
interface HuffmanCode {
|
|
7
|
+
bits: number;
|
|
8
|
+
value: number;
|
|
9
|
+
}
|
|
10
|
+
declare class BitReader {
|
|
11
|
+
private data;
|
|
12
|
+
private buf;
|
|
13
|
+
private pos;
|
|
14
|
+
private val;
|
|
15
|
+
private bitPos;
|
|
16
|
+
private bitEndPos;
|
|
17
|
+
private eos;
|
|
18
|
+
constructor(data: Uint8Array);
|
|
19
|
+
private fillBuffer;
|
|
20
|
+
readMoreInput(): void;
|
|
21
|
+
fillBitWindow(): void;
|
|
22
|
+
readBits(n: number): number;
|
|
23
|
+
get currentBitPos(): number;
|
|
24
|
+
set currentBitPos(v: number);
|
|
25
|
+
get currentVal(): number;
|
|
26
|
+
}
|
|
27
|
+
declare function buildHuffmanTable(rootTable: HuffmanCode[], tableOffset: number, rootBits: number, codeLengths: Uint8Array, codeLengthsSize: number): number;
|
|
28
|
+
declare function getNextKey(key: number, len: number): number;
|
|
29
|
+
declare function replicateValue(table: HuffmanCode[], offset: number, step: number, end: number, code: HuffmanCode): void;
|
|
30
|
+
declare function nextTableBitSize(count: Int32Array, len: number, rootBits: number): number;
|
|
31
|
+
declare function readHuffmanCodeLengths(codeLengthCodeLengths: Uint8Array, numSymbols: number, codeLengths: Uint8Array, br: BitReader): void;
|
|
32
|
+
declare function readBlockLength(table: HuffmanCode[], tableOffset: number, br: BitReader): number;
|
|
6
33
|
/**
|
|
7
34
|
* Decompress Brotli-compressed data
|
|
8
35
|
*/
|
|
9
36
|
export declare function decompress(data: Uint8Array): Uint8Array;
|
|
37
|
+
export declare const __testing: {
|
|
38
|
+
getNextKey: typeof getNextKey;
|
|
39
|
+
replicateValue: typeof replicateValue;
|
|
40
|
+
nextTableBitSize: typeof nextTableBitSize;
|
|
41
|
+
buildHuffmanTable: typeof buildHuffmanTable;
|
|
42
|
+
readBlockLength: typeof readBlockLength;
|
|
43
|
+
readHuffmanCodeLengths: typeof readHuffmanCodeLengths;
|
|
44
|
+
BitReader: typeof BitReader;
|
|
45
|
+
HuffmanCode: {
|
|
46
|
+
bits: number;
|
|
47
|
+
value: number;
|
|
48
|
+
};
|
|
49
|
+
CODE_LENGTH_CODES: number;
|
|
50
|
+
};
|
|
51
|
+
export {};
|
|
@@ -79,7 +79,70 @@ export interface ItemVariationData {
|
|
|
79
79
|
* Parse CFF2 table
|
|
80
80
|
*/
|
|
81
81
|
export declare function parseCff2(reader: Reader): Cff2Table;
|
|
82
|
+
/**
|
|
83
|
+
* Parse CFF2 INDEX structure (uses 32-bit count)
|
|
84
|
+
*/
|
|
85
|
+
declare function parseIndex(reader: Reader): Uint8Array[];
|
|
86
|
+
/**
|
|
87
|
+
* Read offset of given size
|
|
88
|
+
*/
|
|
89
|
+
declare function readOffset(reader: Reader, offSize: number): number;
|
|
90
|
+
/**
|
|
91
|
+
* Parse a CFF2 DICT structure
|
|
92
|
+
*/
|
|
93
|
+
declare function parseDict(reader: Reader): Map<number, number[]>;
|
|
94
|
+
/**
|
|
95
|
+
* Parse real number
|
|
96
|
+
*/
|
|
97
|
+
declare function parseReal(reader: Reader): number;
|
|
98
|
+
/**
|
|
99
|
+
* Parse CFF2 Top DICT
|
|
100
|
+
*/
|
|
101
|
+
declare function parseCff2TopDict(reader: Reader): Cff2TopDict;
|
|
102
|
+
/**
|
|
103
|
+
* Parse CFF2 FD DICT
|
|
104
|
+
*/
|
|
105
|
+
declare function parseCff2FDDict(reader: Reader): Cff2FDDict;
|
|
106
|
+
/**
|
|
107
|
+
* Parse CFF2 Private DICT
|
|
108
|
+
*/
|
|
109
|
+
declare function parseCff2PrivateDict(reader: Reader): Cff2PrivateDict;
|
|
110
|
+
/**
|
|
111
|
+
* Convert delta-encoded values to absolute
|
|
112
|
+
*/
|
|
113
|
+
declare function deltaToAbsolute(deltas: number[]): number[];
|
|
114
|
+
/**
|
|
115
|
+
* Parse FDSelect structure
|
|
116
|
+
*/
|
|
117
|
+
declare function parseFDSelect(reader: Reader, numGlyphs: number): Cff2FDSelect;
|
|
118
|
+
/**
|
|
119
|
+
* Parse ItemVariationStore
|
|
120
|
+
*/
|
|
121
|
+
declare function parseItemVariationStore(reader: Reader): ItemVariationStore;
|
|
122
|
+
/**
|
|
123
|
+
* Parse VariationRegionList
|
|
124
|
+
*/
|
|
125
|
+
declare function parseVariationRegionList(reader: Reader): VariationRegionList;
|
|
126
|
+
/**
|
|
127
|
+
* Parse ItemVariationData
|
|
128
|
+
*/
|
|
129
|
+
declare function parseItemVariationData(reader: Reader): ItemVariationData;
|
|
82
130
|
/**
|
|
83
131
|
* Calculate variation delta for given coordinates
|
|
84
132
|
*/
|
|
85
133
|
export declare function calculateVariationDelta(vstore: ItemVariationStore, outerIndex: number, innerIndex: number, normalizedCoords: number[]): number;
|
|
134
|
+
export declare const __testing: {
|
|
135
|
+
parseIndex: typeof parseIndex;
|
|
136
|
+
readOffset: typeof readOffset;
|
|
137
|
+
parseDict: typeof parseDict;
|
|
138
|
+
parseReal: typeof parseReal;
|
|
139
|
+
parseCff2TopDict: typeof parseCff2TopDict;
|
|
140
|
+
parseCff2FDDict: typeof parseCff2FDDict;
|
|
141
|
+
parseCff2PrivateDict: typeof parseCff2PrivateDict;
|
|
142
|
+
deltaToAbsolute: typeof deltaToAbsolute;
|
|
143
|
+
parseFDSelect: typeof parseFDSelect;
|
|
144
|
+
parseItemVariationStore: typeof parseItemVariationStore;
|
|
145
|
+
parseVariationRegionList: typeof parseVariationRegionList;
|
|
146
|
+
parseItemVariationData: typeof parseItemVariationData;
|
|
147
|
+
};
|
|
148
|
+
export {};
|
|
@@ -134,6 +134,14 @@ export interface GposTable {
|
|
|
134
134
|
}
|
|
135
135
|
export type { Anchor, MarkArray } from "./gpos-mark.ts";
|
|
136
136
|
export declare function parseGpos(reader: Reader): GposTable;
|
|
137
|
+
declare function parseGposLookup(reader: Reader): AnyGposLookup | null;
|
|
138
|
+
declare function parseValueRecord(reader: Reader, valueFormat: uint16, subtableReader?: Reader): ValueRecord;
|
|
139
|
+
declare function parseSinglePos(reader: Reader, subtableOffsets: number[]): SinglePosSubtable[];
|
|
140
|
+
declare function parsePairPos(reader: Reader, subtableOffsets: number[]): PairPosSubtable[];
|
|
141
|
+
declare function parseExtensionLookup(reader: Reader, subtableOffsets: number[], baseProps: {
|
|
142
|
+
flag: uint16;
|
|
143
|
+
markFilteringSet?: uint16;
|
|
144
|
+
}): AnyGposLookup | null;
|
|
137
145
|
export declare function getKerning(lookup: PairPosLookup, firstGlyph: GlyphId, secondGlyph: GlyphId): {
|
|
138
146
|
xAdvance1: number;
|
|
139
147
|
xAdvance2: number;
|
|
@@ -143,3 +151,10 @@ export declare function getKerning(lookup: PairPosLookup, firstGlyph: GlyphId, s
|
|
|
143
151
|
* Returns true if kerning was applied, false otherwise.
|
|
144
152
|
*/
|
|
145
153
|
export declare function applyKerningDirect(lookup: PairPosLookup, firstGlyph: GlyphId, secondGlyph: GlyphId, pos1: GlyphPosition, pos2: GlyphPosition): boolean;
|
|
154
|
+
export declare const __testing: {
|
|
155
|
+
parseGposLookup: typeof parseGposLookup;
|
|
156
|
+
parseExtensionLookup: typeof parseExtensionLookup;
|
|
157
|
+
parseSinglePos: typeof parseSinglePos;
|
|
158
|
+
parsePairPos: typeof parsePairPos;
|
|
159
|
+
parseValueRecord: typeof parseValueRecord;
|
|
160
|
+
};
|