1use crate::ops::TextRenderMode;
18use kurbo::{Affine, Point};
19use pdfrum_font::Font;
20use std::sync::Arc;
21
22#[derive(Debug, Clone)]
28pub struct TextState {
29 pub font: Option<(Arc<Font>, f32)>,
33 pub font_source: Option<pdfrum_object::ObjRef>,
37 pub char_space: f32,
39 pub word_space: f32,
41 pub horz_scale: f32,
43 pub leading: f32,
45 pub rise: f32,
47 pub render_mode: TextRenderMode,
49 pub stroke_ctm: [f32; 4],
58}
59
60impl PartialEq for TextState {
61 fn eq(&self, other: &Self) -> bool {
62 let same_font = match (&self.font, &other.font) {
63 (Some((a, sa)), Some((b, sb))) => a.id() == b.id() && sa == sb,
64 (None, None) => true,
65 _ => false,
66 };
67 same_font
68 && self.char_space == other.char_space
69 && self.word_space == other.word_space
70 && self.horz_scale == other.horz_scale
71 && self.leading == other.leading
72 && self.rise == other.rise
73 && self.render_mode == other.render_mode
74 && self.stroke_ctm == other.stroke_ctm
75 }
76}
77
78impl Default for TextState {
79 fn default() -> Self {
80 Self {
81 font: None,
82 font_source: None,
83 char_space: 0.0,
84 word_space: 0.0,
85 horz_scale: 1.0,
86 leading: 0.0,
87 rise: 0.0,
88 render_mode: TextRenderMode::Fill,
89 stroke_ctm: [1.0, 0.0, 0.0, 1.0],
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq)]
96pub struct TextCursor {
97 pub matrix: Affine,
99 pub pos: Point,
101 pub line_pos: Point,
103}
104
105impl Default for TextCursor {
106 fn default() -> Self {
107 Self {
108 matrix: Affine::IDENTITY,
109 pos: Point::ZERO,
110 line_pos: Point::ZERO,
111 }
112 }
113}
114
115impl TextCursor {
116 pub fn set_matrix(&mut self, matrix: Affine) {
118 self.matrix = matrix;
119 self.pos = Point::ZERO;
120 self.line_pos = Point::ZERO;
121 }
122
123 pub fn move_line(&mut self, tx: f64, ty: f64) {
128 self.line_pos.x += tx;
129 self.line_pos.y += ty;
130 self.pos = self.line_pos;
131 }
132
133 pub fn next_line(&mut self, leading: f64) {
135 self.line_pos.y -= leading;
136 self.pos = self.line_pos;
137 }
138
139 #[must_use]
144 pub fn device_position(&self, rise: f32, ctm: Affine) -> Point {
145 let in_text = Point::new(self.pos.x, self.pos.y + f64::from(rise));
146 ctm * (self.matrix * in_text)
147 }
148
149 pub fn advance(&mut self, amount: f64, vertical: bool) {
154 if vertical {
155 self.pos.y += amount;
156 } else {
157 self.pos.x += amount;
158 }
159 }
160}
161
162#[must_use]
166pub fn kerning_shift(kerning: f32, font_size: f32, horz_scale: f32, vertical: bool) -> f64 {
167 let base = f64::from(kerning) * f64::from(font_size) / 1000.0;
168 if vertical {
169 base
170 } else {
171 base * f64::from(horz_scale)
172 }
173}
174
175#[must_use]
180pub fn glyph_matrix(horz_scale: f32, text_matrix: Affine, ctm: Affine) -> Affine {
181 let scale = Affine::new([f64::from(horz_scale), 0.0, 0.0, 1.0, 0.0, 0.0]);
182 ctm * text_matrix * scale
183}
184
185#[cfg(test)]
186mod tests {
187 #![allow(
191 clippy::unreadable_literal,
192 clippy::float_cmp,
193 clippy::indexing_slicing,
194 clippy::cast_precision_loss,
195 clippy::cast_possible_truncation,
196 reason = "test fixtures quote oracle vectors verbatim and compare exactly"
197 )]
198
199 use super::{TextCursor, TextState, glyph_matrix, kerning_shift};
200 use crate::ops::TextRenderMode;
201 use kurbo::{Affine, Point};
202
203 #[test]
204 fn the_default_horizontal_scale_is_one_not_a_hundred() {
205 let s = TextState::default();
206 assert!((s.horz_scale - 1.0).abs() < 1e-6);
207 assert_eq!(s.render_mode, TextRenderMode::Fill);
208 assert_eq!(s.stroke_ctm, [1.0, 0.0, 0.0, 1.0]);
209 assert!(s.font.is_none());
210 }
211
212 #[test]
213 fn two_states_that_differ_only_in_stroke_ctm_are_unequal() {
214 let a = TextState::default();
215 let b = TextState {
216 stroke_ctm: [2.0, 0.0, 0.0, 3.0],
217 ..TextState::default()
218 };
219 assert_ne!(a, b);
220 }
221
222 #[test]
223 fn td_accumulates_onto_the_line_start() {
224 let mut c = TextCursor::default();
225 c.move_line(10.0, -14.0);
226 assert_eq!(c.pos, Point::new(10.0, -14.0));
227 assert_eq!(c.line_pos, Point::new(10.0, -14.0));
228 c.move_line(5.0, -14.0);
230 assert_eq!(c.pos, Point::new(15.0, -28.0));
231 }
232
233 #[test]
234 fn t_star_returns_to_the_line_start_one_leading_down() {
235 let mut c = TextCursor::default();
236 c.move_line(10.0, 0.0);
237 c.advance(50.0, false);
239 assert_eq!(c.pos.x, 60.0);
240 c.next_line(14.0);
241 assert_eq!(c.pos, Point::new(10.0, -14.0));
242 }
243
244 #[test]
245 fn setting_the_matrix_resets_both_positions() {
246 let mut c = TextCursor::default();
247 c.move_line(10.0, 20.0);
248 c.set_matrix(Affine::translate((100.0, 200.0)));
249 assert_eq!(c.pos, Point::ZERO);
250 assert_eq!(c.line_pos, Point::ZERO);
251 }
252
253 #[test]
254 fn the_rise_goes_through_the_text_matrix() {
255 let c = TextCursor {
256 matrix: Affine::rotate(std::f64::consts::FRAC_PI_2),
258 ..TextCursor::default()
259 };
260 let p = c.device_position(10.0, Affine::IDENTITY);
261 assert!(p.x.abs() > 9.0, "the rise should have been rotated: {p:?}");
262 assert!(p.y.abs() < 1e-6);
263
264 let flat = TextCursor::default().device_position(10.0, Affine::IDENTITY);
266 assert!((flat.y - 10.0).abs() < 1e-6);
267 }
268
269 #[test]
270 fn kerning_scales_by_size_and_by_the_horizontal_scale() {
271 assert!((kerning_shift(1000.0, 12.0, 1.0, false) - 12.0).abs() < 1e-6);
273 assert!((kerning_shift(1000.0, 12.0, 2.0, false) - 24.0).abs() < 1e-6);
274 assert!((kerning_shift(1000.0, 12.0, 2.0, true) - 12.0).abs() < 1e-6);
276 }
277
278 #[test]
279 fn the_glyph_matrix_applies_the_horizontal_scale_first() {
280 let m = glyph_matrix(2.0, Affine::IDENTITY, Affine::IDENTITY);
281 let p = m * Point::new(1.0, 1.0);
283 assert!((p.x - 2.0).abs() < 1e-6);
284 assert!((p.y - 1.0).abs() < 1e-6);
285 }
286
287 #[test]
288 fn vertical_writing_advances_y() {
289 let mut c = TextCursor::default();
290 c.advance(20.0, true);
291 assert_eq!(c.pos, Point::new(0.0, 20.0));
292 }
293}