Something went wrong. Try again.
An OpenType text shaper for Zig, ported from HarfBuzz and checked against its test suite, for fonts decoded by zig-font.
Something went wrong. Try again.
11 kB · 267 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2007,2008,2009,2010 Red Hat, Inc.// SPDX-FileCopyrightText: © 2010,2012 Google, Inc.// SPDX-License-Identifier: MIT AND MIT-Modern-Variant
//! Applying `GSUB` subtables: HarfBuzz's `OT/Layout/GSUB`.
const std = @import("std");const font = @import("font");
const apply = @import("apply.zig");const Buffer = @import("Buffer.zig");
const ApplyContext = apply.ApplyContext;const Error = apply.Error;const GlyphProps = Buffer.GlyphProps;const gsub = font.tables.gsub;
/// Tries each subtable of `lookup` at the cursor, stopping at the first that/// applies.pub fn applySubtables(c: *ApplyContext, lookup: *const gsub.Lookup) Error!bool { for (lookup.subtables) |*st| { if (try applySubtable(c, st)) return true; } return false;}
pub fn isReverse(lookup: *const gsub.Lookup) bool { return lookup.lookup_type == 8;}
fn applySubtable(c: *ApplyContext, st: *const gsub.Subtable) Error!bool { const gid: u16 = @truncate(c.buffer.cur(0).codepoint); switch (st.*) { .single => |s| { const g = s.apply(gid) orelse return false; try c.replaceGlyph(g); return true; }, .multiple => |m| { const index = m.coverage.get(gid) orelse return false; // A coverage index with no sequence is an empty one, which // deletes the glyph, as HarfBuzz reads past the end of the array. const seq: []const u16 = if (index < m.sequences.len) m.sequences[index] else &.{}; return applySequence(c, seq); }, .alternate => |a| { const index = a.coverage.get(gid) orelse return false; if (index >= a.alternate_sets.len) return false; return applyAlternates(c, a.alternate_sets[index]); }, .ligature => |l| { const index = l.coverage.get(gid) orelse return false; if (index >= l.ligature_sets.len) return false; for (l.ligature_sets[index]) |*lig| { if (try applyLigature(c, lig)) return true; } return false; }, .context => |*ctx| return apply.applyContext(c, ctx), .chained_context => |*ctx| return apply.applyChainContext(c, ctx), .reverse_chain_single => |*r| return applyReverseChainSingle(c, r), }}
/// `Sequence::apply`: multiple substitution.fn applySequence(c: *ApplyContext, seq: []const u16) Error!bool { const buffer = c.buffer; // A sequence of one is done in place, and not counted as multiplying. if (seq.len == 1) { try c.replaceGlyph(seq[0]); return true; } // The specification disallows an empty sequence, but Uniscribe allows // it, and deletes the glyph. if (seq.len == 0) { buffer.deleteGlyph(); return true; } const klass: u16 = if (buffer.cur(0).isLigature()) GlyphProps.base_glyph else 0; const lig_id = buffer.cur(0).ligId(); for (seq, 0..) |g, i| { // If attached to a ligature, do not disturb that. if (lig_id == 0) buffer.cur(0).setLigPropsForComponent(@intCast(i)); try c.outputGlyphForComponent(g, klass); } buffer.skipGlyph(); return true;}
/// `AlternateSet::apply`: the feature's value picks the alternate.fn applyAlternates(c: *ApplyContext, alternates: []const u16) Error!bool { const count = alternates.len; if (count == 0) return false; const glyph_mask = c.buffer.cur(0).mask; const lookup_mask = c.lookup_mask; // This breaks badly if two features enabled this lookup together. const shift: u5 = @intCast(@ctz(lookup_mask)); var alt_index: u32 = (lookup_mask & glyph_mask) >> shift; // The maximum value picks one at random, for `rand`. if (alt_index == @import("Map.zig").max_value and c.random) { c.buffer.unsafeToBreakAll(); alt_index = c.randomNumber() % @as(u32, @intCast(count)) + 1; } if (alt_index > count or alt_index == 0) return false; try c.replaceGlyph(alternates[alt_index - 1]); return true;}
/// `Ligature::apply`.fn applyLigature(c: *ApplyContext, lig: *const gsub.Ligature) Error!bool { const count = lig.components.len + 1; // A ligature of one component is done in place, and not counted as // ligating. if (count == 1) { try c.replaceGlyph(lig.glyph); return true; } if (count > apply.max_context_length) return false; var total_component_count: u32 = 0; var match_end: usize = 0; if (!apply.matchInput(c, count, .{ .glyph = lig.components }, &match_end, &total_component_count)) { c.buffer.unsafeToConcat(c.buffer.idx, match_end); return false; } try apply.ligateInput(c, count, match_end, lig.glyph, total_component_count); return true;}
/// `ReverseChainSingleSubstFormat1::apply`.fn applyReverseChainSingle(c: *ApplyContext, r: *const gsub.ReverseChainSingle) Error!bool { const gid: u16 = @truncate(c.buffer.cur(0).codepoint); const index = r.coverage.get(gid) orelse return false; // No chaining to this type. if (c.nesting_level_left != apply.max_nesting_level) return false; if (index >= r.substitutes.len) return false;
var start_index: usize = 0; var end_index: usize = 0; if (apply.matchBacktrack(c, r.backtrack.len, .{ .coverage = r.backtrack }, &start_index) and apply.matchLookahead(c, r.lookahead.len, .{ .coverage = r.lookahead }, c.buffer.idx + 1, &end_index)) { c.buffer.unsafeToBreakFromOutbuffer(start_index, end_index); c.replaceGlyphInplace(r.substitutes[index]); // The cursor is not moved back; the main loop does that, which keeps // a context lookup calling this from being surprised. return true; } c.buffer.unsafeToConcatFromOutbuffer(start_index, end_index); return false;}
/// `hb_ot_layout_lookup_would_substitute`: whether `lookup` would apply to/// exactly the glyph sequence `glyphs`, matched with nothing skipped. With/// `zero_context`, a chained rule with any backtrack or lookahead does not/// count. The shapers use it to ask what a font does with a sequence, such/// as whether a consonant has a below-base form.pub fn wouldApply(lookup: *const gsub.Lookup, glyphs: []const u16, zero_context: bool) bool { if (glyphs.len == 0) return false; // HarfBuzz first asks whether any subtable covers the first glyph. const covered = for (lookup.subtables) |*st| { if (subtableCoverage(st, glyphs[0])) break true; } else false; if (!covered) return false; for (lookup.subtables) |*st| { if (subtableWouldApply(st, glyphs, zero_context)) return true; } return false;}
fn subtableCoverage(st: *const gsub.Subtable, g: u16) bool { const layout = font.tables.layout; const cov: ?layout.Coverage = switch (st.*) { .single => |s| switch (s) { .delta => |d| d.coverage, .list => |l| l.coverage, }, .multiple => |m| m.coverage, .alternate => |a| a.coverage, .ligature => |l| l.coverage, .reverse_chain_single => |r| r.coverage, .context => |c| switch (c) { .glyphs => |f| f.coverage, .classes => |f| f.coverage, .coverages => |f| if (f.coverages.len > 0) f.coverages[0] else null, }, .chained_context => |c| switch (c) { .glyphs => |f| f.coverage, .classes => |f| f.coverage, .coverages => |f| if (f.input.len > 0) f.input[0] else null, }, }; const cv = cov orelse return false; return cv.get(g) != null;}
/// `would_match_input`.fn wouldMatchInput(glyphs: []const u16, count: usize, m: apply.Match) bool { if (count != glyphs.len) return false; for (glyphs[1..], 0..) |g, i| { const ok = switch (m) { .none => true, .glyph => |v| v[i] == g, .class => |c| c.def.get(g) == c.values[i], .coverage => |cs| cs[i].get(g) != null, }; if (!ok) return false; } return true;}
fn ruleSetAt(comptime Rule: type, sets: []const ?[]const Rule, index: ?u16) []const Rule { const i = index orelse return &.{}; if (i >= sets.len) return &.{}; return sets[i] orelse &.{};}
fn subtableWouldApply(st: *const gsub.Subtable, glyphs: []const u16, zero_context: bool) bool { const g0 = glyphs[0]; switch (st.*) { .single, .multiple, .alternate, .reverse_chain_single => return glyphs.len == 1 and subtableCoverage(st, g0), .ligature => |l| { const index = l.coverage.get(g0) orelse return false; if (index >= l.ligature_sets.len) return false; for (l.ligature_sets[index]) |lig| { if (lig.components.len + 1 == glyphs.len and std.mem.eql(u16, lig.components, glyphs[1..])) return true; } return false; }, .context => |*c| switch (c.*) { .glyphs => |*f| { for (ruleSetAt(font.tables.layout.SequenceRule, f.rule_sets, f.coverage.get(g0))) |r| { if (wouldMatchInput(glyphs, r.input.len + 1, .{ .glyph = r.input })) return true; } return false; }, .classes => |*f| { for (ruleSetAt(font.tables.layout.SequenceRule, f.rule_sets, f.class_def.get(g0))) |r| { if (wouldMatchInput(glyphs, r.input.len + 1, .{ .class = .{ .def = &f.class_def, .values = r.input } })) return true; } return false; }, .coverages => |*f| return f.coverages.len > 0 and wouldMatchInput(glyphs, f.coverages.len, .{ .coverage = f.coverages[1..] }), }, .chained_context => |*c| switch (c.*) { .glyphs => |*f| { for (ruleSetAt(font.tables.layout.ChainedSequenceRule, f.rule_sets, f.coverage.get(g0))) |r| { if (zero_context and (r.backtrack.len > 0 or r.lookahead.len > 0)) continue; if (wouldMatchInput(glyphs, r.input.len + 1, .{ .glyph = r.input })) return true; } return false; }, .classes => |*f| { for (ruleSetAt(font.tables.layout.ChainedSequenceRule, f.rule_sets, f.input_class_def.get(g0))) |r| { if (zero_context and (r.backtrack.len > 0 or r.lookahead.len > 0)) continue; if (wouldMatchInput(glyphs, r.input.len + 1, .{ .class = .{ .def = &f.input_class_def, .values = r.input } })) return true; } return false; }, .coverages => |*f| { if (zero_context and (f.backtrack.len > 0 or f.lookahead.len > 0)) return false; return f.input.len > 0 and wouldMatchInput(glyphs, f.input.len, .{ .coverage = f.input[1..] }); }, }, }}