1use crate::document::Page;
6use crate::geom::Rect;
7use crate::object::{Dict, Object};
8use crate::source::AsyncObjectSource;
9use crate::tree::{resolved_dict, Entries};
10
11#[derive(Debug, Clone, PartialEq)]
15pub struct Viewport {
16 pub bbox: Rect,
18 pub name: Option<String>,
20 pub measure: Option<Measure>,
23}
24
25#[derive(Debug, Clone, PartialEq)]
28pub struct Measure {
29 pub subtype: String,
32 pub scale_ratio: Option<String>,
34 pub x: Vec<NumberFormat>,
37 pub y: Vec<NumberFormat>,
39 pub distance: Vec<NumberFormat>,
41 pub area: Vec<NumberFormat>,
43 pub angle: Vec<NumberFormat>,
45 pub slope: Vec<NumberFormat>,
47 pub origin: (f64, f64),
50 pub y_to_x: Option<f64>,
53}
54
55#[derive(Debug, Clone, PartialEq)]
58pub struct NumberFormat {
59 pub unit: String,
61 pub conversion: f64,
64 pub fraction: FractionFormat,
66 pub precision: u32,
69 pub fixed_denominator: bool,
72 pub thousands: String,
74 pub radix: String,
76 pub prefix_spacing: String,
78 pub suffix_spacing: String,
80 pub label: LabelPosition,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub enum FractionFormat {
87 #[default]
89 Decimal,
90 Fraction,
92 Round,
94 Truncate,
96}
97
98impl FractionFormat {
99 pub fn from_name(name: &str) -> Option<FractionFormat> {
101 match name {
102 "D" => Some(FractionFormat::Decimal),
103 "F" => Some(FractionFormat::Fraction),
104 "R" => Some(FractionFormat::Round),
105 "T" => Some(FractionFormat::Truncate),
106 _ => None,
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
113pub enum LabelPosition {
114 #[default]
116 Suffix,
117 Prefix,
119}
120
121impl LabelPosition {
122 pub fn from_name(name: &str) -> Option<LabelPosition> {
124 match name {
125 "S" => Some(LabelPosition::Suffix),
126 "P" => Some(LabelPosition::Prefix),
127 _ => None,
128 }
129 }
130}
131
132pub async fn viewports_with<S: AsyncObjectSource>(src: &S, page: &Page) -> Vec<Viewport> {
139 let Some(entry) = page.dict().get("VP") else {
140 return Vec::new();
141 };
142 let Ok(Object::Array(entries)) = src.resolve(entry).await else {
143 return Vec::new();
144 };
145 let mut viewports = Vec::new();
146 for entry in &entries {
147 let Some(dict) = resolved_dict(src, entry).await else {
148 continue;
149 };
150 let viewport = Entries { src, dict: &dict };
151 let bbox = match numbers_of(src, viewport.value("BBox").await)
152 .await
153 .as_deref()
154 {
155 Some(&[x0, y0, x1, y1]) => {
156 Rect::new(x0 as f32, y0 as f32, x1 as f32, y1 as f32).normalize()
157 }
158 _ => continue,
159 };
160 let measure = match dict.get("Measure") {
161 Some(value) => match resolved_dict(src, value).await {
162 Some(measure) => Some(measure_from(src, &measure).await),
163 None => None,
164 },
165 None => None,
166 };
167 viewports.push(Viewport {
168 bbox,
169 name: viewport.text("Name").await,
170 measure,
171 });
172 }
173 viewports
174}
175
176pub async fn measure_from<S: AsyncObjectSource>(src: &S, dict: &Dict) -> Measure {
181 let entries = Entries { src, dict };
182 let origin = match numbers_of(src, entries.value("O").await).await.as_deref() {
183 Some(&[x, y]) => (x, y),
184 _ => (0.0, 0.0),
185 };
186 Measure {
187 subtype: entries
188 .value("Subtype")
189 .await
190 .as_ref()
191 .and_then(Object::as_name)
192 .map(|name| name.0.clone())
193 .unwrap_or_else(|| "RL".into()),
194 scale_ratio: entries.text("R").await,
195 x: formats_of(src, dict, "X").await,
196 y: formats_of(src, dict, "Y").await,
197 distance: formats_of(src, dict, "D").await,
198 area: formats_of(src, dict, "A").await,
199 angle: formats_of(src, dict, "T").await,
200 slope: formats_of(src, dict, "S").await,
201 origin,
202 y_to_x: entries.value("CYX").await.and_then(|value| value.as_f64()),
203 }
204}
205
206async fn formats_of<S: AsyncObjectSource>(src: &S, dict: &Dict, key: &str) -> Vec<NumberFormat> {
211 let Some(value) = Entries { src, dict }.value(key).await else {
212 return Vec::new();
213 };
214 let Some(items) = value.as_array() else {
215 return Vec::new();
216 };
217 let mut formats = Vec::new();
218 for item in items {
219 let Some(dict) = resolved_dict(src, item).await else {
220 continue;
221 };
222 let format = Entries { src, dict: &dict };
223 let Some(unit) = format.text("U").await else {
224 continue;
225 };
226 let Some(conversion) = format.value("C").await.and_then(|value| value.as_f64()) else {
227 continue;
228 };
229 formats.push(NumberFormat {
230 unit,
231 conversion,
232 fraction: format
233 .named("F", FractionFormat::from_name)
234 .await
235 .unwrap_or_default(),
236 precision: format
237 .value("D")
238 .await
239 .and_then(|value| value.as_int())
240 .and_then(|value| u32::try_from(value).ok())
241 .unwrap_or(100),
242 fixed_denominator: format.flag("FD").await.unwrap_or(false),
243 thousands: format.text("RT").await.unwrap_or_else(|| ",".into()),
244 radix: format.text("RD").await.unwrap_or_else(|| ".".into()),
245 prefix_spacing: format.text("PS").await.unwrap_or_else(|| " ".into()),
246 suffix_spacing: format.text("SS").await.unwrap_or_else(|| " ".into()),
247 label: format
248 .named("O", LabelPosition::from_name)
249 .await
250 .unwrap_or_default(),
251 });
252 }
253 formats
254}
255
256async fn numbers_of<S: AsyncObjectSource>(src: &S, value: Option<Object>) -> Option<Vec<f64>> {
259 let value = value?;
260 let items = value.as_array()?;
261 let mut numbers = Vec::with_capacity(items.len());
262 for item in items {
263 let number = src.resolve(item).await.ok()?.as_f64()?;
264 if !number.is_finite() {
265 return None;
266 }
267 numbers.push(number);
268 }
269 Some(numbers)
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use crate::document::Document;
276 use crate::geom::Rect;
277 use pdfboss_testkit::PdfBuilder;
278
279 fn doc_with(page_extra: &str) -> Document {
283 let mut b = PdfBuilder::new();
284 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
285 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
286 b.object(
287 3,
288 &format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] {page_extra} >>"),
289 );
290 b.object(
291 20,
292 "<< /Type /Viewport /BBox [0 0 612 792] /Name (Map) \
293 /Measure << /Type /Measure /Subtype /RL /R (1in = 0.1 mi) \
294 /X [ << /Type /NumberFormat /U (mi) /C 0.00139 /D 100000 >> ] \
295 /D [ << /U (mi) /C 1 >> << /U (feet) /C 5280 /F /F /D 8 >> ] \
296 /A [ << /U (acres) /C 640 >> ] >> >>",
297 );
298 Document::load(b.build(1)).unwrap()
299 }
300
301 fn viewports(page_extra: &str) -> Vec<Viewport> {
302 let doc = doc_with(page_extra);
303 doc.viewports(&doc.page(0).unwrap())
304 }
305
306 #[test]
311 fn reads_viewports_with_their_measures_and_the_tables_defaults() {
312 let viewports = viewports("/VP [ 20 0 R << /BBox [100 10 10 100] >> ]");
313 assert_eq!(viewports.len(), 2);
314 let map = &viewports[0];
315 assert_eq!(map.bbox, Rect::new(0.0, 0.0, 612.0, 792.0));
316 assert_eq!(map.name.as_deref(), Some("Map"));
317 let measure = map.measure.as_ref().unwrap();
318 assert_eq!(measure.subtype, "RL");
319 assert_eq!(measure.scale_ratio.as_deref(), Some("1in = 0.1 mi"));
320 assert_eq!(
321 measure.x,
322 [NumberFormat {
323 unit: "mi".into(),
324 conversion: 0.00139,
325 fraction: FractionFormat::Decimal,
326 precision: 100000,
327 fixed_denominator: false,
328 thousands: ",".into(),
329 radix: ".".into(),
330 prefix_spacing: " ".into(),
331 suffix_spacing: " ".into(),
332 label: LabelPosition::Suffix,
333 }]
334 );
335 assert_eq!(measure.distance.len(), 2);
336 assert_eq!(measure.distance[0].unit, "mi");
337 assert_eq!(measure.distance[0].conversion, 1.0);
338 assert_eq!(measure.distance[1].unit, "feet");
339 assert_eq!(measure.distance[1].conversion, 5280.0);
340 assert_eq!(measure.distance[1].fraction, FractionFormat::Fraction);
341 assert_eq!(measure.distance[1].precision, 8);
342 assert_eq!(measure.area.len(), 1);
343 assert_eq!(measure.area[0].unit, "acres");
344 assert!(measure.y.is_empty());
345 assert!(measure.angle.is_empty());
346 assert!(measure.slope.is_empty());
347 assert_eq!(measure.origin, (0.0, 0.0));
348 assert_eq!(measure.y_to_x, None);
349 let plain = &viewports[1];
350 assert_eq!(plain.bbox, Rect::new(10.0, 10.0, 100.0, 100.0));
351 assert_eq!(plain.name, None);
352 assert_eq!(plain.measure, None);
353 }
354
355 #[test]
361 fn reads_the_optional_measure_entries() {
362 let viewports = viewports(
363 "/VP [ << /BBox [0 0 1 1] /Measure << /Subtype /GEO /Y [ << /U (m) /C 2 >> ] \
364 /O [5 6] /CYX 2 /T [ << /U (deg) /C 1 /O /P /RT (.) /RD (,) /PS () /SS () \
365 /FD true /F /R >> ] /S [ 7 << /U (%) >> << /C 1 >> ] /D [ << /U (m) /C 1 /F /Weird >> ] >> >> ]",
366 );
367 let measure = viewports[0].measure.as_ref().unwrap();
368 assert_eq!(measure.subtype, "GEO");
369 assert_eq!(measure.scale_ratio, None);
370 assert_eq!(measure.y.len(), 1);
371 assert_eq!(measure.y[0].unit, "m");
372 assert_eq!(measure.y[0].precision, 100);
373 assert_eq!(measure.origin, (5.0, 6.0));
374 assert_eq!(measure.y_to_x, Some(2.0));
375 let angle = &measure.angle[0];
376 assert_eq!(angle.label, LabelPosition::Prefix);
377 assert_eq!(angle.thousands, ".");
378 assert_eq!(angle.radix, ",");
379 assert_eq!(angle.prefix_spacing, "");
380 assert_eq!(angle.suffix_spacing, "");
381 assert!(angle.fixed_denominator);
382 assert_eq!(angle.fraction, FractionFormat::Round);
383 assert!(measure.slope.is_empty());
384 assert_eq!(measure.distance[0].fraction, FractionFormat::Decimal);
385 }
386
387 #[test]
392 fn missing_or_malformed_viewports_read_as_none() {
393 assert!(viewports("").is_empty());
394 assert!(viewports("/VP 5").is_empty());
395 let odd = viewports(
396 "/VP [ 7 << /Name (no box) >> << /BBox [0 0 1] >> << /BBox [0 0 5 5] /Measure 3 >> ]",
397 );
398 assert_eq!(odd.len(), 1);
399 assert_eq!(odd[0].bbox, Rect::new(0.0, 0.0, 5.0, 5.0));
400 assert_eq!(odd[0].measure, None);
401 }
402}