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.
12 kB · 375 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2009 Red Hat, Inc.// SPDX-FileCopyrightText: © 2011 Codethink Limited// SPDX-FileCopyrightText: © 2010,2011,2012 Google, Inc.// SPDX-License-Identifier: MIT AND MIT-Modern-Variant
//! The Unicode character properties shaping consults, from uucode, in the//! form HarfBuzz's shaping code expects them: its ordering of the general//! categories, its modified combining classes, its definition of a default//! ignorable. Ported from HarfBuzz's `hb-unicode.hh` and `hb-unicode.cc`.
const std = @import("std");const uucode = @import("uucode");const compose_table = @import("compose_table");
/// The general categories, in HarfBuzz's order, which some of the shaping/// code depends on.pub const GeneralCategory = enum(u5) { control, format, unassigned, private_use, surrogate, lowercase_letter, modifier_letter, other_letter, titlecase_letter, uppercase_letter, spacing_mark, enclosing_mark, non_spacing_mark, decimal_number, letter_number, other_number, connect_punctuation, dash_punctuation, close_punctuation, final_punctuation, initial_punctuation, other_punctuation, open_punctuation, currency_symbol, modifier_symbol, math_symbol, other_symbol, line_separator, paragraph_separator, space_separator,
pub fn isMark(gc: GeneralCategory) bool { return switch (gc) { .spacing_mark, .enclosing_mark, .non_spacing_mark => true, else => false, }; }
pub fn isLetter(gc: GeneralCategory) bool { return switch (gc) { .lowercase_letter, .modifier_letter, .other_letter, .titlecase_letter, .uppercase_letter => true, else => false, }; }};
pub fn generalCategory(cp: u21) GeneralCategory { return switch (uucode.get(.general_category, cp)) { .letter_uppercase => .uppercase_letter, .letter_lowercase => .lowercase_letter, .letter_titlecase => .titlecase_letter, .letter_modifier => .modifier_letter, .letter_other => .other_letter, .mark_nonspacing => .non_spacing_mark, .mark_spacing_combining => .spacing_mark, .mark_enclosing => .enclosing_mark, .number_decimal_digit => .decimal_number, .number_letter => .letter_number, .number_other => .other_number, .punctuation_connector => .connect_punctuation, .punctuation_dash => .dash_punctuation, .punctuation_open => .open_punctuation, .punctuation_close => .close_punctuation, .punctuation_initial_quote => .initial_punctuation, .punctuation_final_quote => .final_punctuation, .punctuation_other => .other_punctuation, .symbol_math => .math_symbol, .symbol_currency => .currency_symbol, .symbol_modifier => .modifier_symbol, .symbol_other => .other_symbol, .separator_space => .space_separator, .separator_line => .line_separator, .separator_paragraph => .paragraph_separator, .other_control => .control, .other_format => .format, .other_surrogate => .surrogate, .other_private_use => .private_use, .other_not_assigned => .unassigned, };}
pub fn combiningClass(cp: u21) u8 { return uucode.get(.canonical_combining_class, cp);}
/// HarfBuzz's renumbering of the combining classes: the fixed-position/// classes of Hebrew, Arabic, Syriac, Telugu, Thai, Lao and Tibetan are/// reordered so that sorting marks by class puts them in the order fonts/// expect, which is not always the order Unicode's numbers give.const modified_combining_class = blk: { var t: [256]u8 = undefined; for (&t, 0..) |*v, i| v.* = i; // Hebrew t[10] = 22; // sheva t[11] = 15; // hataf segol t[12] = 16; // hataf patah t[13] = 17; // hataf qamats t[14] = 23; // hiriq t[15] = 18; // tsere t[16] = 19; // segol t[17] = 20; // patah t[18] = 21; // qamats and qamats qatan t[19] = 14; // holam and holam haser for vav t[20] = 24; // qubuts t[21] = 12; // dagesh t[22] = 25; // meteg t[23] = 13; // rafe t[24] = 10; // shin dot t[25] = 11; // sin dot t[26] = 26; // point varika // Arabic t[27] = 28; // fathatan t[28] = 29; // dammatan t[29] = 30; // kasratan t[30] = 31; // fatha t[31] = 32; // damma t[32] = 33; // kasra t[33] = 27; // shadda t[34] = 34; // sukun t[35] = 35; // superscript alef // Syriac t[36] = 36; // superscript alaph // Telugu t[84] = 4; // length mark t[91] = 5; // ai length mark // Thai t[103] = 3; // sara u and sara uu t[107] = 107; // mai // Lao t[118] = 118; // sign u and sign uu t[122] = 122; // mai // Tibetan t[129] = 129; // sign aa t[130] = 132; // sign i t[132] = 131; // sign u break :blk t;};
/// `hb_unicode_funcs_t::modified_combining_class`.pub fn modifiedCombiningClass(cp: u21) u8 { // Reorder SAKOT to ensure it comes after any tone marks. if (cp == 0x1A60) return 254; // Reorder PADMA to ensure it comes after any vowel marks. if (cp == 0x0FC6) return 254; // Reorder TSA -PHRU to reorder before U+0F74. if (cp == 0x0F39) return 127; return modified_combining_class[combiningClass(cp)];}
/// The mirrored counterpart of a character, for right-to-left text, or the/// character itself when it has none.pub fn mirroring(cp: u21) u21 { return uucode.get(.bidi_mirroring, cp) orelse cp;}
pub fn isVariationSelector(cp: u21) bool { return (cp >= 0xFE00 and cp <= 0xFE0F) or (cp >= 0xE0100 and cp <= 0xE01EF);}
/// HarfBuzz's own list of default ignorables, which is not quite Unicode's/// `Default_Ignorable_Code_Point`: it leaves out the Hangul fillers and/// U+115F, U+1160, U+3164 and U+FFA0, which fonts draw.pub fn isDefaultIgnorable(ch: u21) bool { const plane = ch >> 16; if (plane == 0) { return switch (ch >> 8) { 0x00 => ch == 0x00AD, 0x03 => ch == 0x034F, 0x06 => ch == 0x061C, 0x17 => ch >= 0x17B4 and ch <= 0x17B5, 0x18 => ch >= 0x180B and ch <= 0x180E, 0x20 => (ch >= 0x200B and ch <= 0x200F) or (ch >= 0x202A and ch <= 0x202E) or (ch >= 0x2060 and ch <= 0x206F), 0xFE => (ch >= 0xFE00 and ch <= 0xFE0F) or ch == 0xFEFF, 0xFF => ch >= 0xFFF0 and ch <= 0xFFF8, else => false, }; } return switch (plane) { 0x01 => ch >= 0x1D173 and ch <= 0x1D17A, 0x0E => ch >= 0xE0000 and ch <= 0xE0FFF, else => false, };}
pub fn isExtendedPictographic(cp: u21) bool { return uucode.get(.is_extended_pictographic, cp);}
/// How wide a space character is, for drawing one with the font's ordinary/// space when the font has no glyph for it. The values are the denominators/// of fractions of an em, apart from the named widths at the end.pub const SpaceType = enum(u8) { not_space = 0, em = 1, em_2 = 2, em_3 = 3, em_4 = 4, em_5 = 5, em_6 = 6, em_16 = 16, four_em_18 = 17, space = 18, figure = 19, punctuation = 20, narrow = 21,};
pub fn spaceFallbackType(cp: u21) SpaceType { return switch (cp) { 0x0020, 0x00A0 => .space, 0x2000, 0x2002 => .em_2, 0x2001, 0x2003, 0x3000 => .em, 0x2004 => .em_3, 0x2005 => .em_4, 0x2006 => .em_6, 0x2007 => .figure, 0x2008 => .punctuation, 0x2009 => .em_5, 0x200A => .em_16, 0x202F => .narrow, 0x205F => .four_em_18, else => .not_space, };}
/// The vertical presentation form of a character, or the character itself.pub fn verticalCharFor(u: u21) u21 { return switch (u) { 0x2013 => 0xfe32, 0x2014 => 0xfe31, 0x2025 => 0xfe30, 0x2026 => 0xfe19, 0x3001 => 0xfe11, 0x3002 => 0xfe12, 0x3008 => 0xfe3f, 0x3009 => 0xfe40, 0x300a => 0xfe3d, 0x300b => 0xfe3e, 0x300c => 0xfe41, 0x300d => 0xfe42, 0x300e => 0xfe43, 0x300f => 0xfe44, 0x3010 => 0xfe3b, 0x3011 => 0xfe3c, 0x3014 => 0xfe39, 0x3015 => 0xfe3a, 0x3016 => 0xfe17, 0x3017 => 0xfe18, 0xfe4f => 0xfe34, 0xff01 => 0xfe15, 0xff08 => 0xfe35, 0xff09 => 0xfe36, 0xff0c => 0xfe10, 0xff1a => 0xfe13, 0xff1b => 0xfe14, 0xff1f => 0xfe16, 0xff3b => 0xfe47, 0xff3d => 0xfe48, 0xff3f => 0xfe33, 0xff5b => 0xfe37, 0xff5d => 0xfe38, else => u, };}
pub const JoiningType = uucode.types.JoiningType;pub const JoiningGroup = uucode.types.JoiningGroup;
pub fn joiningType(cp: u21) JoiningType { return uucode.get(.joining_type, cp);}
pub fn joiningGroup(cp: u21) JoiningGroup { return uucode.get(.joining_group, cp);}
const s_base = 0xAC00;const l_base = 0x1100;const v_base = 0x1161;const t_base = 0x11A7;const l_count = 19;const v_count = 21;const t_count = 28;const n_count = v_count * t_count;const s_count = l_count * n_count;
pub const Decomposition = struct { a: u21, b: u21 };
/// `hb_ucd_decompose`: the canonical decomposition of a character into one/// character or two, with `b` zero for one. Only the first step; applying it/// again to `a` takes it further.pub fn decompose(ab: u21) ?Decomposition { if (ab >= s_base and ab < s_base + s_count) { const si = ab - s_base; if (si % t_count != 0) { return .{ .a = s_base + (si / t_count) * t_count, .b = t_base + (si % t_count) }; } return .{ .a = l_base + si / n_count, .b = v_base + (si % n_count) / t_count }; } if (uucode.get(.decomposition_type, ab) != .canonical) return null; var buf: [1]u21 = undefined; const m = uucode.get(.decomposition_mapping, ab).with(&buf, ab); return switch (m.len) { 1 => if (m[0] == ab) null else .{ .a = m[0], .b = 0 }, 2 => .{ .a = m[0], .b = m[1] }, else => null, };}
/// `hb_ucd_compose`: the primary composite of two characters, if there is one.pub fn compose(a: u21, b: u21) ?u21 { if (a == 0 or b == 0) return null; if (a >= s_base and a < s_base + s_count and b > t_base and b < t_base + t_count and (a - s_base) % t_count == 0) { return a + (b - t_base); } if (a >= l_base and a < l_base + l_count and b >= v_base and b < v_base + v_count) { return s_base + (a - l_base) * n_count + (b - v_base) * t_count; } const key = (@as(u64, a) << 21) | b; const pairs = &compose_table.pairs; var lo: usize = 0; var hi: usize = pairs.len; while (lo < hi) { const mid = lo + (hi - lo) / 2; const k = (@as(u64, pairs[mid][0]) << 21) | pairs[mid][1]; if (key < k) { hi = mid; } else if (key > k) { lo = mid + 1; } else return pairs[mid][2]; } return null;}
test "composition" { try std.testing.expectEqual(@as(?u21, 0x00C5), compose('A', 0x030A)); try std.testing.expectEqual(@as(?u21, null), compose('A', 'B')); try std.testing.expectEqual(@as(?u21, 0xAC00), compose(0x1100, 0x1161)); // U+0344 is a composition exclusion. try std.testing.expectEqual(@as(?u21, null), compose(0x0308, 0x0301)); try std.testing.expectEqual(Decomposition{ .a = 'A', .b = 0x030A }, decompose(0x00C5).?); try std.testing.expectEqual(Decomposition{ .a = 0x212B - 0x212B + 0x00C5, .b = 0 }, decompose(0x212B).?); try std.testing.expectEqual(@as(?Decomposition, null), decompose('A'));}
test "properties" { try std.testing.expectEqual(GeneralCategory.non_spacing_mark, generalCategory(0x0301)); try std.testing.expectEqual(@as(u8, 230), modifiedCombiningClass(0x0301)); try std.testing.expectEqual(@as(u8, 27), modifiedCombiningClass(0x0651)); try std.testing.expectEqual(@as(u21, ')'), mirroring('(')); try std.testing.expect(isDefaultIgnorable(0x200D)); try std.testing.expect(!isDefaultIgnorable(0x3164));}