Merge pull request #6243 from z3r0pix3l/fix/6072-pdf-arabic-ligatures

PDF import: Arabic ligatures keep their letters in order (#6072)
This commit is contained in:
BFlat
2026-11-10 20:01:04 +07:00
committed by GitHub
2 changed files with 56 additions and 0 deletions
+16
View File
@@ -865,6 +865,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),
@@ -894,3 +897,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()
}
+40
View File
@@ -477,3 +477,43 @@ 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);
// Whether `face` draws lam-alef as one glyph, the ligature the fix is about.
let ligates = |face: &vectorcraft_text::FontFace| {
let style = vectorcraft_doc::CharStyle { font_family: face.family.clone(), font_style: face.style.clone(), ..Default::default() };
let t = TextObject::point(kurbo::Point::ORIGIN, "لا", style);
vectorcraft_text::layout(db, &t).glyphs.len() == 1
};
// An installed Arabic font with a lam-alef ligature 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}') && ligates(f))
.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);
}
}