pdfrum_edit/canvas/glyphs.rs
1//! Shaped glyph runs on a canvas: glyph IDs at the positions a layout engine
2//! chose, with the text each was shaped from.
3//!
4//! [`Canvas::text`] shapes nothing and places one string; a run from a real
5//! layout engine arrives already shaped, as glyph IDs with origins, and only
6//! the engine knows which characters each glyph stands for. A run is written
7//! as `TJ` arrays in a [`GlyphFont`], its positions exact: each glyph's
8//! advance is the one `/W` will carry, and the difference to where the layout
9//! put the next glyph goes into the array. The text goes into the font's
10//! `/ToUnicode` when the glyph can carry it there, and into `/ActualText`
11//! around the glyphs when it cannot — a cluster of several glyphs, or a glyph
12//! already mapped to other text.
13
14use std::fmt::Write as _;
15use std::ops::Range;
16
17use kurbo::Affine;
18use pdfrum_object::{Object, names as pdf_names};
19
20use super::{Canvas, Paint, write_f64};
21use crate::Error;
22use crate::font::glyph::{Claim, GlyphFont};
23use crate::write_matrix;
24
25/// One glyph of a run: which glyph, where its origin sits, and which bytes
26/// of the run's text it was shaped from.
27#[derive(Debug, Clone, PartialEq)]
28pub struct RunGlyph {
29 /// The glyph ID in the face.
30 pub id: u16,
31 /// The origin across, in run space: points at the run's size.
32 pub x: f64,
33 /// The origin up, in run space (y up, as text space is).
34 pub y: f64,
35 /// The cluster this glyph belongs to, as a byte range of
36 /// [`GlyphRun::text`]. Glyphs of one cluster share the range; a ligature
37 /// is one glyph whose range covers every letter it joins.
38 pub text: Range<usize>,
39}
40
41/// A shaped run, ready to draw with [`Canvas::glyphs`].
42#[derive(Debug, Clone)]
43pub struct GlyphRun<'a> {
44 /// The face, embedded by [`EditDoc::embed_glyph_font`](crate::EditDoc::embed_glyph_font)
45 /// in this session.
46 pub font: &'a GlyphFont,
47 /// The font size, in run-space units.
48 pub size: f64,
49 /// Run space to canvas space. Run space is text space: y up, glyphs
50 /// upright; a caller in a y-down space flips here.
51 pub transform: Affine,
52 /// The glyphs, in drawing order.
53 pub glyphs: &'a [RunGlyph],
54 /// The text the run was shaped from, which the glyphs' ranges index.
55 pub text: &'a str,
56 /// How the glyphs are painted: filled, stroked (text render mode 1), or
57 /// both (mode 2, a synthetic bold).
58 pub paint: Paint,
59}
60
61/// How one stretch of a run carries its text.
62#[derive(Debug, Clone, PartialEq, Eq)]
63enum Voice {
64 /// Through the font's `/ToUnicode`.
65 Mapped,
66 /// As `/ActualText` around the stretch.
67 Actual(String),
68}
69
70/// Consecutive glyphs written with one `Tm` and one `TJ`.
71#[derive(Debug)]
72struct Stretch {
73 glyphs: Range<usize>,
74 voice: Voice,
75}
76
77impl Canvas<'_, '_> {
78 /// Draw a shaped glyph run.
79 ///
80 /// Each glyph is drawn exactly at its origin; the text a reader copies
81 /// is the run's own, taken from each glyph's cluster range. See the
82 /// module documentation for how that text is written.
83 ///
84 /// # Errors
85 ///
86 /// [`Error::ForeignGlyphFont`] for a font another session embedded, and
87 /// [`Error::TooManyGlyphs`] past 65 535 distinct glyphs in one font, both
88 /// recorded and returned by the drawing call as for [`Canvas::text`].
89 ///
90 /// ```
91 /// use pdfrum_edit::{EditDoc, FontInstance, GlyphRun, Paint, RunGlyph, Size, blank_document};
92 /// use pdfrum_common::Limits;
93 /// use pdfrum_object::ByteSpan;
94 /// use kurbo::Affine;
95 /// use peniko::Color;
96 ///
97 /// let base = blank_document(&[Size::new(200.0, 100.0)])?;
98 /// let mut edit = EditDoc::new(&base);
99 /// let program = ByteSpan::from(include_bytes!("../../tests/files/tiny.ttf").to_vec());
100 /// let font = edit.embed_glyph_font(program, 0, FontInstance::Default)?;
101 /// let glyphs = [
102 /// RunGlyph { id: 1, x: 0.0, y: 0.0, text: 0..1 },
103 /// RunGlyph { id: 2, x: 12.0, y: 0.0, text: 1..2 },
104 /// ];
105 /// edit.draw_page(0, &Limits::default(), |c| {
106 /// c.glyphs(&GlyphRun {
107 /// font: &font,
108 /// size: 20.0,
109 /// transform: Affine::translate((20.0, 40.0)),
110 /// glyphs: &glyphs,
111 /// text: "ab",
112 /// paint: Paint::Fill(Color::BLACK),
113 /// });
114 /// })?;
115 /// # Ok::<(), pdfrum_edit::Error>(())
116 /// ```
117 pub fn glyphs(&mut self, run: &GlyphRun<'_>) {
118 if run.glyphs.is_empty() || !(run.size.is_finite() && run.size > 0.0) {
119 return;
120 }
121 let Some(face) = self.edit.glyph_face(run.font) else {
122 return self.fail(Error::ForeignGlyphFont);
123 };
124 let ids: Vec<u16> = run.glyphs.iter().map(|glyph| glyph.id).collect();
125 let cids = match face.cids(&ids) {
126 Ok(cids) => cids,
127 Err(error) => return self.fail(error),
128 };
129 let stretches = stretches(run, |cid, text| face.claim(cid, text), &cids);
130 let name = self.realize(pdf_names::FONT, Object::Ref(run.font.object()));
131 let mode = match run.paint {
132 Paint::Fill(_) => None,
133 Paint::Stroke(_) => Some(1),
134 Paint::FillStroke(..) => Some(2),
135 };
136 self.out.push_str("q\n");
137 self.set_paint(run.paint.clone());
138 self.out.push_str("BT\n/");
139 self.push_name(&name);
140 self.out.push(' ');
141 write_f64(&mut self.out, run.size);
142 self.out.push_str(" Tf\n");
143 if let Some(mode) = mode {
144 let _ = writeln!(self.out, "{mode} Tr");
145 }
146 for stretch in &stretches {
147 self.stretch(run, &cids, stretch);
148 }
149 self.out.push_str("ET\nQ\n");
150 }
151
152 /// Write one stretch: its `/ActualText` if any, its `Tm`, its `TJ`.
153 fn stretch(&mut self, run: &GlyphRun<'_>, cids: &[(u16, u32)], stretch: &Stretch) {
154 let (Some(first), Some(codes)) = (
155 run.glyphs.get(stretch.glyphs.start),
156 cids.get(stretch.glyphs.clone()),
157 ) else {
158 return;
159 };
160 if let Voice::Actual(text) = &stretch.voice {
161 self.out.push_str("/Span <</ActualText <FEFF");
162 for unit in text.encode_utf16() {
163 let _ = write!(self.out, "{unit:04X}");
164 }
165 self.out.push_str(">>> BDC\n");
166 }
167 write_matrix(
168 &mut self.out,
169 run.transform * Affine::translate((first.x, first.y)),
170 );
171 self.out.push_str(" Tm\n[");
172 let glyphs = run.glyphs.get(stretch.glyphs.clone()).unwrap_or_default();
173 for (at, ((cid, width), glyph)) in codes.iter().zip(glyphs).enumerate() {
174 let _ = write!(self.out, "<{cid:04X}>");
175 if let Some(next) = glyphs.get(at + 1) {
176 // TJ moves the pen back by the number, in thousandths of the
177 // size: the advance `/W` gives, less where the next glyph is.
178 let adjustment = f64::from(*width) - (next.x - glyph.x) * 1000.0 / run.size;
179 if adjustment.abs() > 1e-3 {
180 write_f64(&mut self.out, adjustment);
181 }
182 }
183 }
184 self.out.push_str("] TJ\n");
185 if matches!(stretch.voice, Voice::Actual(_)) {
186 self.out.push_str("EMC\n");
187 }
188 }
189}
190
191/// The run cut into stretches: one per change of baseline, and one per
192/// cluster that must carry its text as `/ActualText`. `claim` records a
193/// single glyph's text in the font and answers whether the font can say it.
194fn stretches(
195 run: &GlyphRun<'_>,
196 mut claim: impl FnMut(u16, &str) -> Claim,
197 cids: &[(u16, u32)],
198) -> Vec<Stretch> {
199 let mut out: Vec<Stretch> = Vec::new();
200 let mut start = 0;
201 while start < run.glyphs.len() {
202 let Some(range) = run.glyphs.get(start).map(|glyph| glyph.text.clone()) else {
203 break;
204 };
205 let end = run
206 .glyphs
207 .iter()
208 .skip(start)
209 .position(|glyph| glyph.text != range)
210 .map_or(run.glyphs.len(), |offset| start + offset);
211 let text = run.text.get(range).unwrap_or_default();
212 let voice = match (end - start, cids.get(start)) {
213 (1, Some((cid, _))) => match claim(*cid, text) {
214 Claim::Mapped => Voice::Mapped,
215 Claim::Taken => Voice::Actual(text.to_owned()),
216 },
217 _ if text.is_empty() => Voice::Mapped,
218 _ => Voice::Actual(text.to_owned()),
219 };
220 let same_line = |a: usize, b: usize| match (run.glyphs.get(a), run.glyphs.get(b)) {
221 (Some(a), Some(b)) => (a.y - b.y).abs() < 1e-9,
222 _ => false,
223 };
224 match out.last_mut() {
225 Some(last)
226 if voice == Voice::Mapped
227 && last.voice == Voice::Mapped
228 && same_line(last.glyphs.start, start)
229 && (start..end).all(|at| same_line(start, at)) =>
230 {
231 last.glyphs.end = end;
232 }
233 _ => {
234 // A stretch never spans a change of baseline: split this
235 // cluster's glyphs wherever `y` moves.
236 let mut from = start;
237 for at in start + 1..=end {
238 if at == end || !same_line(from, at) {
239 out.push(Stretch {
240 glyphs: from..at,
241 voice: voice.clone(),
242 });
243 from = at;
244 }
245 }
246 }
247 }
248 start = end;
249 }
250 out
251}