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}