Skip to main content

pdf_engine/
words_join.rs

1// Copyright (c) 2026 Innovation Trigger B.V.
2//
3// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
4// the PDFluent Commercial Licence. See the LICENSE file in this repository --
5// that file travels with the copy you received, which a URL does not.
6
7//! The rule that decides whether two adjacent pieces read as one word.
8//!
9//! One rule, three callers: the text assembly joins its pieces with it, the
10//! word assembly joins its spans with it, and the emphasis marks find their
11//! runs with it, so joining the words of a line with single spaces is always
12//! the line's own text. Every caller checks for whitespace between the pieces
13//! first and joins only when there is none.
14
15/// Whether two adjacent pieces with no whitespace between them read as one word.
16///
17/// `gap` is the horizontal distance in points from the end of the left piece
18/// to the start of the right piece -- negative where they overlap.
19/// `baseline_drift` is the absolute difference of their baselines in points.
20/// `em` is the larger of the two pieces' type sizes in points.
21///
22/// The pieces join when the gap is under 0.15 em and the drift under 0.3 em:
23/// a real inter-word space parts them, and so does a superscript riding off
24/// the baseline, while a kern split or a style change inside the word does not.
25///
26/// A page breaks one word across adjacent spans more often than it looks: a
27/// style change inside the word, a kerning split written as two `Tj`, a
28/// ligature fallback. Inter-word spaces in body type are around 0.25 em and
29/// more, while kerning gaps are well under 0.1 em, so 0.15 em sits between a
30/// real space and an intra-word split. A superscript rides a third of an em
31/// off the baseline and more, while one line's drift stays under it, so
32/// 0.3 em keeps `x2` two words without splitting a line that merely slopes.
33pub fn pieces_join(gap: f64, baseline_drift: f64, em: f64) -> bool {
34    gap < 0.15 * em && baseline_drift < 0.3 * em
35}
36
37/// Whether two neighbouring spans read as one word, whitespace included.
38///
39/// The boundary [`pieces_join`] draws, with the whitespace check every caller
40/// owes it: a blank at either edge parts the spans whatever the geometry says.
41/// The block rebuild and the emphasis marks share this, so a run the rebuild
42/// joined is a run the marks still find.
43pub(crate) fn spans_join(prev: &crate::text::TextSpan, curr: &crate::text::TextSpan) -> bool {
44    let spaced = prev.text.chars().last().is_some_and(char::is_whitespace)
45        || curr.text.chars().next().is_some_and(char::is_whitespace);
46    !spaced
47        && pieces_join(
48            curr.x - (prev.x + prev.width),
49            (curr.y - prev.y).abs(),
50            prev.font_size.max(curr.font_size),
51        )
52}