| 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. |
| 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/ |
| 1 | + | [workspace] | |
| 2 | + | name = "font" |
| 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) |
| 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 | + | } |
| 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 | + | } |
| 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 | + | } |
| 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] |
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.