Repository navigation
Read the Script property for every character, and match HarfBuzz's default cluster level - #292
Merged
Merged
Conversation
ClusterLevel::default(), and so a new Buffer, was MonotoneCharacters, where HarfBuzz and the C API default to MONOTONE_GRAPHEMES. A combining mark, the second regional indicator of a flag, a ZWJ emoji sequence and the other characters a grapheme merges each kept a cluster of their own under the defaults: 73 of the 554 consumer differential cases had other clusters than HarfBuzz 14.5.0 there. The default is now MonotoneGraphemes, and all 554 match with clusters compared. Callers who want a cluster per character set ClusterLevel::MonotoneCharacters. The C API already defaulted to HarfBuzz's level and is unchanged. The tests that read clusters at the default level now expect HarfBuzz's default-level output.
script_of gave the older UnicodeScript buckets (Latin through Modi) the Unicode blocks of their scripts, and the Script property only to the buckets added in 0.22.0. A letter outside those blocks was Other, so a buffer whose first letter sat there was guessed as Other and shaped under DFLT alone: Vietnamese in Latin Extended Additional lost its ligatures and kerning (Roboto "ỷ fi", the Windows 11 sans "Ựa"), Arabic Extended-A shaped left to right with no joining in all 7 fonts tried, and the Mongolian Supplement, the CJK extensions past B, Devanagari Extended and the other supplements lost their scripts. Every code point now takes the bucket of its Script property (Scripts.txt of Unicode 18.0.0, already the source of the table): Hiragana and Katakana fold into Han as before, and Common, Inherited, Unknown and the scripts without a bucket are Other. Script runs and the guessed script skip Unknown characters as well as Common and Inherited ones, as HarfBuzz's hb_buffer_guess_segment_properties does, and a kana run takes the kana tag by its Script property. The ordinal indicators U+00AA and U+00BA are Latin letters now; the hand-listed combining mark blocks still extend the run before them. The implicit vertical layout of Mongolian (no direction set, which HarfBuzz does not have) now applies to text that starts with a Mongolian character of the Mongolian block. It read the first character with a block bucket before, so with the Script property a leading space or a Mongolian Supplement ornament would have turned text vertical that was horizontal; text that starts with another character (a quotation mark, the Mongolian comma) now stays horizontal, as in HarfBuzz. Against HarfBuzz 14.5.0, texts whose first character sits outside the old blocks (11,043 rows over 30 fonts) go from 1,258 mismatches to 8, all Coptic letters, which have no bucket.
A text of only Common, Inherited and Unknown characters, such as Arabic marks before a tatweel, guesses no script. HarfBuzz's buffer then keeps HB_SCRIPT_INVALID, whose native horizontal direction is left to right, so hb_ensure_native_direction leaves an LTR buffer alone. sigilbuzz took the direction of the text's first strong character instead: the tatweel is AL, so an LTR buffer counted as non-native, its graphemes were reversed, and the marks came out in reverse order. Arial U+064F U+064B U+0640 gave [757, 753, 752] where HarfBuzz gives [753, 757, 752]. Text with no script is now natively left to right. A buffer whose script has no bucket still reads its first strong character, as before.
The stch stretch (U+070F SYRIAC ABBREVIATION MARK) fills the rest of its word, and HarfBuzz counts a character into the word when its General_Category is one of HB_ARABIC_GENERAL_CATEGORY_IS_WORD, which holds the symbols Sc, Sk, Sm and So. The General_Category table had no symbol class, so a symbol ended the word and the stretch came out short: Noto Sans Syriac U+070F U+0712 '+' U+0713 U+0715 repeated its tiles once each, where HarfBuzz repeats them four times. The table gets a Symbol class (Sc, Sk, Sm, So, from the same Unicode 17.0.0 DerivedGeneralCategory.txt), and the stretch counts it into the word. GeneralCategoryClass is #[non_exhaustive] from here on, so a later class is not a breaking change. The table grows by 242 ranges.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #288
Four commits, one per audit item. Each one passes the core and C API tests on its own.
src/buffer/flags.rs).ClusterLevel::default(), and so a newBuffer, is nowMonotoneGraphemes. HarfBuzz defaults to this level, and so does the C API (hb_buffer_createalready usedHB_BUFFER_CLUSTER_LEVEL_MONOTONE_GRAPHEMES, so the C API is unchanged). Tests that read clusters at the default level now expect HarfBuzz's default-level output.docs/STABILITY.mdsays so.src/unicode/mod.rs,script_table.rs,tests/unicode_table_gen.rs).script_ofreturns the bucket of each character's Unicode Script property, from the sameScripts.txt18.0.0 snapshot that already fed the table.TABLE_SCRIPTS. The generator maps every ISO code to its bucket, with Hira and Kana folding into Han. Common, Inherited, Unknown and the scripts without a bucket map toOther.hb_buffer_guess_segment_propertiesdoes.kanaby the Script property instead of by block.src/shape/native_direction.rs).HB_SCRIPT_INVALIDis.stchword (src/unicode/general_category*.rs,src/shape/stch.rs).Symbolclass (Sc, Sk, Sm, So). The snapshot is refreshed from the same Unicode 17.0.0DerivedGeneralCategory.txt, keeping the four symbol categories.stchstretch counts symbols into its word.GeneralCategoryClass(under the hiddenunicode::path) gains the variant and becomes#[non_exhaustive].New parity file
tests/script_property_parity.rs: 21 cases, all HarfBuzz 14.5.0 output, on the repo's own fonts (Rubik, Amiri, Noto Sans Mongolian / Devanagari / Sinhala / Syriac). It covers Latin Extended Additional, Arabic Extended-A and the math letters, the Mongolian Supplement, Devanagari Extended / Extended-A, Sinhala Archaic Numbers, leading marks before a tatweel, and symbols in anstchword. 0.23.1 fails 19 of the 21 cases. Unit tests coverscript_of,is_common_or_inherited, theSymbolclass,stchword membership, native direction with no script, and the Mongolian vertical trigger. The generator test now checks every code point:script_ofmust equal the bucket ofscript_code.Before and after (HarfBuzz 14.5.0 through uharfbuzz 0.56.2)
The issue's repros, each a buffer with guessed segment properties:
Rows that differ from HarfBuzz:
scripts.tsv(first letter in the old block)scripts2.tsv(Vietnamese, 6 Latin fonts)scripts3.tsv(Arabic Extended-A, Mongolian Supplement, 8 fonts)scripts4.tsvmarks.tsvThe 8 remaining rows are SansSerifCollection texts with a Coptic letter followed by a space. Coptic has no bucket (see Not done).
Broad sweep in the style of
review283/shape_cmp.py. It covers 26 fonts (13 from the repo, 13 Windows and downloaded fonts; 9 are variable, at 2 random settings each besides the default). Per font it shapes 1,000 random cmap characters and 500 random triples, and where the font maps combining marks, 200 base+mark+mark texts and 100 mark+mark+base texts, in h and v. sigilbuzz shapes each text twice: once withoutguess_segment_properties, as the original harness did, and once with it. HarfBuzz always guesses. Rows are font × setting × text:Other. The kerning pair across the cut was lost, and by chance that matched HarfBuzz's Cyrillic or Greek shaping.Othertexts) are unchanged.Table size
libsigilbuzz.rlib, release,--no-default-featuresopt-level = "z", LTO,panic = "abort", stripped)The Script table keeps its size. Only its bucket column changes, and the block arms of
script_ofare gone. The General_Category table grows by 242 ranges, which is the symbol class.Verification
cargo fmt --all -- --check: clean.cargo +1.99.0 clippywith--workspace --all-targets --all-features, with--workspace --all-targets --no-default-features, and with-p sigilbuzz --no-default-features, all with-D warnings: clean.cargo test --workspace --all-features: 195 test binaries, 3,682 tests pass. That includes the Indic, USE, Khmer, Myanmar, Hangul, bidi, cluster-level, Mongolian, Tibetan, line-breaking and word-breaking parity suites and the C API tests.cargo build --no-default-features: OK. MSRV, withCARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback cargo +stable generate-lockfile, thencargo +1.81 check --workspace --lib --no-default-featuresand--lib --bins --all-features: OK.shapefuzz target, 10 minutes in Docker (rustlang/rust:nightly,-O -a, seeds are the repo fonts under 65 KB): 102,391 runs, 1,706 new units, peak RSS 467 MB. No crash, timeout or leak.unicode_table_genregenerates every table byte for byte from the snapshots.Not done
OtherunderDFLT, where HarfBuzz tries their own tag first (armn,copt, ...). The 8 Coptic rows above and most of the remaining SansSerifCollection rows in the guessed sweep come from this. Fixing it needsOthersegments to carry their ISO code.hani; HarfBuzz useskana. Unguessed segments already getkana.stch:stchstill tells Lm and Lo from cased letters through Rust's case properties, so a titlecase letter, or a modifier letter with Other_Lowercase, is misjudged.CHANGELOG.mdanddocs/ROADMAP.mdare not edited. Two bullets in the ROADMAP "Gaps" list are done by this PR and can go: thestchsymbols bullet and the "olderUnicodeScriptbuckets" bullet.CHANGELOG lines (for the 0.24.0 section)
Changed:
MonotoneGraphemes, HarfBuzz's default (Read the Script property for every character, and match HarfBuzz's default cluster level #288).ClusterLevel::default(), and so every newBuffer, wasMonotoneCharacters. Under the defaults, a combining mark, a variation selector, the second regional indicator of a flag, the rest of a ZWJ emoji sequence and the other characters a grapheme merges now share the cluster of the character they continue, as in HarfBuzz. Glyphs and positions do not change. Code that expects a cluster per character callsbuffer.set_cluster_level(ClusterLevel::MonotoneCharacters). The C API already defaulted toHB_BUFFER_CLUSTER_LEVEL_MONOTONE_GRAPHEMESand is unchanged. Thesigilbuzz shapecommand builds its buffer with the default, so itscluster=values change the same way and now matchhb-shape. Under defaults, all 554 consumer differential cases now match HarfBuzz 14.5.0's clusters.script_ofreturns the bucket of a character's Unicode Script property for every bucket (Read the Script property for every character, and match HarfBuzz's default cluster level #288). The older buckets, Latin through Modi, covered the Unicode blocks of their scripts. So letters outside those blocks wereOther: Latin Extended Additional, Arabic Extended-A, -B and -C, the Arabic mathematical letters, the CJK extensions past B and the compatibility ideographs, the Mongolian Supplement, Devanagari Extended and others. They now take their script's bucket. The Common and Inherited characters inside the blocks took the block's bucket and are nowOther. These include digits, punctuation, the space, the tatweel, the dandas and combining marks, soscript_of('0')wasLatin. The Coptic letters of the Greek and Coptic block areOther, notGreek.Buffer::script_runsand the segmentation ofshape) andBuffer::guess_segment_propertiesskip unassigned and private-use characters, as well as Common and Inherited ones, ashb_buffer_guess_segment_propertiesdoes (Read the Script property for every character, and match HarfBuzz's default cluster level #288). A private-use icon inside Latin text stays in the Latin run. The ordinal indicators U+00AA and U+00BA count as Latin letters, so inside text of another script they start a Latin run when the buffer has no script set.Direction::Ttbfor vertical Mongolian.unicode::general_category::GeneralCategoryClasshas aSymbolclass (Sc, Sk, Sm, So) and is#[non_exhaustive](Read the Script property for every character, and match HarfBuzz's default cluster level #288).general_category_classreturnsSome(Symbol)for the symbols, which wereNone. The table grows by 242 ranges.Fixed:
Text whose first letter sits outside the blocks of the older script buckets shapes with its script, as in HarfBuzz (Read the Script property for every character, and match HarfBuzz's default cluster level #288). It shaped as
OtherunderDFLT:Over 11,043 such texts in 30 fonts, mismatches against HarfBuzz 14.5.0 fall from 1,258 to 8 (Coptic, which has no bucket).
Text with no script-bearing character shapes left to right, as HarfBuzz's invalid script does (Read the Script property for every character, and match HarfBuzz's default cluster level #288). Leading Arabic marks before a tatweel were reversed: Arial U+064F U+064B U+0640 gave [757, 753, 752], where HarfBuzz gives [753, 757, 752].
The
stchstretch (U+070F SYRIAC ABBREVIATION MARK) counts symbols (General_Category Sc, Sk, Sm and So) into the word it fills, as HarfBuzz does (Read the Script property for every character, and match HarfBuzz's default cluster level #288). A symbol ended the word, so the stretch came out short.