1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! A typographic ligature is one glyph for several letters, and `extract_text`
//! hands back the letters (#437, class R-G of #389).
//!
//! `office` set in a serif face is drawn with one `ffi` glyph, and a font that
//! carries a `ToUnicode` map for it usually names U+FB03 -- the code point for
//! the ligature -- rather than the three letters it stands for. Keeping that
//! code point is faithful to the file and useless to everyone downstream: a
//! search for `office`, a RAG chunk, a `grep`, an index all compare characters,
//! and U+FB03 is not `f` followed by `f` followed by `i`. mutool folds;
//! pdftotext does not, and its output is the reason a person who copies a word
//! out of a PDF cannot paste it back into a search box.
//!
//! So the extracted string folds, and the option to keep the code points rides
//! on the extraction device ([`crate::text::TextExtractionDevice::keep_ligatures`],
//! reachable from [`crate::PdfDocument::set_keep_ligatures`]). Structured
//! output is not folded at all: a [`crate::TextBlock`] span carries the
//! characters of the glyphs it was assembled from, because a caller who asked
//! for glyph-level geometry asked for the glyphs.
//!
//! WHAT IS FOLDED, AND WHAT IS DELIBERATELY NOT:
//!
//! - The ligatures of the Alphabetic Presentation Forms block, and only those:
//! U+FB00-FB06 (Latin) and U+FB13-FB17 (Armenian). U+FB07-FB12 are
//! unassigned, so the two ranges together are every ligature the block has.
//! - U+FB05 `ſt` folds to `st`, not to NFKC's `ſt`. The long s is a second
//! presentation form of the same letter and has the same problem the
//! ligature has: nobody types it. This is the one place the table departs
//! from NFKC, and it departs in the direction the rest of the table goes.
//! - No other compatibility folding. NFKC also rewrites superscripts,
//! fractions, full-width forms and the Arabic presentation forms, and each of
//! those changes what a document says rather than how it was set: `2³` is not
//! `23`, and `½` is not `1/2` in a table of measurements. A ligature is the
//! only compatibility mapping where the glyph carries no meaning the letters
//! do not.
//! - The Hebrew ligature U+FB4F `ﭏ` is outside both ranges and stays: it sits
//! among the Hebrew presentation forms, where the neighbouring code points
//! are pointed letters that must not be folded.
// Copyright (c) 2026 Innovation Trigger B.V.
//
// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
// the PDFluent Commercial Licence. See the LICENSE file in this repository --
// that file travels with the copy you received, which a URL does not.
use Cow;
/// The letters a ligature stands for, or `None` for every other character.
///
/// Latin first (U+FB00-FB06), then Armenian (U+FB13-FB17). The Armenian
/// expansions are the NFKC ones; the Latin ones are too, apart from U+FB05,
/// which the module docs explain.
pub const
/// Replace every typographic ligature in `text` with the letters it stands for.
///
/// Borrows when there is nothing to fold, which is every page of almost every
/// document: the scan is one pass and the common answer costs no allocation.
pub