Skip to main content

pdfboss_core/
measure.rs

1//! Measurement properties (ISO 32000-1 §12.9): a page's `/VP` viewports,
2//! each a rectangle with a measure dictionary that maps user space to
3//! real-world distances, read as data with the tables' defaults filled in.
4
5use crate::document::Page;
6use crate::geom::Rect;
7use crate::object::{Dict, Object};
8use crate::source::AsyncObjectSource;
9use crate::tree::{resolved_dict, Entries};
10
11/// A viewport (ISO 32000-1 §12.9, Table 260): a page rectangle with its own
12/// measurement scale. Where viewports overlap, the last one in the array
13/// whose box contains a point applies to it.
14#[derive(Debug, Clone, PartialEq)]
15pub struct Viewport {
16    /// `/BBox`: the rectangle in default user space, normalized.
17    pub bbox: Rect,
18    /// `/Name`: a descriptive title.
19    pub name: Option<String>,
20    /// `/Measure`: the units of the viewport's coordinate system; `None`
21    /// without one.
22    pub measure: Option<Measure>,
23}
24
25/// A measure dictionary (ISO 32000-1 §12.9, Tables 261 and 262): how
26/// distances, areas and angles in a viewport convert to real-world units.
27#[derive(Debug, Clone, PartialEq)]
28pub struct Measure {
29    /// `/Subtype`: `RL`, rectilinear, the one kind ISO 32000-1 defines and
30    /// the default; any other name is kept as written.
31    pub subtype: String,
32    /// `/R`: the scale ratio as text, `1in = 0.1 mi` style.
33    pub scale_ratio: Option<String>,
34    /// `/X`: the number formats for x distances, a chain from the coarsest
35    /// unit to the finest.
36    pub x: Vec<NumberFormat>,
37    /// `/Y`: the formats for y distances, empty when they share `x`'s.
38    pub y: Vec<NumberFormat>,
39    /// `/D`: the formats for distances.
40    pub distance: Vec<NumberFormat>,
41    /// `/A`: the formats for areas.
42    pub area: Vec<NumberFormat>,
43    /// `/T`: the formats for angles.
44    pub angle: Vec<NumberFormat>,
45    /// `/S`: the formats for slopes.
46    pub slope: Vec<NumberFormat>,
47    /// `/O`: the origin of the measurement coordinate system in default user
48    /// space, `(0, 0)` when absent.
49    pub origin: (f64, f64),
50    /// `/CYX`: the factor that converts y units to x units when the two
51    /// differ.
52    pub y_to_x: Option<f64>,
53}
54
55/// A number format (ISO 32000-1 §12.9, Table 263): one unit of a
56/// measurement chain and how its value is shown.
57#[derive(Debug, Clone, PartialEq)]
58pub struct NumberFormat {
59    /// `/U`: the unit label.
60    pub unit: String,
61    /// `/C`: the factor that converts the previous unit of the chain, user
62    /// space units for the first, into this one.
63    pub conversion: f64,
64    /// `/F`: how the fractional part is shown, decimal when absent.
65    pub fraction: FractionFormat,
66    /// `/D`: the places of a decimal or the denominator of a fraction, 100
67    /// when absent.
68    pub precision: u32,
69    /// `/FD`: whether a fraction keeps the denominator `precision` names
70    /// instead of being reduced.
71    pub fixed_denominator: bool,
72    /// `/RT`: the text between orders of thousands, a comma when absent.
73    pub thousands: String,
74    /// `/RD`: the decimal point text, a period when absent.
75    pub radix: String,
76    /// `/PS`: the text before the label, one space when absent.
77    pub prefix_spacing: String,
78    /// `/SS`: the text after the label, one space when absent.
79    pub suffix_spacing: String,
80    /// `/O`: where the label goes, after the value when absent.
81    pub label: LabelPosition,
82}
83
84/// `/F`: how a number format shows the fractional part of a value.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub enum FractionFormat {
87    /// `/D`: as a decimal to `precision` places, the default.
88    #[default]
89    Decimal,
90    /// `/F`: as a fraction over the denominator `precision` names.
91    Fraction,
92    /// `/R`: rounded to a whole number.
93    Round,
94    /// `/T`: truncated to a whole number.
95    Truncate,
96}
97
98impl FractionFormat {
99    /// The format `/F` names; `None` for a name the table does not list.
100    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/// `/O`: whether a number format's label follows or precedes the value.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
113pub enum LabelPosition {
114    /// `/S`: after the value, the default.
115    #[default]
116    Suffix,
117    /// `/P`: before the value.
118    Prefix,
119}
120
121impl LabelPosition {
122    /// The position `/O` names; `None` for any other name.
123    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
132/// The viewports of `page`'s `/VP` array, in order: empty without one. A
133/// viewport that is no dictionary, or whose `/BBox` is no array of four
134/// finite numbers, is skipped; a `/Measure` that is no dictionary reads as
135/// absent.
136///
137/// Covers ISO 32000-1 §12.9.
138pub 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
176/// Tables 261 and 262 with defaults: a subtype other than `RL` keeps its
177/// name and whatever rectilinear entries it carries.
178///
179/// Covers ISO 32000-1 §12.9.
180pub 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
206/// The number formats under `key`, in order; a format that is no
207/// dictionary, or lacks the required `/U` label or `/C` factor, is skipped.
208///
209/// Covers ISO 32000-1 §12.9.
210async 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
256/// The finite numbers of an array, each resolved; `None` when the value is
257/// no array or any item is no finite number.
258async 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    /// A one-page document whose page carries `page_extra`; object 20 is a
280    /// viewport in the manner of the clause's example, reachable by
281    /// reference.
282    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    /// The `/VP` array reads in order, by reference or written directly:
307    /// the bounding box (normalized), the name and the rectilinear measure
308    /// with its number formats, absent entries at Table 263's defaults.
309    // Covers ISO 32000-1 §12.9.
310    #[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    /// The optional measure entries: a `/Y` chain, an origin, the y-to-x
356    /// factor, a subtype other than RL kept by name, and the number
357    /// format's spacing, separator, position and fraction entries; a format
358    /// that is no dictionary or lacks `/U` or `/C` is skipped.
359    // Covers ISO 32000-1 §12.9.
360    #[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    /// A page without `/VP`, or whose `/VP` is no array, has no viewports;
388    /// a viewport that is no dictionary or has no four-number `/BBox` is
389    /// skipped, and a `/Measure` that is no dictionary reads as absent.
390    // Covers ISO 32000-1 §12.9.
391    #[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}