1use crate::build::BuildContext;
23use crate::ops::Op;
24use crate::page::PageObject;
25use crate::resources::Resources;
26use kurbo::{Affine, Rect};
27use pdfrum_common::{Diagnostics, Limits};
28use pdfrum_font::{CharCode, Type3Font};
29use pdfrum_object::Resolve;
30
31const TEXT_UNIT_IN_GLYPH_UNITS: f64 = 1000.0;
33
34#[derive(Debug, Clone, PartialEq)]
37pub struct Type3Metrics {
38 pub width: f32,
40 pub bbox: Rect,
49 pub painted: Rect,
55 pub colored: bool,
64 pub objects: Vec<PageObject>,
72}
73
74#[must_use]
79pub fn metrics<R: Resolve>(
80 font: &Type3Font,
81 code: CharCode,
82 page_resources: Option<&pdfrum_object::Dict>,
83 r: &R,
84 ctx: &mut BuildContext,
85 limits: &Limits,
86 diags: &mut Diagnostics,
87) -> Option<Type3Metrics> {
88 let stream = font.char_proc(code, r)?;
89 let content = pdfrum_filters::decode_chain(&stream, 0, r, limits, diags).data;
90 let ops = crate::parse_content(&content, limits, diags);
91
92 let mut declared: Option<(f32, Option<Rect>, bool)> = None;
97 for op in &ops {
98 match op {
99 Op::Type3Width(wx, _) => {
100 declared = Some((*wx, None, true));
101 break;
102 }
103 Op::Type3WidthBBox(wx, _, llx, lly, urx, ury) => {
104 declared = Some((
105 *wx,
106 Some(Rect::new(
107 f64::from(*llx),
108 f64::from(*lly),
109 f64::from(*urx),
110 f64::from(*ury),
111 )),
112 false,
113 ));
114 break;
115 }
116 _ => {}
117 }
118 }
119 let (width, stated, colored) = declared.unwrap_or((0.0, None, false));
120
121 let objects = if ctx.enter_type3() {
130 let resources = Resources::choose(
131 font.resources.clone(),
132 page_resources.cloned(),
133 page_resources.cloned(),
134 );
135 let page = crate::build_page(&ops, &resources, r, ctx, limits, diags);
136 ctx.leave_type3();
137 page.objects
138 } else {
139 diags.record(
140 pdfrum_common::Severity::Recovered,
141 pdfrum_common::DiagKind::FormRecursionRefused,
142 None,
143 );
144 Vec::new()
145 };
146
147 let usable = stated.filter(|b| b.x1 > b.x0 && b.y1 > b.y0);
150 let painted = painted_extent(&objects);
151 let text_box = match usable {
152 Some(rect) => scale(rect, TEXT_UNIT_IN_GLYPH_UNITS),
153 None => scale(painted, TEXT_UNIT_IN_GLYPH_UNITS),
154 };
155
156 Some(Type3Metrics {
157 width: round(f64::from(width) * TEXT_UNIT_IN_GLYPH_UNITS),
158 bbox: transform_rect(font.font_matrix, text_box),
159 painted,
160 colored,
161 objects,
162 })
163}
164
165fn painted_extent(objects: &[PageObject]) -> Rect {
168 let mut out: Option<Rect> = None;
169 let mut add = |rect: Rect| {
170 out = Some(match out {
171 Some(current) => current.union(rect),
172 None => rect,
173 });
174 };
175 for object in objects {
176 match object {
177 PageObject::Path(content) => {
178 let path = &content.object;
179 add(transform_rect(
180 path.matrix,
181 kurbo::Shape::bounding_box(&path.path),
182 ));
183 }
184 PageObject::Image(content) => {
185 add(transform_rect(
186 content.object.matrix,
187 Rect::new(0.0, 0.0, 1.0, 1.0),
188 ));
189 }
190 PageObject::Shading(content) => add(content.object.bounds),
191 PageObject::Form(content) => {
192 let inner = painted_extent(&content.object.objects);
193 if inner.width() > 0.0 || inner.height() > 0.0 {
194 add(inner);
195 }
196 }
197 PageObject::Text(_) => {}
201 }
202 }
203 out.unwrap_or(Rect::ZERO)
204}
205
206fn scale(rect: Rect, factor: f64) -> Rect {
207 Rect::new(
208 rect.x0 * factor,
209 rect.y0 * factor,
210 rect.x1 * factor,
211 rect.y1 * factor,
212 )
213}
214
215fn transform_rect(matrix: Affine, rect: Rect) -> Rect {
216 let corners = [
217 matrix * kurbo::Point::new(rect.x0, rect.y0),
218 matrix * kurbo::Point::new(rect.x1, rect.y0),
219 matrix * kurbo::Point::new(rect.x0, rect.y1),
220 matrix * kurbo::Point::new(rect.x1, rect.y1),
221 ];
222 let xs = corners.map(|p| p.x);
223 let ys = corners.map(|p| p.y);
224 Rect::new(
225 xs.iter().copied().fold(f64::INFINITY, f64::min),
226 ys.iter().copied().fold(f64::INFINITY, f64::min),
227 xs.iter().copied().fold(f64::NEG_INFINITY, f64::max),
228 ys.iter().copied().fold(f64::NEG_INFINITY, f64::max),
229 )
230}
231
232fn round(value: f64) -> f32 {
234 #[expect(
235 clippy::cast_possible_truncation,
236 reason = "a glyph advance outside f32 is nonsense whatever we do"
237 )]
238 let rounded = value.round() as f32;
239 rounded
240}
241
242#[cfg(test)]
243mod tests {
244 #![allow(
248 clippy::float_cmp,
249 clippy::indexing_slicing,
250 clippy::unreadable_literal,
251 reason = "test fixtures quote oracle vectors verbatim and compare exactly"
252 )]
253
254 use super::*;
255
256 #[test]
257 fn a_text_space_box_scales_by_a_thousand() {
258 assert_eq!(
259 scale(Rect::new(0.0, 0.0, 0.5, 0.75), 1000.0),
260 Rect::new(0.0, 0.0, 500.0, 750.0)
261 );
262 }
263
264 #[test]
265 fn rounding_is_half_away_from_zero() {
266 assert_eq!(round(0.5), 1.0);
267 assert_eq!(round(-0.5), -1.0);
268 assert_eq!(round(1.4), 1.0);
269 assert_eq!(round(1.6), 2.0);
270 }
271
272 #[test]
273 fn a_procedure_that_paints_nothing_has_an_empty_extent() {
274 assert_eq!(painted_extent(&[]), Rect::ZERO);
275 }
276
277 struct OneStream(std::sync::Arc<pdfrum_object::Object>);
279
280 impl pdfrum_object::Resolve for OneStream {
281 fn fetch(
282 &self,
283 r: pdfrum_object::ObjRef,
284 ) -> Result<std::sync::Arc<pdfrum_object::Object>, pdfrum_object::Error> {
285 if r.num == 1 {
286 Ok(std::sync::Arc::clone(&self.0))
287 } else {
288 Err(pdfrum_object::Error::UnresolvedRef(r))
289 }
290 }
291 }
292
293 fn one_glyph_font(proc_body: &[u8]) -> (pdfrum_font::Font, OneStream) {
296 use pdfrum_object::{Array, ByteSpan, Dict, Name, Object, Stream};
297 let proc = Stream::new(Dict::new(), ByteSpan::from(proc_body.to_vec()));
298 let store = OneStream(std::sync::Arc::new(Object::Stream(Box::new(proc))));
299 let font = Dict::from_pairs([
300 (Name::from("Type"), Object::Name(Name::from("Font"))),
301 (Name::from("Subtype"), Object::Name(Name::from("Type3"))),
302 (
303 Name::from("CharProcs"),
304 Object::Dict(Dict::from_pairs([(
305 Name::from("g"),
306 Object::Ref(pdfrum_object::ObjRef {
307 num: 1,
308 generation: 0,
309 }),
310 )])),
311 ),
312 (
313 Name::from("Encoding"),
314 Object::Dict(Dict::from_pairs([(
315 Name::from("Differences"),
316 Object::Array(
317 [Object::Int(65), Object::Name(Name::from("g"))]
318 .into_iter()
319 .collect::<Array>(),
320 ),
321 )])),
322 ),
323 (Name::from("FirstChar"), Object::Int(65)),
324 (Name::from("LastChar"), Object::Int(65)),
325 (
326 Name::from("Widths"),
327 Object::Array([Object::Int(100)].into_iter().collect::<Array>()),
328 ),
329 ]);
330 let cache = pdfrum_font::FontCache::new();
331 let mut diags = Diagnostics::default();
332 let loaded = pdfrum_font::load(&font, &store, &cache, &Limits::default(), &mut diags)
333 .expect("a type 3 font");
334 (loaded, store)
335 }
336
337 fn glyph_metrics(proc_body: &[u8]) -> Type3Metrics {
338 let (font, store) = one_glyph_font(proc_body);
339 let type3 = font.type3().expect("a type 3 font");
340 let mut ctx = BuildContext::new();
341 let mut diags = Diagnostics::default();
342 metrics(
343 type3,
344 pdfrum_font::CharCode(65),
345 None,
346 &store,
347 &mut ctx,
348 &Limits::default(),
349 &mut diags,
350 )
351 .expect("the glyph names a procedure")
352 }
353
354 #[test]
355 fn a_d1_procedure_is_uncoloured_and_a_d0_one_is_coloured() {
356 assert!(!glyph_metrics(b"100 0 0 0 50 50 d1 0 0 50 50 re f").colored);
360 assert!(glyph_metrics(b"100 0 d0 1 0 0 rg 0 0 50 50 re f").colored);
361 assert!(!glyph_metrics(b"0 0 50 50 re f").colored);
364 }
365
366 #[test]
367 fn a_procedure_yields_the_objects_it_paints() {
368 let m = glyph_metrics(b"100 0 0 0 50 50 d1 0 0 50 50 re f");
371 assert_eq!(m.objects.len(), 1, "one rectangle: {:?}", m.objects);
372 assert!(matches!(m.objects.first(), Some(PageObject::Path(_))));
373 assert!(glyph_metrics(b"100 0 0 0 50 50 d1").objects.is_empty());
375 }
376
377 #[test]
378 fn a_declared_box_is_believed_and_a_degenerate_one_is_not() {
379 let stated = glyph_metrics(b"100 0 0 0 10 10 d1 0 0 40 40 re f");
382 assert_eq!(stated.bbox, Rect::new(0.0, 0.0, 10_000.0, 10_000.0));
383 let computed = glyph_metrics(b"100 0 10 10 0 0 d1 0 0 40 40 re f");
386 assert_eq!(computed.bbox, Rect::new(0.0, 0.0, 40_000.0, 40_000.0));
387 }
388
389 #[test]
390 fn the_advance_is_the_declared_width_in_glyph_units() {
391 assert_eq!(glyph_metrics(b"100 0 d0 0 0 1 1 re f").width, 100_000.0);
393 }
394
395 #[test]
396 fn the_painted_extent_is_the_objects_own_and_ignores_the_declared_box() {
397 let m = glyph_metrics(b"100 0 0 0 10 10 d1 0 0 40 40 re f");
403 assert_eq!(m.painted, Rect::new(0.0, 0.0, 40.0, 40.0));
404 assert_eq!(m.bbox, Rect::new(0.0, 0.0, 10_000.0, 10_000.0));
407 assert_eq!(glyph_metrics(b"100 0 0 0 10 10 d1").painted, Rect::ZERO);
410 }
411
412 #[test]
413 fn a_font_matrix_transforms_the_glyph_box() {
414 let matrix = Affine::new([0.001, 0.0, 0.0, 0.001, 0.0, 0.0]);
416 let out = transform_rect(matrix, Rect::new(0.0, 0.0, 1000.0, 1000.0));
417 assert_eq!(out, Rect::new(0.0, 0.0, 1.0, 1.0));
418 }
419}