Skip to main content

otf_pdf_content/
device.rs

1//! What the interpreter reports: the [`Device`] trait and what it receives,
2//! and the [`Font`] trait the interpreter needs to lay out text.
3
4use std::fmt;
5use std::sync::Arc;
6
7use otf_pdf_doc::Store;
8use otf_pdf_syntax::{Dict, Name, ObjId, Object};
9
10use crate::matrix::Matrix;
11
12/// A loaded font, as the interpreter needs it: how a string splits into
13/// character codes, how far each code advances, and (for extraction) what
14/// text it stands for. Implemented by `otf-pdf-font`.
15pub trait Font: Send + Sync + fmt::Debug {
16    /// Splits a shown string into `(code, byte length)` pairs.
17    fn codes(&self, bytes: &[u8]) -> Vec<(u32, usize)>;
18    /// Horizontal advance of `code`, in text space units per unit of font
19    /// size (glyph width / 1000 for most fonts).
20    fn width(&self, code: u32) -> f64;
21    /// Whether the font is written vertically (an `-V` CMap).
22    fn is_vertical(&self) -> bool {
23        false
24    }
25    /// Vertical advance of `code` in vertical writing (negative is down).
26    fn vertical_advance(&self, _code: u32) -> f64 {
27        -1.0
28    }
29    /// The Unicode text for `code`, if known.
30    fn unicode(&self, code: u32) -> Option<String>;
31    /// The font's name (`/BaseFont`), for reporting.
32    fn name(&self) -> &str;
33}
34
35/// Loads fonts from font dictionaries; implementations cache them.
36pub trait FontLoader {
37    /// The font for `font` (a reference to, or a, font dictionary).
38    fn load(&self, store: &Store, font: &Object) -> Option<Arc<dyn Font>>;
39}
40
41/// The text state (§9.3).
42#[derive(Debug, Clone)]
43pub struct TextState {
44    /// The current font.
45    pub font: Option<Arc<dyn Font>>,
46    /// Font size (`Tf`).
47    pub size: f64,
48    /// Character spacing (`Tc`).
49    pub char_spacing: f64,
50    /// Word spacing (`Tw`).
51    pub word_spacing: f64,
52    /// Horizontal scaling as a fraction (`Tz` / 100).
53    pub horizontal_scaling: f64,
54    /// Leading (`TL`).
55    pub leading: f64,
56    /// Rise (`Ts`).
57    pub rise: f64,
58    /// Rendering mode (`Tr`); 3 is invisible (OCR text layers).
59    pub render_mode: i64,
60}
61
62impl Default for TextState {
63    fn default() -> Self {
64        Self {
65            font: None,
66            size: 0.0,
67            char_spacing: 0.0,
68            word_spacing: 0.0,
69            horizontal_scaling: 1.0,
70            leading: 0.0,
71            rise: 0.0,
72            render_mode: 0,
73        }
74    }
75}
76
77/// The parts of the graphics state the interpreter tracks.
78#[derive(Debug, Clone)]
79pub struct GraphicsState {
80    /// Current transformation matrix: user space to page space.
81    pub ctm: Matrix,
82    /// Line width.
83    pub line_width: f64,
84    /// Text state.
85    pub text: TextState,
86    /// The clip, approximated by its bounding box in page space
87    /// (`[x0, y0, x1, y1]`); `None` is unclipped. Form and annotation
88    /// bounding boxes and `W`/`W*` paths narrow it.
89    pub clip: Option<[f64; 4]>,
90}
91
92impl GraphicsState {
93    /// Narrows the clip to the page-space bounding box of `points`
94    /// (user space).
95    pub fn clip_to(&mut self, points: impl IntoIterator<Item = (f64, f64)>) {
96        let mut b = [f64::MAX, f64::MAX, f64::MIN, f64::MIN];
97        for (x, y) in points {
98            let (px, py) = self.ctm.apply(x, y);
99            b = [b[0].min(px), b[1].min(py), b[2].max(px), b[3].max(py)];
100        }
101        if b[0] > b[2] {
102            return;
103        }
104        self.clip = Some(match self.clip {
105            Some(c) => [
106                c[0].max(b[0]),
107                c[1].max(b[1]),
108                c[2].min(b[2]),
109                c[3].min(b[3]),
110            ],
111            None => b,
112        });
113    }
114
115    /// Whether a page-space point is inside the clip (with a tolerance of
116    /// `slack` points).
117    pub fn in_clip(&self, (x, y): (f64, f64), slack: f64) -> bool {
118        self.clip.is_none_or(|c| {
119            x >= c[0] - slack && x <= c[2] + slack && y >= c[1] - slack && y <= c[3] + slack
120        })
121    }
122}
123
124impl Default for GraphicsState {
125    fn default() -> Self {
126        Self {
127            ctm: Matrix::IDENTITY,
128            line_width: 1.0,
129            text: TextState::default(),
130            clip: None,
131        }
132    }
133}
134
135/// One shown glyph.
136#[derive(Debug, Clone)]
137pub struct Glyph {
138    /// The character code.
139    pub code: u32,
140    /// Its Unicode text, if the font knows it.
141    pub unicode: Option<String>,
142    /// The font.
143    pub font: Arc<dyn Font>,
144    /// Text rendering matrix: glyph space (scaled by the font size) to page
145    /// space.
146    pub trm: Matrix,
147    /// Where the glyph starts on the baseline, in page space.
148    pub origin: (f64, f64),
149    /// Where the next glyph would start, in page space.
150    pub end: (f64, f64),
151    /// The font size in page space (the height of one em).
152    pub size: f64,
153    /// Text rendering mode.
154    pub render_mode: i64,
155}
156
157/// A path segment, in user space.
158#[derive(Debug, Clone, Copy, PartialEq)]
159pub enum PathSegment {
160    /// Start a subpath.
161    MoveTo(f64, f64),
162    /// Straight line.
163    LineTo(f64, f64),
164    /// Cubic Bézier curve: two control points and the end point.
165    CurveTo(f64, f64, f64, f64, f64, f64),
166    /// Close the subpath.
167    Close,
168}
169
170/// How a path's inside is decided.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum FillRule {
173    /// Nonzero winding.
174    NonZero,
175    /// Even-odd.
176    EvenOdd,
177}
178
179/// An image to draw: an XObject or an inline image.
180#[derive(Debug, Clone)]
181pub struct Image {
182    /// The image XObject's id, or `None` for an inline image.
183    pub id: Option<ObjId>,
184    /// The image dictionary.
185    pub dict: Dict,
186    /// The inline image's (still encoded) data; empty for an XObject.
187    pub inline_data: Vec<u8>,
188}
189
190/// Receives what a content stream draws. Every method has an empty default,
191/// so a device implements only what it needs: text extraction takes glyphs
192/// and marked content; a renderer takes everything.
193#[allow(unused_variables)]
194pub trait Device {
195    /// Fills `path` (user space) with the current fill colour.
196    fn fill_path(&mut self, path: &[PathSegment], rule: FillRule, state: &GraphicsState) {}
197    /// Strokes `path` (user space).
198    fn stroke_path(&mut self, path: &[PathSegment], state: &GraphicsState) {}
199    /// Intersects the clip with `path`.
200    fn clip(&mut self, path: &[PathSegment], rule: FillRule, state: &GraphicsState) {}
201    /// Draws one glyph.
202    fn draw_glyph(&mut self, glyph: &Glyph, state: &GraphicsState) {}
203    /// Draws an image into the unit square of `state.ctm`.
204    fn draw_image(&mut self, image: &Image, state: &GraphicsState) {}
205    /// Starts a marked-content sequence (`BMC`/`BDC`).
206    fn begin_marked_content(&mut self, tag: &Name, properties: Option<&Dict>) {}
207    /// Ends the innermost marked-content sequence (`EMC`).
208    fn end_marked_content(&mut self) {}
209    /// Starts a Form XObject (a group when it has `/Group`).
210    fn begin_group(&mut self, form: Option<ObjId>, dict: &Dict, state: &GraphicsState) {}
211    /// Ends the Form XObject started last.
212    fn end_group(&mut self) {}
213}