Skip to main content

pdfrum_edit/content/
num.rs

1//! Number spelling inside content streams (ISO 32000 §7.3.3).
2//!
3//! Four writers, each producing exactly the digits and nothing else: **no
4//! leading space, no trailing space, no `+` on positives**. Every caller
5//! supplies its own separator, which is why the operator emitters below read
6//! `write_float(out, w); out.push_str(" w ")` rather than composing padded
7//! fragments.
8//!
9//! The digits themselves come from [`pdfrum_object::fmt_number`], the
10//! shortest-round-trip spelling the whole workspace shares; these functions
11//! add only the layout.
12
13use pdfrum_common::kurbo::{Affine, Point, Rect};
14use pdfrum_object::fmt_number;
15
16/// Append one number: `10.5`, `.29`, `-7`, `0`.
17///
18/// ```
19/// let mut out = String::new();
20/// pdfrum_edit::write_float(&mut out, 0.5);
21/// assert_eq!(out, ".5");
22/// ```
23pub fn write_float(out: &mut String, value: f32) {
24    out.push_str(&fmt_number(value));
25}
26
27/// Append a matrix as `a b c d e f` — single spaces, no brackets, no trailing
28/// space. This is the operand list of `cm` and `Tm`.
29///
30/// ```
31/// use pdfrum_common::kurbo::Affine;
32///
33/// let mut out = String::new();
34/// pdfrum_edit::write_matrix(&mut out, Affine::new([1.0, 0.0, 0.0, 1.0, 10.5, 20.25]));
35/// assert_eq!(out, "1 0 0 1 10.5 20.25");
36/// ```
37pub fn write_matrix(out: &mut String, m: Affine) {
38    let c = m.as_coeffs();
39    for (i, v) in c.iter().enumerate() {
40        if i > 0 {
41            out.push(' ');
42        }
43        #[expect(
44            clippy::cast_possible_truncation,
45            reason = "PDF numbers are f32; the geometry vocabulary is f64"
46        )]
47        write_float(out, *v as f32);
48    }
49}
50
51/// Append a point as `x y`.
52///
53/// ```
54/// use pdfrum_common::kurbo::Point;
55///
56/// let mut out = String::new();
57/// pdfrum_edit::write_point(&mut out, Point::new(1.0, 2.5));
58/// assert_eq!(out, "1 2.5");
59/// ```
60pub fn write_point(out: &mut String, p: Point) {
61    #[expect(
62        clippy::cast_possible_truncation,
63        reason = "PDF numbers are f32; the geometry vocabulary is f64"
64    )]
65    {
66        write_float(out, p.x as f32);
67        out.push(' ');
68        write_float(out, p.y as f32);
69    }
70}
71
72/// Append a rectangle as `left bottom width height` — **not** the
73/// `x0 y0 x1 y1` an array-valued `/MediaBox` uses. This is the operand list of
74/// `re`, so the last two numbers are extents and may be negative.
75///
76/// ```
77/// use pdfrum_common::kurbo::Rect;
78///
79/// let mut out = String::new();
80/// pdfrum_edit::write_rect(&mut out, Rect::new(1.0, 2.0, 10.0, 20.0));
81/// assert_eq!(out, "1 2 9 18");
82/// ```
83pub fn write_rect(out: &mut String, r: Rect) {
84    #[expect(
85        clippy::cast_possible_truncation,
86        reason = "PDF numbers are f32; the geometry vocabulary is f64"
87    )]
88    {
89        write_float(out, r.x0 as f32);
90        out.push(' ');
91        write_float(out, r.y0 as f32);
92        out.push(' ');
93        write_float(out, (r.x1 - r.x0) as f32);
94        out.push(' ');
95        write_float(out, (r.y1 - r.y0) as f32);
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::{write_float, write_matrix, write_point, write_rect};
102    use pdfrum_common::kurbo::{Affine, Point, Rect};
103
104    fn s(f: impl FnOnce(&mut String)) -> String {
105        let mut out = String::new();
106        f(&mut out);
107        out
108    }
109
110    // From cpdf_contentstream_write_utils_unittest.cpp:26-69, restated for the
111    // no-space contract: these writers add layout, never padding.
112    #[test]
113    fn floats_carry_no_space_and_no_sign() {
114        assert_eq!(s(|o| write_float(o, 0.0)), "0");
115        assert_eq!(s(|o| write_float(o, 1.0)), "1");
116        assert_eq!(s(|o| write_float(o, -1.0)), "-1");
117        assert_eq!(s(|o| write_float(o, 0.5)), ".5");
118        assert_eq!(s(|o| write_float(o, -0.5)), "-.5");
119        assert_eq!(s(|o| write_float(o, 10.5)), "10.5");
120    }
121
122    #[test]
123    fn matrix_is_six_space_separated_numbers() {
124        assert_eq!(
125            s(|o| write_matrix(o, Affine::new([1.0, 0.0, 0.0, 1.0, 10.5, 20.25]))),
126            "1 0 0 1 10.5 20.25"
127        );
128    }
129
130    #[test]
131    fn point_is_two_numbers() {
132        assert_eq!(s(|o| write_point(o, Point::new(1.0, 2.5))), "1 2.5");
133    }
134
135    // The one that surprises: WriteRect emits extents, not corners.
136    #[test]
137    fn rect_is_left_bottom_width_height() {
138        assert_eq!(
139            s(|o| write_rect(o, Rect::new(1.0, 2.0, 10.0, 20.0))),
140            "1 2 9 18"
141        );
142    }
143
144    // The N-up golden (cpdf_npagetooneexporter_unittest.cpp:10-19) is as much
145    // a number test as a layout one: leading zeros go, tiny and huge values
146    // stay positional.
147    #[test]
148    fn extreme_magnitudes_stay_positional() {
149        assert_eq!(s(|o| write_float(o, 0.000_001)), ".000001");
150        assert_eq!(s(|o| write_float(o, 1_000_000_000_000.0)), "1000000000000");
151        assert_eq!(s(|o| write_float(o, 0.5)), ".5");
152    }
153}