PDF import: Arabic ligatures keep their letters in order (#6072)

A PDF names a ligature's characters in logical order (lam then alef for
the lam-alef glyph), while a horizontal line of type is gathered glyph by
glyph in visual order and then put back in logical order character by
character. The ligature's characters were turned round with the line:
لا came in as ال, بلال as بالل and الله as هللا. Each glyph's
characters now go into the line in visual order, so putting the line back
restores them.
This commit is contained in:
z3r0pix3l
2026-11-08 21:13:54 +03:00
parent 0b0d2f0ab7
commit e71b4783f2
2 changed files with 50 additions and 0 deletions
+16
View File
@@ -842,6 +842,9 @@ impl<'p> Builder<'p> {
let vx = self.fonts.get(&key).and_then(|f| f.vertical.as_ref()).map_or(500.0, |v| v.centre(glyph));
m * kurbo::Point::new(vx, 880.0)
});
// A horizontal line is gathered in visual order and put back in logical order as a whole:
// a ligature's characters (lam-alef, lam-lam-heh) go in visual order too (#6072).
let text = if top.is_none() { visual_glyph_text(text) } else { text };
let place = |line: &mut TextLine| match top {
Some(top) => line.push_upright(&look, at, opacity, Upright { top }, &text),
None => line.push(&look, at, opacity, advance, &text),
@@ -871,3 +874,16 @@ impl<'p> Builder<'p> {
true
}
}
/// The characters of one glyph in the order they are drawn: a PDF names a ligature's characters
/// in logical order (lam then alef for لا), while a horizontal line of type is gathered glyph by
/// glyph in visual order and only then put back in logical order. Right-to-left characters are
/// turned round here so that putting the line back restores them.
fn visual_glyph_text(text: String) -> String {
if text.chars().nth(1).is_none() {
return text;
}
let Some((order, _)) = vectorcraft_text::logical_order(&text) else { return text };
let chars: Vec<char> = text.chars().collect();
order.iter().filter_map(|&i| chars.get(i)).collect()
}
+34
View File
@@ -477,3 +477,37 @@ fn hebrew_comes_back_in_logical_order() {
assert_eq!(shown(&t[0]), shown(&original), "{text}");
}
}
/// The text objects a PDF of `text` set in `face` imports as.
fn imported_texts(face: &vectorcraft_text::FontFace, text: &str) -> Vec<String> {
let mut pdf = krilla::Document::new();
let mut page = pdf.start_page_with(PageSettings::from_wh(300.0, 200.0).unwrap());
let mut s = page.surface();
let font = Font::new(face.file_data().to_vec().into(), face.face_index()).unwrap();
s.draw_text(KPoint::from_xy(20.0, 50.0), font, 24.0, text, false, TextDirection::Auto);
s.finish();
page.finish();
let r = import_with_report(&pdf.finish().unwrap(), &ImportOptions::default()).unwrap();
texts(&r.document).iter().map(|t| t.plain_text()).collect()
}
#[test]
fn arabic_ligatures_keep_their_letters_in_order() {
// A lam-alef ligature is one glyph whose text is lam then alef: it must not be turned round
// with the line it sits in (#6072).
let db = vectorcraft_text::FontDb::global();
let preferred = ["DejaVu Sans", "Arial", "Times New Roman", "Noto Sans Arabic", "Noto Naskh Arabic"].map(String::from);
// An installed Arabic font whose glyphs come back as live type.
let Some(face) = preferred
.iter()
.chain(&db.families())
.filter_map(|f| db.face(f, "Regular"))
.filter(|f| f.covers('\u{0644}') && f.covers('\u{0627}'))
.find(|f| !imported_texts(f, "بب").is_empty())
else {
return; // No such font on this host.
};
for text in ["لا", "بلال", "السلام عليكم", "لا إله إلا الله"] {
assert_eq!(imported_texts(&face, text), [text], "in {}", face.family);
}
}