Skip to main content

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}