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.
15 kB · 369 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2011,2012 Google, Inc.// SPDX-License-Identifier: MIT AND MIT-Modern-Variant
//! What the shaper does when the font cannot: positions marks from the//! glyphs' ink boxes when there is no `GPOS` to attach them, and widens//! space characters drawn with the font's ordinary space. HarfBuzz's//! `hb-ot-shape-fallback.cc`.
const std = @import("std");
const Buffer = @import("Buffer.zig");const Face = @import("Face.zig");const script_mod = @import("script.zig");
const GlyphInfo = Buffer.GlyphInfo;
/// Canonical combining classes with names, as HarfBuzz names them.const Ccc = struct { const attached_below_left = 200; const attached_below = 202; const attached_above = 214; const attached_above_right = 216; const below_left = 218; const below = 220; const below_right = 222; const left = 224; const right = 226; const above_left = 228; const above = 230; const above_right = 232; const double_below = 233; const double_above = 234;};
/// Maps the modified combining classes of Hebrew, Arabic, Thai, Lao and/// Tibetan marks, which only order them, to classes that say where they go.fn recategorizeCombiningClass(u: u32, klass_in: u8) u8 { var klass = klass_in; if (klass >= 200) return klass;
// Thai and Lao need some per-character work. if (u & ~@as(u32, 0xFF) == 0x0E00) { if (klass == 0) { switch (u) { 0x0E31, 0x0E34, 0x0E35, 0x0E36, 0x0E37, 0x0E47, 0x0E4C, 0x0E4D, 0x0E4E => klass = Ccc.above_right, 0x0EB1, 0x0EB4, 0x0EB5, 0x0EB6, 0x0EB7, 0x0EBB, 0x0ECC, 0x0ECD => klass = Ccc.above, 0x0EBC => klass = Ccc.below, else => {}, } } else { // Thai virama is below-right. if (u == 0x0E3A) klass = Ccc.below_right; } }
return switch (klass) { // Hebrew: sheva, hataf segol, hataf patah, hataf qamats, hiriq, tsere, // segol, patah, qamats and qamats qatan, qubuts, meteg. 22, 15, 16, 17, 23, 18, 19, 20, 21, 24, 25 => Ccc.below, 13 => Ccc.attached_above, // rafe 10 => Ccc.above_right, // shin dot 11, 14 => Ccc.above_left, // sin dot; holam and holam haser for vav 26 => Ccc.above, // point varika 12 => klass, // dagesh // Arabic and Syriac: fathatan, dammatan, fatha, damma, shadda, sukun, // superscript alef, superscript alaph. 28, 29, 31, 32, 27, 34, 35, 36 => Ccc.above, 30, 33 => Ccc.below, // kasratan, kasra // Thai 3 => Ccc.below_right, // sara u and sara uu 107 => Ccc.above_right, // mai // Lao 118 => Ccc.below, // sign u and sign uu 122 => Ccc.above, // mai // Tibetan 129 => Ccc.below, // sign aa 132 => Ccc.above, // sign i 131 => Ccc.below, // sign u else => klass, };}
/// `_hb_ot_shape_fallback_mark_position_recategorize_marks`.pub fn recategorizeMarks(buffer: *Buffer) void { for (buffer.info.items) |*info| { if (info.generalCategory() == .non_spacing_mark) { const cc = recategorizeCombiningClass(info.codepoint, info.modifiedCombiningClass()); info.setModifiedCombiningClass(cc); } }}
fn zeroMarkAdvances(buffer: *Buffer, start: usize, end: usize, adjust_offsets_when_zeroing: bool) void { const info = buffer.info.items; const pos = buffer.pos.items; for (start..end) |i| { if (info[i].generalCategory() == .non_spacing_mark) { if (adjust_offsets_when_zeroing) { pos[i].x_offset -= pos[i].x_advance; pos[i].y_offset -= pos[i].y_advance; } pos[i].x_advance = 0; pos[i].y_advance = 0; } }}
fn positionMark(face: *const Face, buffer: *Buffer, base_extents: *Face.Extents, i: usize, combining_class: u8) void { const mark_extents = face.extents(buffer.info.items[i].codepoint) orelse return; const y_gap = @divTrunc(face.y_scale, 16); const pos = &buffer.pos.items[i]; pos.x_offset = 0; pos.y_offset = 0;
// LEFT and RIGHT marks are not positioned.
// X positioning. const dir = buffer.direction.?; x: switch (combining_class) { Ccc.double_below, Ccc.double_above => { if (dir == .ltr) { pos.x_offset += base_extents.x_bearing + base_extents.width - @divTrunc(mark_extents.width, 2) - mark_extents.x_bearing; break :x; } else if (dir == .rtl) { pos.x_offset += base_extents.x_bearing - @divTrunc(mark_extents.width, 2) - mark_extents.x_bearing; break :x; } continue :x Ccc.above; }, Ccc.attached_below_left, Ccc.below_left, Ccc.above_left => { // Left align. pos.x_offset += base_extents.x_bearing - mark_extents.x_bearing; }, Ccc.attached_above_right, Ccc.below_right, Ccc.above_right => { // Right align. pos.x_offset += base_extents.x_bearing + base_extents.width - mark_extents.width - mark_extents.x_bearing; }, else => { // Center align. pos.x_offset += base_extents.x_bearing + @divTrunc(base_extents.width - mark_extents.width, 2) - mark_extents.x_bearing; }, }
// Y positioning. switch (combining_class) { Ccc.double_below, Ccc.below_left, Ccc.below, Ccc.below_right, Ccc.attached_below_left, Ccc.attached_below => { if (combining_class != Ccc.attached_below_left and combining_class != Ccc.attached_below) { // Add gap. base_extents.height -= y_gap; } pos.y_offset = base_extents.y_bearing + base_extents.height - mark_extents.y_bearing; // Never shift up "below" marks. if ((y_gap > 0) == (pos.y_offset > 0)) { base_extents.height -= pos.y_offset; pos.y_offset = 0; } base_extents.height += mark_extents.height; }, Ccc.double_above, Ccc.above_left, Ccc.above, Ccc.above_right, Ccc.attached_above, Ccc.attached_above_right => { if (combining_class != Ccc.attached_above and combining_class != Ccc.attached_above_right) { // Add gap. base_extents.y_bearing += y_gap; base_extents.height -= y_gap; } pos.y_offset = base_extents.y_bearing - (mark_extents.y_bearing + mark_extents.height); // Don't shift down "above" marks too much. if ((y_gap > 0) != (pos.y_offset > 0)) { const correction = -@divTrunc(pos.y_offset, 2); base_extents.y_bearing += correction; base_extents.height -= correction; pos.y_offset += correction; } base_extents.y_bearing -= mark_extents.height; base_extents.height += mark_extents.height; }, else => {}, }}
fn positionAroundBase(face: *const Face, buffer: *Buffer, plan_dir: Buffer.Direction, plan_script: script_mod.Script, base: usize, end: usize, adjust_offsets_when_zeroing: bool) void { var horiz_dir: ?Buffer.Direction = null; buffer.unsafeToBreak(base, end);
const info = buffer.info.items; const pos = buffer.pos.items; var base_extents = face.extents(info[base].codepoint) orelse { // If extents don't work, zero marks and go home. zeroMarkAdvances(buffer, base + 1, end, adjust_offsets_when_zeroing); return; }; base_extents.y_bearing += pos[base].y_offset; // Use the horizontal advance for horizontal positioning; generally a // better idea, and it works for zero-ink glyphs too. base_extents.x_bearing = 0; base_extents.width = face.hAdvance(info[base].codepoint);
const lig_id = info[base].ligId(); const num_lig_components: i32 = info[base].ligNumComps();
var x_offset: i32 = 0; var y_offset: i32 = 0; const dir = buffer.direction.?; if (dir.isForward()) { x_offset -= pos[base].x_advance; y_offset -= pos[base].y_advance; }
var component_extents = base_extents; var last_lig_component: i32 = -1; var last_combining_class: u32 = 255; var cluster_extents = base_extents; for (base + 1..end) |i| { if (info[i].modifiedCombiningClass() != 0) { if (num_lig_components > 1) { const this_lig_id = info[i].ligId(); var this_lig_component: i32 = @as(i32, info[i].ligComp()) - 1; // Conditions for attaching to the last component. if (lig_id == 0 or lig_id != this_lig_id or this_lig_component >= num_lig_components) this_lig_component = num_lig_components - 1; if (last_lig_component != this_lig_component) { last_lig_component = this_lig_component; last_combining_class = 255; component_extents = base_extents; if (horiz_dir == null) { horiz_dir = if (plan_dir.isHorizontal()) plan_dir else script_mod.horizontalDirection(plan_script); } if (horiz_dir == .ltr) { component_extents.x_bearing += @divTrunc(this_lig_component * component_extents.width, num_lig_components); } else { component_extents.x_bearing += @divTrunc((num_lig_components - 1 - this_lig_component) * component_extents.width, num_lig_components); } component_extents.width = @divTrunc(component_extents.width, num_lig_components); } }
const this_combining_class = info[i].modifiedCombiningClass(); if (last_combining_class != this_combining_class) { last_combining_class = this_combining_class; cluster_extents = component_extents; }
positionMark(face, buffer, &cluster_extents, i, this_combining_class);
pos[i].x_advance = 0; pos[i].y_advance = 0; pos[i].x_offset += x_offset; pos[i].y_offset += y_offset; } else { if (dir.isForward()) { x_offset -= pos[i].x_advance; y_offset -= pos[i].y_advance; } else { x_offset += pos[i].x_advance; y_offset += pos[i].y_advance; } } }}
fn positionCluster(face: *const Face, buffer: *Buffer, plan_dir: Buffer.Direction, plan_script: script_mod.Script, start: usize, end: usize, adjust_offsets_when_zeroing: bool) void { if (end - start < 2) return; // Find the base glyph. const info = buffer.info.items; var i = start; while (i < end) : (i += 1) { if (!info[i].isUnicodeMark()) { // Find the mark glyphs. var j = i + 1; while (j < end) : (j += 1) { if (info[j].isHidden() or info[j].isDefaultIgnorable()) continue; if (!info[j].isUnicodeMark()) break; } positionAroundBase(face, buffer, plan_dir, plan_script, i, j, adjust_offsets_when_zeroing); i = j - 1; } }}
/// `_hb_ot_shape_fallback_mark_position`.pub fn markPosition(face: *const Face, buffer: *Buffer, plan_dir: Buffer.Direction, plan_script: script_mod.Script, adjust_offsets_when_zeroing: bool) void { var start: usize = 0; const info = buffer.info.items; if (info.len == 0) return; for (1..info.len) |i| { if (!info[i].isUnicodeMark() and !info[i].isHidden() and !info[i].isDefaultIgnorable()) { positionCluster(face, buffer, plan_dir, plan_script, start, i, adjust_offsets_when_zeroing); start = i; } } positionCluster(face, buffer, plan_dir, plan_script, start, info.len, adjust_offsets_when_zeroing);}
/// `_hb_ot_shape_fallback_spaces`: gives the space characters drawn with the/// font's ordinary space the width they should have.pub fn spaces(face: *const Face, buffer: *Buffer) void { const info = buffer.info.items; const pos = buffer.pos.items; const horizontal = buffer.direction.?.isHorizontal(); const x_scale = face.x_scale; const y_scale = face.y_scale; for (info, pos) |*inf, *p| { if (!inf.isUnicodeSpace() or inf.ligated()) continue;
// If the font had no ASCII space and the invisible glyph was used, // give it a quarter-em default advance. if (buffer.invisible != 0 and inf.codepoint == buffer.invisible) { if (horizontal) { p.x_advance = @divTrunc(x_scale, 4); } else { p.y_advance = -@divTrunc(y_scale, 4); } }
const space_type = inf.spaceFallbackType(); switch (space_type) { .not_space, .space => {}, .em, .em_2, .em_3, .em_4, .em_5, .em_6, .em_16 => { const d: i32 = @backingInt(space_type); if (horizontal) { p.x_advance = @divTrunc(x_scale + @divTrunc(d, 2), d); } else { p.y_advance = -@divTrunc(y_scale + @divTrunc(d, 2), d); } }, .four_em_18 => { if (horizontal) { p.x_advance = @intCast(@divTrunc(@as(i64, x_scale) * 4, 18)); } else { p.y_advance = @intCast(@divTrunc(-@as(i64, y_scale) * 4, 18)); } }, .figure => { var u: u32 = '0'; while (u <= '9') : (u += 1) { if (face.nominalGlyph(u)) |g| { if (horizontal) { p.x_advance = face.hAdvance(g); } else { p.y_advance = face.vAdvance(g); } break; } } }, .punctuation => { if (face.nominalGlyph('.') orelse face.nominalGlyph(',')) |g| { if (horizontal) { p.x_advance = face.hAdvance(g); } else { p.y_advance = face.vAdvance(g); } } }, .narrow => { // Half the font's space; the Unicode charts say a quarter or a // fifth of an em, but fonts' spaces are often about that. if (horizontal) { p.x_advance = @divTrunc(p.x_advance, 2); } else { p.y_advance = @divTrunc(p.y_advance, 2); } }, } }}