diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 97eb641b..43da6abb 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -300,8 +300,7 @@ impl Exporter<'_> { } let Some(font) = self.font(&run.face) else { continue }; let n = run.glyphs.len(); - let char_starts: Vec = text.char_indices().map(|(b, _)| b).chain(std::iter::once(text.len())).collect(); - let one_to_one = char_starts.len() - 1 == n; + let ranges = glyph_text_ranges(&text, run.chars, &run.cells, n); let (x0, y0) = (run.glyphs[0].1, run.glyphs[0].2); let kg: Vec = run .glyphs @@ -309,13 +308,7 @@ impl Exporter<'_> { .enumerate() .map(|(gi, g)| { let next = run.glyphs.get(gi + 1).map_or(g.1 + run.face.advance(g.0) * run.size / run.face.upem.max(1.0), |nx| nx.1); - let range = if one_to_one { - char_starts[gi]..char_starts[gi + 1] - } else if gi == 0 { - 0..text.len() - } else { - text.len()..text.len() - }; + let range = ranges[gi].clone(); KrillaGlyph::new(GlyphId::new(g.0), ((next - g.1) / run.size) as f32, 0.0, ((g.2 - y0) / run.size) as f32, 0.0, range, None) }) .collect(); @@ -423,6 +416,20 @@ impl Exporter<'_> { } } +/// Byte range in `text` (the run's characters `chars`) of each of `n` glyphs, for the ToUnicode map. +/// `cells` holds each glyph's shaping-cluster head (paragraph character index); a cluster runs to the +/// next head in the run, so a ligature maps to all its characters and every glyph of a cluster shares +/// its range (krilla then writes the text once). +fn glyph_text_ranges(text: &str, chars: (usize, usize), cells: &[(usize, char)], n: usize) -> Vec> { + let starts: Vec = text.char_indices().map(|(b, _)| b).collect(); + let byte = |ci: usize| ci.checked_sub(chars.0).and_then(|i| starts.get(i).copied()).unwrap_or(text.len()); + let mut heads: Vec = cells.iter().map(|c| c.0).collect(); + heads.sort_unstable(); + heads.dedup(); + let end = |ci: usize| heads.get(heads.partition_point(|&h| h <= ci)).copied().unwrap_or(chars.1); + (0..n).map(|gi| cells.get(gi).map_or(text.len()..text.len(), |&(ci, _)| byte(ci)..byte(end(ci)))).collect() +} + fn to_path(bp: &kurbo::BezPath) -> Option { use kurbo::PathEl; let mut pb = krilla::geom::PathBuilder::new(); diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index 305f1457..ffc18b40 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -66,3 +66,13 @@ fn handout_boxes_fit_the_paper() { } } } + +#[test] +fn glyph_ranges_follow_shaping_clusters() { + // "fl" ligature: its glyph maps to both characters, the rest stay one to one (#164). + assert_eq!(glyph_text_ranges("flash", (0, 5), &[(0, 'f'), (2, 'a'), (3, 's'), (4, 'h')], 4), vec![0..2, 2..3, 3..4, 4..5]); + // Two glyphs in one cluster (base and mark) share its range. + assert_eq!(glyph_text_ranges("ab", (0, 2), &[(0, 'a'), (0, 'a'), (1, 'b')], 3), vec![0..1, 0..1, 1..2]); + // A run starting mid-paragraph, multi-byte characters in visual (right-to-left) order. + assert_eq!(glyph_text_ranges("αβγ", (3, 6), &[(5, 'γ'), (4, 'β'), (3, 'α')], 3), vec![4..6, 2..4, 0..2]); +}