Harbor

Changelog b6ff6bd8a9f8

import font library

@sky · 1 month ago
22 added 0 modified 0 deleted
.github/copilot-instructions.md +70 -0 added
1 + # Copilot Instructions — Font
2 +
3 + ## Build & Check
4 +
5 + ```bash
6 + # Type-check the package (it's a library, no entry point)
7 + odin check . -no-entry-point
8 +
9 + # Type-check with vet warnings (stricter)
10 + odin check . -vet -no-entry-point
11 +
12 + # Run oracle comparison tests
13 + ./tests/run_tests.sh
14 +
15 + # Build and run a single test (e.g. the harness)
16 + odin build tests/harness -out:tests/harness/test_harness
17 + ./tests/harness/test_harness tests/fonts/AdwaitaSans-Regular.ttf
18 + ```
19 +
20 + ## Architecture
21 +
22 + An Odin library in the `font` package, split across per-concern files. Parses and rasterizes TrueType (.ttf) and OpenType/CFF (.otf) fonts including variable fonts. Built from stb_truetype's design with coverage-accumulation rasterizer, Raph Levien's analytical Bézier flattening, and multi-channel SDF generation.
23 +
24 + ### Source files
25 +
26 + - `types.odin` — structs, constants, inline big-endian readers
27 + - `parser.odin` — font init, table lookup, CFF/buf utilities, cmap, metrics
28 + - `shapes.odin` — glyph shape extraction (TrueType contours + CFF charstrings)
29 + - `raster.odin` — curve flattening, scanline rasterizer, bitmap API
30 + - `sdf.odin` — signed distance field generation
31 + - `msdf.odin` — multi-channel SDF with edge coloring
32 + - `svg.odin` — SVG glyph lookup
33 + - `packing.odin` — font atlas packing, baking, oversampling
34 + - `kern.odin` — kerning (kern table + GPOS pair adjustment)
35 + - `gsub.odin` — OpenType GSUB glyph substitution (types 1/2/4/6/7)
36 + - `layout.odin` — text layout engine (positioning, wrapping, alignment)
37 + - `variations.odin` — variable font support (fvar/avar axis parsing)
38 + - `gvar.odin` — glyph variation deltas for variable fonts
39 + - `autohint.odin` — lightweight metrics-driven grid-fitting
40 + - `color.odin` — COLR/CPAL color font support
41 + - `os2_post.odin` — OS/2 metrics (weight, width, x-height) and post glyph names
42 + - `names.odin` — font name API, UTF conversion, font matching
43 +
44 + ### Rendering pipeline
45 +
46 + 1. **Font init** — `init_font` parses font tables, caches offsets, detects CFF vs TrueType (stored in `is_cff` flag for dispatch throughout)
47 + 2. **Glyph lookup** — `find_glyph_index` maps Unicode codepoints → glyph indices via cmap table formats (0/2/4/6/10/12/13)
48 + 3. **GSUB** — `apply_gsub_default` / `apply_gsub_feature` apply glyph substitutions (ligatures, contextual alternates, etc.)
49 + 4. **Shape extraction** — `get_glyph_shape_tt` (TrueType contours) or `get_glyph_shape_t2`/`run_charstring` (CFF CharString interpreter) produce `Vertex` arrays
50 + 5. **Curve flattening** — `flatten_curves` dispatches to `tesselate_curve` (quadratic, uses Raph Levien analytical approach) and `tesselate_cubic` (cubic, recursive subdivision). The analytical path maps curves to canonical parabola form via `map_to_basic`
51 + 6. **Rasterization** — `rasterize` → `rasterize_sorted_edges` uses scanline active-edge with anti-aliasing
52 + 7. **SDF** — `get_glyph_sdf` generates signed distance fields via ray-intersection against Bézier curves
53 + 8. **Text layout** — `layout_text` positions glyphs with metrics, kerning, wrapping, and alignment
54 +
55 + ### API layering
56 +
57 + Most functionality has two levels: a `get_codepoint_*` wrapper that calls `find_glyph_index` then delegates to the corresponding `get_glyph_*` function. When adding new features, follow this pattern.
58 +
59 + ### Reference sources
60 +
61 + `ref/` contains cleaned source-only copies of related projects for reference: stb_truetype.h, fontdue (Rust), ttf-parser (Rust), msdfgen (C++), harfbuzz (C++).
62 +
63 + ## Conventions
64 +
65 + - **Visibility**: Internal helpers use `@(private)`. Public API has no annotation.
66 + - **Performance-critical helpers**: Big-endian readers (`ttBYTE`, `ttUSHORT`, `ttULONG`, etc.) and small hot-path functions use `#force_inline`.
67 + - **Naming**: Types are `Title_Snake_Case`, procedures are `snake_case`, constants are `UPPER_SNAKE_CASE`. The `tt*` prefixed reader functions retain stb_truetype naming heritage.
68 + - **Memory**: Public allocation functions accept `allocator := context.allocator`. Use `mem.free` for cleanup. Font data is accessed via raw pointers (`[^]u8`).
69 + - **Error handling**: Functions return `bool` for success/failure or use sentinel values (e.g., negative offsets). No exceptions or error union types.
70 + - **Font data**: All font data is big-endian; the `tt*` inline helpers handle byte-order conversion.
.gitignore +35 -0 added
1 + # IDE / AI tool configs
2 + /.claude/
3 + /.memsearch/
4 +
5 + # Build artifacts
6 + *.exe
7 + *.obj
8 + *.lib
9 + *.pdb
10 + *.ilk
11 + *.bin
12 + *.o
13 +
14 + # Test outputs
15 + tests/test_output/
16 + tests/harness/test_output/
17 + tests/harness/test_harness
18 + tests/correctness/output/
19 + tests/correctness/render_fontdue/target/
20 + tests/correctness/render_fontdue/Cargo.lock
21 + tests/correctness/render_freetype
22 + tests/correctness/render_stb
23 + tests/correctness/render_odin_bin
24 + tests/correctness/render_odin_hinted_bin
25 + tests/correctness/pgm_compare
26 + tests/bench/bench_stb
27 + tests/bench/bench_odin_bin
28 + tests/bench/bench_msdfgen
29 + tests/bench/bench_fontdue_bin
30 + tests/bench/bench_fontdue/target/
31 + tests/bench/bench_fontdue/Cargo.lock
32 +
33 + # Oracle binary
34 + tests/oracle
35 + ref/
.harbor +2 -0 added
1 + [workspace]
2 + name = "font"
README.md +153 -0 added
1 + # Font
2 +
3 + A high-performance text rendering library for Odin. Parses TrueType (.ttf), OpenType/CFF (.otf), and variable fonts with bitmap rasterization, SDF, MSDF, GSUB shaping, and text layout — all in pure Odin with zero dependencies.
4 +
5 + ## Feature Comparison
6 +
7 + | Feature | Font | stb_truetype | fontdue | msdfgen | FreeType |
8 + |---------|:-------:|:------------:|:-------:|:-------:|:--------:|
9 + | TrueType outlines | ✓ | ✓ | ✓ | — | ✓ |
10 + | CFF/OTF outlines | ✓ | ✓ | ✓ | — | ✓ |
11 + | Bitmap rasterization | ✓ | ✓ | ✓ | — | ✓ |
12 + | SDF generation | ✓ | ✓ | — | ✓ | — |
13 + | MSDF generation | ✓ | — | — | ✓ | — |
14 + | MTSDF generation | ✓ | — | — | ✓ | — |
15 + | GSUB substitution | ✓ | — | — | — | — |
16 + | GPOS kerning | ✓ | partial | ✓ | — | ✓ |
17 + | Text layout | ✓ | — | ✓ | — | — |
18 + | Variable fonts | ✓ | — | — | — | ✓ |
19 + | COLR/CPAL color | ✓ | — | — | — | ✓ |
20 + | Autohinting | ✓ | — | — | — | ✓ |
21 + | Font atlas packing | ✓ | ✓ | — | ✓ | — |
22 + | OS/2 metadata | ✓ | — | — | — | ✓ |
23 + | Glyph names | ✓ | — | — | — | ✓ |
24 + | TrueType hinting VM | — | — | — | — | ✓ |
25 + | Zero dependencies | ✓ | ✓ | ✓ | — | — |
26 +
27 + ## Quick Start
28 +
29 + ```odin
30 + import tt "font"
31 +
32 + // Load a font
33 + font: tt.Font_Info
34 + tt.init_font(&font, raw_data(font_data), 0)
35 + scale := tt.scale_for_pixel_height(&font, 24.0)
36 +
37 + // Render a glyph to bitmap
38 + w, h, xoff, yoff: i32
39 + bitmap := tt.get_codepoint_bitmap(&font, scale, scale, 'A', &w, &h, &xoff, &yoff)
40 + defer tt.free_bitmap(bitmap)
41 +
42 + // Lay out a string (with kerning + GSUB)
43 + result := tt.layout_text(&font, "Hello!", {
44 + scale = scale, max_width = 400, wrap = .Word, apply_gsub = true,
45 + })
46 + defer delete(result.glyphs)
47 +
48 + // Generate MSDF for GPU text
49 + msdf := tt.get_codepoint_msdf(&font, scale, 'A', 4, 128, 32.0, &w, &h, &xoff, &yoff)
50 + defer tt.free_msdf(msdf)
51 +
52 + // Variable font at weight=700
53 + coords: tt.Var_Coords
54 + tags := [?]string{"wght"}
55 + vals := [?]f32{700}
56 + tt.set_variation(&font, &coords, tags[:], vals[:])
57 + bitmap_bold := tt.get_glyph_bitmap_var(&font, scale, scale, glyph, &coords, &w, &h, &xoff, &yoff)
58 + ```
59 +
60 + ## API Overview
61 +
62 + ### Core — Font Loading
63 +
64 + | Procedure | Purpose |
65 + |-----------|---------|
66 + | `init_font(info, data, offset)` | Initialize a font from raw bytes |
67 + | `scale_for_pixel_height(info, px)` | Get scale factor for a target pixel height |
68 + | `find_glyph_index(info, codepoint)` | Map Unicode codepoint → glyph index |
69 +
70 + ### Rendering — Bitmaps
71 +
72 + | Procedure | Purpose |
73 + |-----------|---------|
74 + | `get_codepoint_bitmap(info, sx, sy, cp, ...)` | Render a glyph to a new grayscale bitmap |
75 + | `make_codepoint_bitmap(info, buf, w, h, stride, sx, sy, cp)` | Render into an existing buffer |
76 + | `get_glyph_bitmap_hinted(info, sx, sy, gi, mode, ...)` | Render with autohinting |
77 + | `get_glyph_bitmap_var(info, sx, sy, gi, coords, ...)` | Render at specific variation coordinates |
78 + | `free_bitmap(bmp)` | Free a rendered bitmap |
79 +
80 + ### Rendering — Distance Fields
81 +
82 + | Procedure | Purpose |
83 + |-----------|---------|
84 + | `get_codepoint_sdf(info, scale, cp, pad, ...)` | Single-channel signed distance field |
85 + | `get_codepoint_msdf(info, scale, cp, pad, ...)` | Multi-channel SDF (3 channels, sharp corners) |
86 + | `get_codepoint_mtsdf(info, scale, cp, pad, ...)` | Multi-channel + true distance (4 channels) |
87 +
88 + ### Text — Layout & Shaping
89 +
90 + | Procedure | Purpose |
91 + |-----------|---------|
92 + | `layout_text(info, text, settings)` | Position glyphs with wrapping, alignment, kerning |
93 + | `apply_gsub_default(info, glyphs)` | Apply default GSUB substitutions |
94 + | `apply_gsub_feature(info, tag, glyphs)` | Apply a specific feature (e.g. "liga") |
95 + | `get_codepoint_kern_advance(info, cp1, cp2)` | Get kerning between two codepoints |
96 +
97 + ### Metrics
98 +
99 + | Procedure | Purpose |
100 + |-----------|---------|
101 + | `get_font_vmetrics(info, &asc, &desc, &gap)` | Ascent, descent, line gap |
102 + | `get_codepoint_hmetrics(info, cp, &adv, &lsb)` | Advance width, left side bearing |
103 + | `get_codepoint_box(info, cp, &x0, &y0, &x1, &y1)` | Glyph bounding box |
104 + | `get_os2_metrics(info, &os2)` | Weight, width, x-height, cap-height |
105 + | `get_glyph_name(info, gi)` | PostScript glyph name |
106 +
107 + ### Variable Fonts
108 +
109 + | Procedure | Purpose |
110 + |-----------|---------|
111 + | `is_variable_font(info)` | Check if font has variation axes |
112 + | `get_variation_axes(info, axes)` | Enumerate axes (weight, width, etc.) |
113 + | `set_variation(info, coords, tags, values)` | Set axis values with avar remapping |
114 + | `get_glyph_hmetrics_var(info, gi, coords, &adv, &lsb)` | Metrics at specific axes |
115 +
116 + ### Atlas Packing
117 +
118 + | Procedure | Purpose |
119 + |-----------|---------|
120 + | `pack_begin(ctx, pixels, w, h, stride, pad)` | Start packing glyphs into atlas |
121 + | `pack_font_ranges(ctx, data, index, ranges, n)` | Pack character ranges |
122 + | `pack_end(ctx)` | Finish packing |
123 + | `get_packed_quad(chardata, w, h, idx, &x, &y, &q, align)` | Get UV coords for rendering |
124 +
125 + ## Performance
126 +
127 + Benchmarked against stb_truetype (C), fontdue (Rust), and msdfgen (C++) on AdwaitaSans-Regular.ttf:
128 +
129 + | Operation | stb (C) | **Font** | fontdue (Rust) | msdfgen (C++) |
130 + |-----------|--------:|--------:|--------:|--------:|
131 + | init | 46 ns | **48 ns** | 11M ns | — |
132 + | cmap lookup | 10.8 ns | **5.5 ns** | 2.2 ns | — |
133 + | glyph shape | 59 ns | **6 ns** | — | 1,074 ns |
134 + | bitmap | **1,146 ns** | 2,669 ns | **941 ns** | — |
135 + | SDF | 76,862 ns | **67,684 ns** | — | 106,242 ns |
136 + | MSDF | — | **242,689 ns** | — | 803,245 ns |
137 + | kerning | 64 ns | **19 ns** | 4.2 ns | — |
138 + | var bitmap | — | **2,867 ns** | — | — |
139 +
140 + ## Testing
141 +
142 + Tests and benchmarks are on the `testing` branch:
143 +
144 + ```bash
145 + git checkout testing
146 + ./tests/run_tests.sh # Oracle comparison vs stb_truetype
147 + ./tests/bench/run_bench.sh # Four-way performance benchmark
148 + ./tests/correctness/run_correctness.sh # Pixel-level comparison vs FreeType
149 + ```
150 +
151 + ## License
152 +
153 + Sharkk Minimal License — see [license.md](license.md)
autohint.odin +227 -0 added
1 + package font
2 +
3 + import "core:math"
4 + import "core:mem"
5 +
6 + // ============================================================================
7 + // AUTOHINTER — Lightweight grid-fitting without TrueType bytecode
8 + // ============================================================================
9 +
10 + // Hint mode controls the aggressiveness of grid-fitting
11 + Hint_Mode :: enum {
12 + None, // No hinting (default, best for high-DPI)
13 + Light, // Vertical only — snap Y stems to pixel grid, preserve X
14 + Full, // Vertical + horizontal stem snapping
15 + }
16 +
17 + // Autohint settings
18 + Hint_Settings :: struct {
19 + mode: Hint_Mode,
20 + ppem: f32, // pixels per em (derived from scale)
21 + }
22 +
23 + // Apply autohinting to glyph vertices in-place.
24 + // Modifies vertex Y coordinates (and optionally X) to align with pixel grid.
25 + // scale is the scale_for_pixel_height value.
26 + autohint_glyph :: proc(
27 + info: ^Font_Info,
28 + vertices: [^]Vertex,
29 + num_verts: i32,
30 + scale: f32,
31 + mode: Hint_Mode,
32 + ) {
33 + if mode == .None || num_verts <= 0 do return
34 +
35 + // Get key vertical metrics in font units
36 + ascent, descent, line_gap: i32
37 + get_font_vmetrics(info, &ascent, &descent, &line_gap)
38 +
39 + // Get OS/2 metrics for x-height and cap-height
40 + x_height: i32 = 0
41 + cap_height: i32 = 0
42 + os2: OS2_Metrics
43 + if get_os2_metrics(info, &os2) {
44 + x_height = i32(os2.x_height)
45 + cap_height = i32(os2.cap_height)
46 + }
47 +
48 + // Define alignment zones (font-unit Y values → snapped pixel Y)
49 + zones: [6]Hint_Zone
50 + zone_count: i32 = 0
51 +
52 + // Baseline at 0
53 + zones[zone_count] = make_zone(0, scale)
54 + zone_count += 1
55 +
56 + // Ascender
57 + if ascent != 0 {
58 + zones[zone_count] = make_zone(ascent, scale)
59 + zone_count += 1
60 + }
61 +
62 + // Descender
63 + if descent != 0 {
64 + zones[zone_count] = make_zone(descent, scale)
65 + zone_count += 1
66 + }
67 +
68 + // Cap height
69 + if cap_height > 0 {
70 + zones[zone_count] = make_zone(cap_height, scale)
71 + zone_count += 1
72 + }
73 +
74 + // x-height
75 + if x_height > 0 {
76 + zones[zone_count] = make_zone(x_height, scale)
77 + zone_count += 1
78 + }
79 +
80 + // Apply Y-direction grid fitting
81 + for i in 0..<num_verts {
82 + v := &vertices[i]
83 + fy := f32(v.y)
84 +
85 + // Snap to nearest alignment zone
86 + snapped := snap_to_zone(fy, zones[:zone_count], scale)
87 + v.y = i16(snapped)
88 +
89 + // For curves, also snap control points proportionally
90 + if v.type == VCURVE || v.type == VCUBIC {
91 + fcy := f32(v.cy)
92 + v.cy = i16(snap_to_zone(fcy, zones[:zone_count], scale))
93 + }
94 + if v.type == VCUBIC {
95 + fcy1 := f32(v.cy1)
96 + v.cy1 = i16(snap_to_zone(fcy1, zones[:zone_count], scale))
97 + }
98 +
99 + // Full mode: also regularize X stems
100 + if mode == .Full {
101 + v.x = i16(snap_stem_x(f32(v.x), scale))
102 + if v.type == VCURVE || v.type == VCUBIC {
103 + v.cx = i16(snap_stem_x(f32(v.cx), scale))
104 + }
105 + if v.type == VCUBIC {
106 + v.cx1 = i16(snap_stem_x(f32(v.cx1), scale))
107 + }
108 + }
109 + }
110 + }
111 +
112 + // Render a hinted glyph bitmap
113 + get_glyph_bitmap_hinted :: proc(
114 + info: ^Font_Info,
115 + scale_x: f32, scale_y: f32,
116 + glyph: i32,
117 + mode: Hint_Mode,
118 + width: ^i32, height: ^i32, xoff: ^i32, yoff: ^i32,
119 + allocator := context.allocator,
120 + ) -> [^]u8 {
121 + arena_backing: [256 * 1024]u8
122 + arena: mem.Arena
123 + mem.arena_init(&arena, arena_backing[:])
124 + context.allocator = mem.arena_allocator(&arena)
125 +
126 + vertices_temp: ^Vertex = nil
127 + // Get uncached shape (we'll modify it)
128 + nv: i32
129 + if info.is_cff {
130 + nv = get_glyph_shape_t2(info, glyph, cast(^^Vertex)&vertices_temp)
131 + } else {
132 + nv = get_glyph_shape_tt(info, glyph, cast(^^Vertex)&vertices_temp)
133 + }
134 + vertices := ([^]Vertex)(vertices_temp)
135 +
136 + if nv <= 0 || vertices == nil {
137 + if width != nil do width^ = 0
138 + if height != nil do height^ = 0
139 + return nil
140 + }
141 +
142 + // Apply autohinting
143 + autohint_glyph(info, vertices, nv, scale_y, mode)
144 +
145 + scale_x := scale_x; scale_y := scale_y
146 + if scale_x == 0 do scale_x = scale_y
147 + if scale_y == 0 { if scale_x == 0 do return nil; scale_y = scale_x }
148 +
149 + ix0, iy0, ix1, iy1: i32
150 + get_glyph_bitmap_box_subpixel(info, glyph, scale_x, scale_y, 0, 0, &ix0, &iy0, &ix1, &iy1)
151 +
152 + gbm: Bitmap
153 + gbm.w = ix1 - ix0; gbm.h = iy1 - iy0
154 + if width != nil do width^ = gbm.w
155 + if height != nil do height^ = gbm.h
156 + if xoff != nil do xoff^ = ix0
157 + if yoff != nil do yoff^ = iy0
158 +
159 + if gbm.w != 0 && gbm.h != 0 {
160 + ptr, _ := mem.alloc(int(gbm.w * gbm.h), allocator = allocator)
161 + gbm.pixels = ([^]u8)(ptr)
162 + if gbm.pixels != nil {
163 + gbm.stride = gbm.w
164 + rasterize_glyph(&gbm, 0.35, vertices, nv, scale_x, scale_y, 0, 0, ix0, iy0, 1)
165 + }
166 + }
167 + return gbm.pixels
168 + }
169 +
170 + get_codepoint_bitmap_hinted :: proc(
171 + info: ^Font_Info, scale: f32, codepoint: i32, mode: Hint_Mode,
172 + width: ^i32, height: ^i32, xoff: ^i32, yoff: ^i32,
173 + allocator := context.allocator,
174 + ) -> [^]u8 {
175 + return get_glyph_bitmap_hinted(info, scale, scale, find_glyph_index(info, codepoint),
176 + mode, width, height, xoff, yoff, allocator)
177 + }
178 +
179 + // ============================================================================
180 + // Internal
181 + // ============================================================================
182 +
183 + @(private)
184 + Hint_Zone :: struct {
185 + font_y: f32, // Y coordinate in font units
186 + pixel_y: f32, // Snapped pixel Y (rounded font_y * scale)
187 + }
188 +
189 + @(private)
190 + make_zone :: proc(font_y: i32, scale: f32) -> Hint_Zone {
191 + pixel_y := f32(font_y) * scale
192 + return {
193 + font_y = f32(font_y),
194 + pixel_y = math.round(pixel_y),
195 + }
196 + }
197 +
198 + // Snap a font-unit Y coordinate to the nearest alignment zone
199 + @(private)
200 + snap_to_zone :: proc(font_y: f32, zones: []Hint_Zone, scale: f32) -> f32 {
201 + // Zone capture radius in font units
202 + radius := 1.0 / scale * 0.5 // half-pixel in font units
203 +
204 + best_dist := radius
205 + result := font_y
206 +
207 + for z in zones {
208 + dist := abs(font_y - z.font_y)
209 + if dist < best_dist {
210 + best_dist = dist
211 + // Shift this coordinate by the same delta the zone was shifted
212 + delta := z.pixel_y - z.font_y * scale
213 + result = font_y + delta / scale
214 + }
215 + }
216 +
217 + return result
218 + }
219 +
220 + // Snap X coordinate to half-pixel grid for consistent stem widths
221 + @(private)
222 + snap_stem_x :: proc(font_x: f32, scale: f32) -> f32 {
223 + px := font_x * scale
224 + // Snap to half-pixel boundaries for stem consistency
225 + snapped := math.round(px * 2) / 2
226 + return snapped / scale
227 + }
color.odin +145 -0 added
1 + package font
2 +
3 + // ============================================================================
4 + // COLR/CPAL — Color Fonts
5 + // ============================================================================
6 +
7 + // RGBA color
8 + RGBA_Color :: struct {
9 + r, g, b, a: u8,
10 + }
11 +
12 + // A color layer in a COLR v0 glyph
13 + Color_Layer :: struct {
14 + glyph_index: i32,
15 + palette_entry: u16,
16 + }
17 +
18 + // Get the number of color palettes in the font (from CPAL table)
19 + get_color_palette_count :: proc(info: ^Font_Info) -> i32 {
20 + cpal := find_table(info.data, u32(info.fontstart), "CPAL")
21 + if cpal == 0 do return 0
22 +
23 + data := info.data[cpal:]
24 + version := ttUSHORT(data)
25 + if version > 1 do return 0
26 +
27 + return i32(ttUSHORT(data[4:]))
28 + }
29 +
30 + // Get a color from a palette. Returns false if indices are out of range.
31 + get_color_from_palette :: proc(info: ^Font_Info, palette_index: i32, entry_index: i32, color: ^RGBA_Color) -> bool {
32 + cpal := find_table(info.data, u32(info.fontstart), "CPAL")
33 + if cpal == 0 do return false
34 +
35 + data := info.data[cpal:]
36 + version := ttUSHORT(data)
37 + if version > 1 do return false
38 +
39 + num_entries := i32(ttUSHORT(data[2:]))
40 + num_palettes := i32(ttUSHORT(data[4:]))
41 + // num_colors := i32(ttUSHORT(data[6:]))
42 + color_records_offset := u32(ttULONG(data[8:]))
43 +
44 + if palette_index < 0 || palette_index >= num_palettes do return false
45 + if entry_index < 0 || entry_index >= num_entries do return false
46 +
47 + // Color indices array starts at offset 12
48 + first_color_index := i32(ttUSHORT(data[12 + u32(palette_index) * 2:]))
49 + color_idx := first_color_index + entry_index
50 +
51 + // Color records are BGRA (4 bytes each)
52 + rec := data[color_records_offset + u32(color_idx) * 4:]
53 + color.b = rec[0]
54 + color.g = rec[1]
55 + color.r = rec[2]
56 + color.a = rec[3]
57 + return true
58 + }
59 +
60 + // Get color layers for a glyph (COLR v0).
61 + // Returns the number of layers, or 0 if the glyph has no color layers.
62 + // Layers are written to the provided slice.
63 + get_glyph_color_layers :: proc(info: ^Font_Info, glyph_index: i32, layers: []Color_Layer) -> i32 {
64 + colr := find_table(info.data, u32(info.fontstart), "COLR")
65 + if colr == 0 do return 0
66 +
67 + data := info.data[colr:]
68 + version := ttUSHORT(data)
69 +
70 + // We support COLR v0 (layered glyphs)
71 + if version > 1 do return 0
72 +
73 + num_base_glyphs := i32(ttUSHORT(data[2:]))
74 + base_glyph_offset := u32(ttULONG(data[4:]))
75 + layer_records_offset := u32(ttULONG(data[8:]))
76 + // num_layer_records := i32(ttUSHORT(data[12:]))
77 +
78 + // Binary search for the glyph in base glyph records
79 + base_glyphs := data[base_glyph_offset:]
80 + l: i32 = 0
81 + r := num_base_glyphs - 1
82 +
83 + for l <= r {
84 + m := (l + r) >> 1
85 + rec := base_glyphs[m * 6:]
86 + gid := i32(ttUSHORT(rec))
87 + if glyph_index < gid {
88 + r = m - 1
89 + } else if glyph_index > gid {
90 + l = m + 1
91 + } else {
92 + first_layer := i32(ttUSHORT(rec[2:]))
93 + num_layers := i32(ttUSHORT(rec[4:]))
94 +
95 + n := min(num_layers, i32(len(layers)))
96 + layer_data := data[layer_records_offset:]
97 + for i in 0..<n {
98 + layer_rec := layer_data[(first_layer + i) * 4:]
99 + layers[i] = {
100 + glyph_index = i32(ttUSHORT(layer_rec)),
101 + palette_entry = ttUSHORT(layer_rec[2:]),
102 + }
103 + }
104 + return num_layers
105 + }
106 + }
107 +
108 + return 0
109 + }
110 +
111 + // Check if a glyph has color layers
112 + is_color_glyph :: proc(info: ^Font_Info, glyph_index: i32) -> bool {
113 + // Check COLR
114 + colr := find_table(info.data, u32(info.fontstart), "COLR")
115 + if colr != 0 {
116 + data := info.data[colr:]
117 + num_base := i32(ttUSHORT(data[2:]))
118 + base_off := u32(ttULONG(data[4:]))
119 + base_glyphs := data[base_off:]
120 +
121 + l: i32 = 0
122 + r := num_base - 1
123 + for l <= r {
124 + m := (l + r) >> 1
125 + gid := i32(ttUSHORT(base_glyphs[m * 6:]))
126 + if glyph_index < gid {
127 + r = m - 1
128 + } else if glyph_index > gid {
129 + l = m + 1
130 + } else {
131 + return true
132 + }
133 + }
134 + }
135 +
136 + // Also check SVG
137 + if info.svg != 0 {
138 + svg_data: [^]u8
139 + if get_glyph_svg(info, glyph_index, &svg_data) > 0 {
140 + return true
141 + }
142 + }
143 +
144 + return false
145 + }
gsub.odin +435 -0 added
1 + package font
2 +
3 + // ============================================================================
4 + // GSUB — OpenType Glyph Substitution
5 + // ============================================================================
6 +
7 + // Substitution result for a single glyph position
8 + GSUB_Subst :: struct {
9 + glyph: i32, // replacement glyph ID
10 + skip: i32, // number of additional input glyphs consumed (for ligatures)
11 + }
12 +
13 + // Apply all default GSUB substitutions to a glyph buffer.
14 + // Processes the default script/language with all default features.
15 + // Modifies glyphs in-place. Returns the new length (may shrink due to ligatures).
16 + apply_gsub_default :: proc(info: ^Font_Info, glyphs: []i32) -> i32 {
17 + if info.gsub == 0 do return i32(len(glyphs))
18 + data := info.data
19 + gsub := u32(info.gsub)
20 +
21 + // Validate GSUB header
22 + major := ttUSHORT(data[gsub:])
23 + if major != 1 do return i32(len(glyphs))
24 +
25 + script_list := gsub + u32(ttUSHORT(data[gsub + 4:]))
26 + feature_list := gsub + u32(ttUSHORT(data[gsub + 6:]))
27 + lookup_list := gsub + u32(ttUSHORT(data[gsub + 8:]))
28 +
29 + // Find default LangSys: try "DFLT" then "latn" scripts, then first script
30 + lang_sys := gsub_find_default_langsys(data, script_list)
31 + if lang_sys == 0 do return i32(len(glyphs))
32 +
33 + // Collect lookup indices from all features referenced by the default LangSys
34 + feature_count := i32(ttUSHORT(data[lang_sys + 4:]))
35 + feature_indices := data[lang_sys + 6:]
36 +
37 + // Process required feature if present
38 + req_feature := i32(ttUSHORT(data[lang_sys + 2:]))
39 +
40 + // Work buffer — copy input glyphs
41 + buf: [1024]i32
42 + n := min(i32(len(glyphs)), 1024)
43 + for i in 0..<n {
44 + buf[i] = glyphs[i]
45 + }
46 +
47 + // Apply required feature
48 + if req_feature != 0xFFFF {
49 + n = gsub_apply_feature(info, data, gsub, feature_list, lookup_list, req_feature, buf[:n])
50 + }
51 +
52 + // Apply each default feature
53 + for fi in 0..<feature_count {
54 + feat_idx := i32(ttUSHORT(feature_indices[fi * 2:]))
55 + n = gsub_apply_feature(info, data, gsub, feature_list, lookup_list, feat_idx, buf[:n])
56 + }
57 +
58 + // Write back
59 + for i in 0..<n {
60 + glyphs[i] = buf[i]
61 + }
62 + return n
63 + }
64 +
65 + // Apply a specific GSUB feature tag (e.g. "liga", "smcp") to a glyph buffer.
66 + // Returns the new glyph count.
67 + apply_gsub_feature :: proc(info: ^Font_Info, tag: string, glyphs: []i32) -> i32 {
68 + if info.gsub == 0 || len(tag) != 4 do return i32(len(glyphs))
69 + data := info.data
70 + gsub := u32(info.gsub)
71 +
72 + major := ttUSHORT(data[gsub:])
73 + if major != 1 do return i32(len(glyphs))
74 +
75 + feature_list := gsub + u32(ttUSHORT(data[gsub + 6:]))
76 + lookup_list := gsub + u32(ttUSHORT(data[gsub + 8:]))
77 +
78 + // Find the feature by tag
79 + feat_count := i32(ttUSHORT(data[feature_list:]))
80 + feat_records := data[feature_list + 2:]
81 +
82 + buf: [1024]i32
83 + n := min(i32(len(glyphs)), 1024)
84 + for i in 0..<n {
85 + buf[i] = glyphs[i]
86 + }
87 +
88 + for fi in 0..<feat_count {
89 + rec := feat_records[fi * 6:]
90 + if rec[0] == tag[0] && rec[1] == tag[1] && rec[2] == tag[2] && rec[3] == tag[3] {
91 + n = gsub_apply_feature(info, data, gsub, feature_list, lookup_list, fi, buf[:n])
92 + }
93 + }
94 +
95 + for i in 0..<n {
96 + glyphs[i] = buf[i]
97 + }
98 + return n
99 + }
100 +
101 + // ============================================================================
102 + // Internal helpers
103 + // ============================================================================
104 +
105 + @(private)
106 + gsub_find_default_langsys :: proc(data: [^]u8, script_list: u32) -> u32 {
107 + script_count := i32(ttUSHORT(data[script_list:]))
108 + records := data[script_list + 2:]
109 +
110 + // Priority: "DFLT", "latn", first available
111 + try_tags := [?]string{"DFLT", "latn"}
112 +
113 + for t in try_tags {
114 + for si in 0..<script_count {
115 + rec := records[si * 6:]
116 + if rec[0] == t[0] && rec[1] == t[1] && rec[2] == t[2] && rec[3] == t[3] {
117 + script_offset := script_list + u32(ttUSHORT(rec[4:]))
118 + default_lang_sys_offset := u32(ttUSHORT(data[script_offset:]))
119 + if default_lang_sys_offset != 0 {
120 + return script_offset + default_lang_sys_offset
121 + }
122 + }
123 + }
124 + }
125 +
126 + // Fallback: first script's default langsys
127 + if script_count > 0 {
128 + script_offset := script_list + u32(ttUSHORT(records[4:]))
129 + default_lang_sys_offset := u32(ttUSHORT(data[script_offset:]))
130 + if default_lang_sys_offset != 0 {
131 + return script_offset + default_lang_sys_offset
132 + }
133 + }
134 +
135 + return 0
136 + }
137 +
138 + @(private)
139 + gsub_apply_feature :: proc(info: ^Font_Info, data: [^]u8, gsub: u32, feature_list: u32, lookup_list: u32, feat_idx: i32, glyphs: []i32) -> i32 {
140 + feat_count := i32(ttUSHORT(data[feature_list:]))
141 + if feat_idx < 0 || feat_idx >= feat_count do return i32(len(glyphs))
142 +
143 + feat_records := data[feature_list + 2:]
144 + feat_offset := feature_list + u32(ttUSHORT(feat_records[feat_idx * 6 + 4:]))
145 +
146 + lookup_count := i32(ttUSHORT(data[feat_offset + 2:]))
147 + lookup_indices := data[feat_offset + 4:]
148 +
149 + n := i32(len(glyphs))
150 + for li in 0..<lookup_count {
151 + lookup_idx := i32(ttUSHORT(lookup_indices[li * 2:]))
152 + n = gsub_apply_lookup(info, data, gsub, lookup_list, lookup_idx, glyphs[:n])
153 + }
154 + return n
155 + }
156 +
157 + @(private)
158 + gsub_apply_lookup :: proc(info: ^Font_Info, data: [^]u8, gsub: u32, lookup_list: u32, lookup_idx: i32, glyphs: []i32) -> i32 {
159 + lookup_count := i32(ttUSHORT(data[lookup_list:]))
160 + if lookup_idx < 0 || lookup_idx >= lookup_count do return i32(len(glyphs))
161 +
162 + lookup_offset := lookup_list + u32(ttUSHORT(data[lookup_list + 2 + u32(lookup_idx) * 2:]))
163 + lookup_type := i32(ttUSHORT(data[lookup_offset:]))
164 + // lookup_flag := ttUSHORT(data[lookup_offset + 2:])
165 + subtable_count := i32(ttUSHORT(data[lookup_offset + 4:]))
166 +
167 + n := i32(len(glyphs))
168 +
169 + for sti in 0..<subtable_count {
170 + subtable_off := lookup_offset + u32(ttUSHORT(data[lookup_offset + 6 + u32(sti) * 2:]))
171 + actual_type := lookup_type
172 + actual_off := subtable_off
173 +
174 + // Handle Extension Substitution (Type 7) — unwrap to actual subtable
175 + if lookup_type == 7 {
176 + ext_format := ttUSHORT(data[subtable_off:])
177 + if ext_format == 1 {
178 + actual_type = i32(ttUSHORT(data[subtable_off + 2:]))
179 + actual_off = subtable_off + u32(ttULONG(data[subtable_off + 4:]))
180 + } else {
181 + continue
182 + }
183 + }
184 +
185 + switch actual_type {
186 + case 1:
187 + n = gsub_single_subst(data, actual_off, glyphs[:n])
188 + case 2:
189 + n = gsub_multiple_subst(data, actual_off, glyphs[:n])
190 + case 4:
191 + n = gsub_ligature_subst(data, actual_off, glyphs[:n])
192 + case 6:
193 + n = gsub_chaining_context_subst(info, data, gsub, actual_off, glyphs[:n])
194 + }
195 + }
196 + return n
197 + }
198 +
199 + // Lookup Type 1: Single Substitution (1:1 replacement)
200 + @(private)
201 + gsub_single_subst :: proc(data: [^]u8, subtable: u32, glyphs: []i32) -> i32 {
202 + format := ttUSHORT(data[subtable:])
203 + coverage_off := subtable + u32(ttUSHORT(data[subtable + 2:]))
204 +
205 + switch format {
206 + case 1: // Delta
207 + delta := i32(ttSHORT(data[subtable + 4:]))
208 + for i in 0..<len(glyphs) {
209 + ci := get_coverage_index(data[coverage_off:], glyphs[i])
210 + if ci >= 0 {
211 + glyphs[i] = (glyphs[i] + delta) & 0xFFFF
212 + }
213 + }
214 + case 2: // Substitute array
215 + glyph_count := i32(ttUSHORT(data[subtable + 4:]))
216 + subst_array := data[subtable + 6:]
217 + for i in 0..<len(glyphs) {
218 + ci := get_coverage_index(data[coverage_off:], glyphs[i])
219 + if ci >= 0 && ci < glyph_count {
220 + glyphs[i] = i32(ttUSHORT(subst_array[ci * 2:]))
221 + }
222 + }
223 + }
224 + return i32(len(glyphs))
225 + }
226 +
227 + // Lookup Type 2: Multiple Substitution (1:M replacement)
228 + @(private)
229 + gsub_multiple_subst :: proc(data: [^]u8, subtable: u32, glyphs: []i32) -> i32 {
230 + format := ttUSHORT(data[subtable:])
231 + if format != 1 do return i32(len(glyphs))
232 +
233 + coverage_off := subtable + u32(ttUSHORT(data[subtable + 2:]))
234 + seq_count := i32(ttUSHORT(data[subtable + 4:]))
235 + seq_offsets := data[subtable + 6:]
236 +
237 + n := i32(len(glyphs))
238 + i: i32 = 0
239 + for i < n {
240 + ci := get_coverage_index(data[coverage_off:], glyphs[i])
241 + if ci >= 0 && ci < seq_count {
242 + seq_off := subtable + u32(ttUSHORT(seq_offsets[ci * 2:]))
243 + replace_count := i32(ttUSHORT(data[seq_off:]))
244 + replace_glyphs := data[seq_off + 2:]
245 +
246 + if replace_count == 1 {
247 + // Simple 1:1, just replace in place
248 + glyphs[i] = i32(ttUSHORT(replace_glyphs))
249 + } else if replace_count == 0 {
250 + // Delete glyph
251 + for j in i..<n-1 {
252 + glyphs[j] = glyphs[j+1]
253 + }
254 + n -= 1
255 + continue // don't advance i
256 + } else if replace_count > 1 && n + replace_count - 1 <= i32(len(glyphs)) {
257 + // Expand: shift right to make room
258 + shift := replace_count - 1
259 + for j := n - 1; j > i; j -= 1 {
260 + glyphs[j + shift] = glyphs[j]
261 + }
262 + for ri in 0..<replace_count {
263 + glyphs[i + ri] = i32(ttUSHORT(replace_glyphs[ri * 2:]))
264 + }
265 + n += shift
266 + i += replace_count
267 + continue
268 + }
269 + }
270 + i += 1
271 + }
272 + return n
273 + }
274 +
275 + // Lookup Type 4: Ligature Substitution (N:1 replacement)
276 + @(private)
277 + gsub_ligature_subst :: proc(data: [^]u8, subtable: u32, glyphs: []i32) -> i32 {
278 + format := ttUSHORT(data[subtable:])
279 + if format != 1 do return i32(len(glyphs))
280 +
281 + coverage_off := subtable + u32(ttUSHORT(data[subtable + 2:]))
282 + lig_set_count := i32(ttUSHORT(data[subtable + 4:]))
283 + lig_set_offsets := data[subtable + 6:]
284 +
285 + n := i32(len(glyphs))
286 + i: i32 = 0
287 + for i < n {
288 + ci := get_coverage_index(data[coverage_off:], glyphs[i])
289 + if ci >= 0 && ci < lig_set_count {
290 + lig_set_off := subtable + u32(ttUSHORT(lig_set_offsets[ci * 2:]))
291 + lig_count := i32(ttUSHORT(data[lig_set_off:]))
292 + lig_offsets := data[lig_set_off + 2:]
293 +
294 + matched := false
295 + for li in 0..<lig_count {
296 + lig_off := lig_set_off + u32(ttUSHORT(lig_offsets[li * 2:]))
297 + lig_glyph := i32(ttUSHORT(data[lig_off:]))
298 + comp_count := i32(ttUSHORT(data[lig_off + 2:])) // includes first glyph
299 + components := data[lig_off + 4:]
300 +
301 + // Check if remaining glyphs match the ligature components
302 + if i + comp_count > n do continue
303 +
304 + match := true
305 + for ci2 in 0..<comp_count - 1 {
306 + if glyphs[i + 1 + ci2] != i32(ttUSHORT(components[ci2 * 2:])) {
307 + match = false
308 + break
309 + }
310 + }
311 +
312 + if match {
313 + // Replace first glyph with ligature
314 + glyphs[i] = lig_glyph
315 + // Remove consumed glyphs
316 + remove_count := comp_count - 1
317 + for j := i + 1; j < n - remove_count; j += 1 {
318 + glyphs[j] = glyphs[j + remove_count]
319 + }
320 + n -= remove_count
321 + matched = true
322 + break
323 + }
324 + }
325 +
326 + if matched {
327 + i += 1
328 + continue
329 + }
330 + }
331 + i += 1
332 + }
333 + return n
334 + }
335 +
336 + // Lookup Type 6: Chaining Context Substitution
337 + @(private)
338 + gsub_chaining_context_subst :: proc(info: ^Font_Info, data: [^]u8, gsub: u32, subtable: u32, glyphs: []i32) -> i32 {
339 + format := ttUSHORT(data[subtable:])
340 + if format != 3 do return i32(len(glyphs)) // Only format 3 for now
341 +
342 + lookup_list := gsub + u32(ttUSHORT(data[gsub + 8:]))
343 + n := i32(len(glyphs))
344 +
345 + // Parse chaining context format 3
346 + off: u32 = subtable + 2
347 +
348 + // Backtrack coverages
349 + backtrack_count := i32(ttUSHORT(data[off:]))
350 + off += 2
351 + backtrack_covs: [16]u32
352 + for bi in 0..<min(backtrack_count, 16) {
353 + backtrack_covs[bi] = subtable + u32(ttUSHORT(data[off:]))
354 + off += 2
355 + }
356 + if backtrack_count > 16 do off += u32(backtrack_count - 16) * 2
357 +
358 + // Input coverages
359 + input_count := i32(ttUSHORT(data[off:]))
360 + off += 2
361 + input_covs: [16]u32
362 + for ii in 0..<min(input_count, 16) {
363 + input_covs[ii] = subtable + u32(ttUSHORT(data[off:]))
364 + off += 2
365 + }
366 + if input_count > 16 do off += u32(input_count - 16) * 2
367 +
368 + // Lookahead coverages
369 + lookahead_count := i32(ttUSHORT(data[off:]))
370 + off += 2
371 + lookahead_covs: [16]u32
372 + for li in 0..<min(lookahead_count, 16) {
373 + lookahead_covs[li] = subtable + u32(ttUSHORT(data[off:]))
374 + off += 2
375 + }
376 + if lookahead_count > 16 do off += u32(lookahead_count - 16) * 2
377 +
378 + // Substitution lookup records
379 + subst_count := i32(ttUSHORT(data[off:]))
380 + off += 2
381 + subst_records := data[off:]
382 +
383 + // Apply to glyph buffer
384 + i: i32 = 0
385 + for i < n {
386 + // Check if enough glyphs for backtrack + input + lookahead
387 + if i < backtrack_count { i += 1; continue }
388 + if i + input_count + lookahead_count > n do break
389 +
390 + // Match input coverages
391 + input_match := true
392 + for ii in 0..<input_count {
393 + if get_coverage_index(data[input_covs[ii]:], glyphs[i + ii]) < 0 {
394 + input_match = false
395 + break
396 + }
397 + }
398 + if !input_match { i += 1; continue }
399 +
400 + // Match backtrack coverages (reverse order)
401 + backtrack_match := true
402 + for bi in 0..<backtrack_count {
403 + if get_coverage_index(data[backtrack_covs[bi]:], glyphs[i - 1 - bi]) < 0 {
404 + backtrack_match = false
405 + break
406 + }
407 + }
408 + if !backtrack_match { i += 1; continue }
409 +
410 + // Match lookahead coverages
411 + lookahead_match := true
412 + for li in 0..<lookahead_count {
413 + if get_coverage_index(data[lookahead_covs[li]:], glyphs[i + input_count + li]) < 0 {
414 + lookahead_match = false
415 + break
416 + }
417 + }
418 + if !lookahead_match { i += 1; continue }
419 +
420 + // All matched — apply substitution lookups
421 + for si in 0..<subst_count {
422 + seq_idx := i32(ttUSHORT(subst_records[si * 4:]))
423 + lookup_idx := i32(ttUSHORT(subst_records[si * 4 + 2:]))
424 + target := i + seq_idx
425 + if target >= 0 && target < n {
426 + // Apply the referenced lookup to just this position
427 + one_glyph := glyphs[target:target+1]
428 + gsub_apply_lookup(info, data, gsub, lookup_list, lookup_idx, one_glyph)
429 + }
430 + }
431 +
432 + i += input_count
433 + }
434 + return n
435 + }
gvar.odin +290 -0 added
1 + package font
2 +
3 + import "core:mem"
4 + import "core:math"
5 +
6 + // ============================================================================
7 + // GVAR — Glyph Variations (TrueType outline deltas)
8 + // ============================================================================
9 +
10 + // Apply glyph variation deltas to a set of points.
11 + // Takes raw glyph points (x, y pairs as i16) and modifies them in-place
12 + // based on the normalized variation coordinates.
13 + // Returns true if deltas were applied.
14 + apply_gvar_deltas :: proc(
15 + info: ^Font_Info,
16 + glyph_index: i32,
17 + coords: ^Var_Coords,
18 + points_x: []f32,
19 + points_y: []f32,
20 + ) -> bool {
21 + gvar := find_table(info.data, u32(info.fontstart), "gvar")
22 + if gvar == 0 do return false
23 +
24 + data := info.data[gvar:]
25 + version := ttULONG(data)
26 + if version != 0x00010000 do return false
27 +
28 + axis_count := i32(ttUSHORT(data[4:]))
29 + shared_tuple_count := i32(ttUSHORT(data[6:]))
30 + shared_tuples_off := ttULONG(data[8:])
31 + glyph_count := i32(ttUSHORT(data[12:]))
32 + flags := ttUSHORT(data[14:])
33 + gvar_data_off := ttULONG(data[16:])
34 +
35 + if glyph_index < 0 || glyph_index >= glyph_count do return false
36 +
37 + long_offsets := (flags & 1) != 0
38 + num_points := i32(len(points_x))
39 +
40 + // Get offset to this glyph's variation data
41 + var_off, var_end: u32
42 + if long_offsets {
43 + off_arr := data[20:]
44 + var_off = gvar_data_off + ttULONG(off_arr[u32(glyph_index) * 4:])
45 + var_end = gvar_data_off + ttULONG(off_arr[u32(glyph_index + 1) * 4:])
46 + } else {
47 + off_arr := data[20:]
48 + var_off = gvar_data_off + u32(ttUSHORT(off_arr[u32(glyph_index) * 2:])) * 2
49 + var_end = gvar_data_off + u32(ttUSHORT(off_arr[u32(glyph_index + 1) * 2:])) * 2
50 + }
51 +
52 + if var_off == var_end do return false // no variation data
53 + if var_off >= var_end do return false
54 +
55 + // Parse glyph variation data header
56 + gd := data[var_off:]
57 + tuple_count_raw := ttUSHORT(gd)
58 + data_offset := u32(ttUSHORT(gd[2:]))
59 + tuple_count := i32(tuple_count_raw & 0x0FFF)
60 + has_shared_points := (tuple_count_raw & 0x8000) != 0
61 +
62 + serialized := gd[data_offset:] // packed deltas start here
63 + ser_off: u32 = 0
64 +
65 + // Parse shared points if present
66 + shared_points: [256]i32
67 + shared_point_count: i32 = 0
68 + if has_shared_points {
69 + shared_point_count, ser_off = unpack_points(serialized, shared_points[:])
70 + }
71 +
72 + // Process each tuple
73 + hdr_off: u32 = 4
74 + for ti in 0..<tuple_count {
75 + var_data_size := u32(ttUSHORT(gd[hdr_off:]))
76 + tuple_index := ttUSHORT(gd[hdr_off + 2:])
77 + hdr_off += 4
78 +
79 + has_embedded_peak := (tuple_index & 0x8000) != 0
80 + has_intermediate := (tuple_index & 0x4000) != 0
81 + has_private_points := (tuple_index & 0x2000) != 0
82 + tuple_idx := i32(tuple_index & 0x0FFF)
83 +
84 + // Read peak tuple
85 + peak: [MAX_VAR_AXES]f32
86 + if has_embedded_peak {
87 + for ai in 0..<min(axis_count, MAX_VAR_AXES) {
88 + peak[ai] = f32(i16(ttUSHORT(gd[hdr_off:]))) / 16384.0
89 + hdr_off += 2
90 + }
91 + } else if tuple_idx < shared_tuple_count {
92 + st := data[shared_tuples_off + u32(tuple_idx) * u32(axis_count) * 2:]
93 + for ai in 0..<min(axis_count, MAX_VAR_AXES) {
94 + peak[ai] = f32(i16(ttUSHORT(st[u32(ai) * 2:]))) / 16384.0
95 + }
96 + }
97 +
98 + // Read intermediate tuples
99 + start_tuple, end_tuple: [MAX_VAR_AXES]f32
100 + if has_intermediate {
101 + for ai in 0..<min(axis_count, MAX_VAR_AXES) {
102 + start_tuple[ai] = f32(i16(ttUSHORT(gd[hdr_off:]))) / 16384.0
103 + hdr_off += 2
104 + }
105 + for ai in 0..<min(axis_count, MAX_VAR_AXES) {
106 + end_tuple[ai] = f32(i16(ttUSHORT(gd[hdr_off:]))) / 16384.0
107 + hdr_off += 2
108 + }
109 + }
110 +
111 + // Compute scalar for this tuple
112 + scalar := compute_tuple_scalar(coords, &peak, has_intermediate, &start_tuple, &end_tuple, axis_count)
113 + if scalar == 0 {
114 + ser_off += var_data_size
115 + continue
116 + }
117 +
118 + // Determine which points get deltas
119 + pt_indices: [256]i32
120 + pt_count: i32 = 0
121 + local_ser_off := ser_off
122 + all_points := false
123 +
124 + if has_private_points {
125 + pt_count, local_ser_off = unpack_points(serialized[ser_off:], pt_indices[:])
126 + local_ser_off += ser_off
127 + if pt_count == 0 {
128 + all_points = true
129 + pt_count = num_points
130 + }
131 + } else if shared_point_count > 0 {
132 + for i in 0..<min(shared_point_count, 256) {
133 + pt_indices[i] = shared_points[i]
kern.odin added

Inline diff hidden to keep this page fast.

layout.odin added

Inline diff hidden to keep this page fast.

license.md added

Inline diff hidden to keep this page fast.

msdf.odin added

Inline diff hidden to keep this page fast.

names.odin added

Inline diff hidden to keep this page fast.

os2_post.odin added

Inline diff hidden to keep this page fast.

packing.odin added

Inline diff hidden to keep this page fast.

parser.odin added

Inline diff hidden to keep this page fast.

raster.odin added

Inline diff hidden to keep this page fast.

sdf.odin added

Inline diff hidden to keep this page fast.

shapes.odin added

Inline diff hidden to keep this page fast.

svg.odin added

Inline diff hidden to keep this page fast.

types.odin added

Inline diff hidden to keep this page fast.

variations.odin added

Inline diff hidden to keep this page fast.