1use zpdf_core::Rect;
9
10#[derive(Debug, Clone)]
14pub struct InkAnnotationBuilder {
15 ink_list: Vec<Vec<(f64, f64)>>,
18 color: (f64, f64, f64),
20 width: f64,
22}
23
24impl InkAnnotationBuilder {
25 pub fn new() -> Self {
27 Self {
28 ink_list: Vec::new(),
29 color: (0.0, 0.0, 0.0), width: 1.0,
31 }
32 }
33
34 pub fn add_stroke(&mut self, points: Vec<(f64, f64)>) {
39 if points.len() >= 2 {
40 self.ink_list.push(points);
41 }
42 }
43
44 pub fn set_color(&mut self, r: f64, g: f64, b: f64) {
46 self.color = (r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0));
47 }
48
49 pub fn set_width(&mut self, w: f64) {
51 self.width = w.max(0.1);
52 }
53
54 pub fn compute_rect(&self) -> Option<Rect> {
57 if self.ink_list.is_empty() {
58 return None;
59 }
60 let mut min_x = f64::INFINITY;
61 let mut max_x = f64::NEG_INFINITY;
62 let mut min_y = f64::INFINITY;
63 let mut max_y = f64::NEG_INFINITY;
64
65 for stroke in &self.ink_list {
66 for &(x, y) in stroke {
67 min_x = min_x.min(x);
68 max_x = max_x.max(x);
69 min_y = min_y.min(y);
70 max_y = max_y.max(y);
71 }
72 }
73
74 let margin = self.width / 2.0 + 1.0;
76 Some(Rect {
77 x0: min_x - margin,
78 y0: min_y - margin,
79 x1: max_x + margin,
80 y1: max_y + margin,
81 })
82 }
83
84 pub fn build(&self) -> Option<(InkAnnotDict, Vec<u8>)> {
95 let rect = self.compute_rect()?;
96
97 let dict = InkAnnotDict {
99 rect,
100 ink_list: self.ink_list.clone(),
101 color: self.color,
102 width: self.width,
103 };
104
105 let appearance = self.build_appearance_stream(&rect);
107
108 Some((dict, appearance))
109 }
110
111 fn build_appearance_stream(&self, _rect: &Rect) -> Vec<u8> {
114 let mut stream = Vec::new();
115 let (r, g, b) = self.color;
116
117 stream.extend_from_slice(b"q\n");
123 stream.extend_from_slice(format!("{:.3} w\n", self.width).as_bytes());
124 stream.extend_from_slice(format!("{:.3} {:.3} {:.3} RG\n", r, g, b).as_bytes());
125
126 for stroke in &self.ink_list {
127 if let Some(&(x0, y0)) = stroke.first() {
128 stream.extend_from_slice(format!("{:.2} {:.2} m\n", x0, y0).as_bytes());
129 for &(x, y) in &stroke[1..] {
130 stream.extend_from_slice(format!("{:.2} {:.2} l\n", x, y).as_bytes());
131 }
132 stream.extend_from_slice(b"S\n");
133 }
134 }
135
136 stream.extend_from_slice(b"Q\n");
137 stream
138 }
139}
140
141impl Default for InkAnnotationBuilder {
142 fn default() -> Self {
143 Self::new()
144 }
145}
146
147#[derive(Debug, Clone)]
149pub struct InkAnnotDict {
150 pub rect: Rect,
152 pub ink_list: Vec<Vec<(f64, f64)>>,
154 pub color: (f64, f64, f64),
156 pub width: f64,
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn compute_rect_includes_all_points_with_margin() {
166 let mut builder = InkAnnotationBuilder::new();
167 builder.set_width(2.0);
168 builder.add_stroke(vec![(10.0, 20.0), (30.0, 40.0)]);
169 builder.add_stroke(vec![(5.0, 15.0), (35.0, 45.0)]);
170
171 let rect = builder.compute_rect().expect("rect");
172 assert_eq!(rect.x0, 3.0);
174 assert_eq!(rect.y0, 13.0);
175 assert_eq!(rect.x1, 37.0);
176 assert_eq!(rect.y1, 47.0);
177 }
178
179 #[test]
180 fn single_point_strokes_are_dropped() {
181 let mut builder = InkAnnotationBuilder::new();
182 builder.add_stroke(vec![(10.0, 20.0)]); builder.add_stroke(vec![]); assert!(builder.compute_rect().is_none());
185 }
186
187 #[test]
188 fn build_produces_dict_and_appearance() {
189 let mut builder = InkAnnotationBuilder::new();
190 builder.set_color(1.0, 0.0, 0.0); builder.set_width(3.0);
192 builder.add_stroke(vec![(100.0, 200.0), (150.0, 250.0)]);
193
194 let (dict, appearance) = builder.build().expect("build");
195 assert_eq!(dict.color, (1.0, 0.0, 0.0));
196 assert_eq!(dict.width, 3.0);
197 assert_eq!(dict.ink_list.len(), 1);
198
199 let s = String::from_utf8_lossy(&appearance);
201 assert!(s.contains("1.000 0.000 0.000 RG")); assert!(s.contains("3.000 w")); assert!(s.contains("100.00 200.00 m")); assert!(s.contains("150.00 250.00 l")); assert!(s.contains("S")); }
207
208 #[test]
209 fn color_clamped_to_valid_range() {
210 let mut builder = InkAnnotationBuilder::new();
211 builder.set_color(-0.5, 1.5, 0.5);
212 assert_eq!(builder.color, (0.0, 1.0, 0.5));
213 }
214
215 #[test]
216 fn width_has_minimum() {
217 let mut builder = InkAnnotationBuilder::new();
218 builder.set_width(0.0);
219 assert_eq!(builder.width, 0.1);
220 builder.set_width(-5.0);
221 assert_eq!(builder.width, 0.1);
222 }
223}