mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 10:56:51 -06:00
fonts: port complex-text shaping to harfrust 0.14
This commit is contained in:
+36
-11
@@ -8,6 +8,7 @@
|
||||
|
||||
use std::ops::Range;
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_geom::Vec2;
|
||||
|
||||
@@ -182,28 +183,31 @@ struct Glyph {
|
||||
/// Shape characters `r` of `cs` (one direction, one face) into glyphs in display order.
|
||||
fn shape_run(face: &Face, cs: &[char], r: Range<usize>, rtl: bool) -> Vec<Glyph> {
|
||||
let run = cs.get(r.clone()).unwrap_or_default();
|
||||
let Ok(font) = harfrust::FontRef::from_index(&face.bytes, face.index) else { return Vec::new() };
|
||||
let data = harfrust::ShaperData::new(&font);
|
||||
let shaper = data.shaper(&font).build();
|
||||
let mut buf = harfrust::UnicodeBuffer::new();
|
||||
// Shares the face's bytes instead of copying them.
|
||||
let bytes: Arc<dyn AsRef<[u8]> + Send + Sync> = face.bytes.clone();
|
||||
let Some(font) = harfrust::Font::new(bytes, face.index) else { return Vec::new() };
|
||||
let shaper = harfrust::ShaperFont::new(&font);
|
||||
let mut buf = harfrust::Buffer::new();
|
||||
let arabic = run.iter().any(|c| matches!(*c, '\u{0600}'..='\u{08FF}'));
|
||||
if arabic && !face.has_joining_features() {
|
||||
for (c, i) in bidi::presentation_forms(run, |p| face.has_glyph(p)) {
|
||||
buf.add(c, u32::try_from(r.start + i).unwrap_or(u32::MAX));
|
||||
buf.push(u32::from(c), u32::try_from(r.start + i).unwrap_or(u32::MAX));
|
||||
}
|
||||
} else {
|
||||
for (i, c) in run.iter().enumerate() {
|
||||
buf.add(*c, u32::try_from(r.start + i).unwrap_or(u32::MAX));
|
||||
buf.push(u32::from(*c), u32::try_from(r.start + i).unwrap_or(u32::MAX));
|
||||
}
|
||||
}
|
||||
buf.set_direction(if rtl { harfrust::Direction::RightToLeft } else { harfrust::Direction::LeftToRight });
|
||||
buf.guess_segment_properties();
|
||||
let shaped = shaper.shape(buf, harfrust::ShapeOptions::new());
|
||||
let k = 1000.0 / f64::from(shaper.units_per_em().max(1));
|
||||
shaped
|
||||
.glyph_infos()
|
||||
// Without a plan, shaping fails only on an already shaped buffer or an unset direction.
|
||||
if harfrust::shape(&shaper, &mut buf, harfrust::ShapeOptions::new()).is_err() {
|
||||
return Vec::new();
|
||||
}
|
||||
let k = 1000.0 / f64::from(font.units_per_em().max(1));
|
||||
buf.glyph_infos()
|
||||
.iter()
|
||||
.zip(shaped.glyph_positions())
|
||||
.zip(buf.glyph_positions())
|
||||
.map(|(gi, gp)| Glyph {
|
||||
gid: gi.glyph_id,
|
||||
cluster: usize::try_from(gi.cluster).unwrap_or(usize::MAX),
|
||||
@@ -291,6 +295,27 @@ mod tests {
|
||||
assert!((crate::line_width("قیمت 25 تومان", 1.0, 1.0) - sh.width).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn devanagari_run_is_reordered_with_line_clusters_and_advances_per_1000_em() {
|
||||
use skrifa::MetadataProvider;
|
||||
use skrifa::instance::{LocationRef, Size};
|
||||
let Some(face) = ttf::fallback_for('\u{0915}') else { return };
|
||||
let Some(ka) = nominal(&face, '\u{0915}') else { return };
|
||||
// "कि" after two other characters of the line: the vowel sign i is drawn before the
|
||||
// consonant (Indic reordering, which needs the script the shaper guesses from the text),
|
||||
// both glyphs carry the cluster of the consonant's index in the line, not in the run.
|
||||
let cs: Vec<char> = "ab\u{0915}\u{093F}".chars().collect();
|
||||
let gs = shape_run(&face, &cs, 2..4, false);
|
||||
let gids: Vec<u32> = gs.iter().map(|g| g.gid).collect();
|
||||
assert_eq!(gids.len(), 2, "{gids:?}");
|
||||
assert!(gids.first() != Some(&ka) && gids.last() == Some(&ka), "vowel sign before ka: {gids:?}");
|
||||
assert!(gs.iter().all(|g| g.cluster == 2), "{:?}", gs.iter().map(|g| g.cluster).collect::<Vec<_>>());
|
||||
// Advances come out at 1000 units per em, whatever the font's own em.
|
||||
let hmtx = face.font().and_then(|f| f.glyph_metrics(Size::new(1000.0), LocationRef::default()).advance_width(skrifa::GlyphId::new(ka)));
|
||||
let adv = gs.last().map(|g| g.advance);
|
||||
assert!(matches!((adv, hmtx), (Some(a), Some(h)) if (a - f64::from(h)).abs() < 1e-3), "{adv:?} vs {hmtx:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_rtl_lines_never_panic() {
|
||||
for s in ["\u{202E}%%u\u{05D0}%%", "\u{2067}\u{0644}\u{0627}\u{2069}%%o", "\u{064E}\u{064E}\u{05D0}", "\u{05D0}\u{E000}\u{10FFFF}"] {
|
||||
|
||||
Reference in New Issue
Block a user