Skip to main content

pdf_annot/
appearance_writer.rs

1//! Appearance stream generation for PDF annotations.
2//!
3//! Generates Form XObject streams containing the visual representation
4//! of annotations using PDF content stream operators.
5
6#[cfg(feature = "write")]
7use lopdf::content::Operation;
8#[cfg(feature = "write")]
9use lopdf::Object;
10
11/// RGB color for annotation appearance.
12#[cfg(feature = "write")]
13#[derive(Debug, Clone, Copy)]
14pub struct AppearanceColor {
15    pub r: f64,
16    pub g: f64,
17    pub b: f64,
18}
19
20#[cfg(feature = "write")]
21impl AppearanceColor {
22    pub fn new(r: f64, g: f64, b: f64) -> Self {
23        Self { r, g, b }
24    }
25
26    /// Push fill color operators (rg).
27    pub fn fill_ops(&self) -> Operation {
28        Operation::new(
29            "rg",
30            vec![
31                Object::Real(self.r as f32),
32                Object::Real(self.g as f32),
33                Object::Real(self.b as f32),
34            ],
35        )
36    }
37
38    /// Push stroke color operators (RG).
39    pub fn stroke_ops(&self) -> Operation {
40        Operation::new(
41            "RG",
42            vec![
43                Object::Real(self.r as f32),
44                Object::Real(self.g as f32),
45                Object::Real(self.b as f32),
46            ],
47        )
48    }
49}
50
51/// Builds content stream operations for annotation appearance streams.
52///
53/// All coordinates are in the Form XObject's local coordinate system,
54/// where (0,0) is the bottom-left of the annotation rectangle.
55#[cfg(feature = "write")]
56pub struct AppearanceStreamBuilder {
57    ops: Vec<Operation>,
58    width: f64,
59    height: f64,
60}
61
62#[cfg(feature = "write")]
63impl AppearanceStreamBuilder {
64    /// Create a new builder for an appearance stream with the given dimensions.
65    pub fn new(width: f64, height: f64) -> Self {
66        Self {
67            ops: Vec::new(),
68            width,
69            height,
70        }
71    }
72
73    /// Save the current graphics state.
74    pub fn save_state(&mut self) -> &mut Self {
75        self.ops.push(Operation::new("q", vec![]));
76        self
77    }
78
79    /// Restore the graphics state.
80    pub fn restore_state(&mut self) -> &mut Self {
81        self.ops.push(Operation::new("Q", vec![]));
82        self
83    }
84
85    /// Set the fill color (RGB).
86    pub fn set_fill_color(&mut self, color: &AppearanceColor) -> &mut Self {
87        self.ops.push(color.fill_ops());
88        self
89    }
90
91    /// Set the stroke color (RGB).
92    pub fn set_stroke_color(&mut self, color: &AppearanceColor) -> &mut Self {
93        self.ops.push(color.stroke_ops());
94        self
95    }
96
97    /// Set the line width for stroked paths.
98    pub fn set_line_width(&mut self, width: f64) -> &mut Self {
99        self.ops
100            .push(Operation::new("w", vec![Object::Real(width as f32)]));
101        self
102    }
103
104    /// Set the dash pattern for stroked paths.
105    pub fn set_dash_pattern(&mut self, dash: &[f64], phase: f64) -> &mut Self {
106        let arr: Vec<Object> = dash.iter().map(|&d| Object::Real(d as f32)).collect();
107        self.ops.push(Operation::new(
108            "d",
109            vec![Object::Array(arr), Object::Real(phase as f32)],
110        ));
111        self
112    }
113
114    /// Draw a rectangle path.
115    pub fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) -> &mut Self {
116        self.ops.push(Operation::new(
117            "re",
118            vec![
119                Object::Real(x as f32),
120                Object::Real(y as f32),
121                Object::Real(w as f32),
122                Object::Real(h as f32),
123            ],
124        ));
125        self
126    }
127
128    /// Move to a point (start a new subpath).
129    pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
130        self.ops.push(Operation::new(
131            "m",
132            vec![Object::Real(x as f32), Object::Real(y as f32)],
133        ));
134        self
135    }
136
137    /// Line to a point.
138    pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
139        self.ops.push(Operation::new(
140            "l",
141            vec![Object::Real(x as f32), Object::Real(y as f32)],
142        ));
143        self
144    }
145
146    /// Cubic bezier curve.
147    pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
148        self.ops.push(Operation::new(
149            "c",
150            vec![
151                Object::Real(x1 as f32),
152                Object::Real(y1 as f32),
153                Object::Real(x2 as f32),
154                Object::Real(y2 as f32),
155                Object::Real(x3 as f32),
156                Object::Real(y3 as f32),
157            ],
158        ));
159        self
160    }
161
162    /// Close the current subpath.
163    pub fn close_path(&mut self) -> &mut Self {
164        self.ops.push(Operation::new("h", vec![]));
165        self
166    }
167
168    /// Stroke the current path.
169    pub fn stroke(&mut self) -> &mut Self {
170        self.ops.push(Operation::new("S", vec![]));
171        self
172    }
173
174    /// Fill the current path (non-zero winding rule).
175    pub fn fill(&mut self) -> &mut Self {
176        self.ops.push(Operation::new("f", vec![]));
177        self
178    }
179
180    /// Fill then stroke the current path.
181    pub fn fill_and_stroke(&mut self) -> &mut Self {
182        self.ops.push(Operation::new("B", vec![]));
183        self
184    }
185
186    /// Close, fill and stroke.
187    pub fn close_fill_and_stroke(&mut self) -> &mut Self {
188        self.ops.push(Operation::new("b", vec![]));
189        self
190    }
191
192    /// Add a filled rectangle covering the full annotation area.
193    pub fn filled_rect(&mut self, color: &AppearanceColor) -> &mut Self {
194        self.save_state();
195        self.set_fill_color(color);
196        self.rect(0.0, 0.0, self.width, self.height);
197        self.fill();
198        self.restore_state();
199        self
200    }
201
202    /// Add a stroked rectangle (border) inside the annotation area.
203    pub fn stroked_rect(&mut self, color: &AppearanceColor, line_width: f64) -> &mut Self {
204        let half = line_width / 2.0;
205        self.save_state();
206        self.set_stroke_color(color);
207        self.set_line_width(line_width);
208        self.rect(
209            half,
210            half,
211            self.width - line_width,
212            self.height - line_width,
213        );
214        self.stroke();
215        self.restore_state();
216        self
217    }
218
219    /// Draw a filled and stroked rectangle.
220    pub fn filled_stroked_rect(
221        &mut self,
222        fill: &AppearanceColor,
223        stroke: &AppearanceColor,
224        line_width: f64,
225    ) -> &mut Self {
226        let half = line_width / 2.0;
227        self.save_state();
228        self.set_fill_color(fill);
229        self.set_stroke_color(stroke);
230        self.set_line_width(line_width);
231        self.rect(
232            half,
233            half,
234            self.width - line_width,
235            self.height - line_width,
236        );
237        self.fill_and_stroke();
238        self.restore_state();
239        self
240    }
241
242    /// Draw an ellipse (circle if width == height) using cubic bezier approximation.
243    pub fn ellipse(&mut self) -> &mut Self {
244        // Approximate ellipse with 4 cubic bezier curves.
245        // Control point factor: 4*(sqrt(2)-1)/3 ≈ 0.5523
246        let k = 0.5523;
247        let cx = self.width / 2.0;
248        let cy = self.height / 2.0;
249        let rx = cx;
250        let ry = cy;
251
252        self.move_to(cx + rx, cy);
253        self.curve_to(cx + rx, cy + ry * k, cx + rx * k, cy + ry, cx, cy + ry);
254        self.curve_to(cx - rx * k, cy + ry, cx - rx, cy + ry * k, cx - rx, cy);
255        self.curve_to(cx - rx, cy - ry * k, cx - rx * k, cy - ry, cx, cy - ry);
256        self.curve_to(cx + rx * k, cy - ry, cx + rx, cy - ry * k, cx + rx, cy);
257        self.close_path();
258        self
259    }
260
261    /// Draw a line between two points in local coordinates.
262    pub fn line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64) -> &mut Self {
263        self.move_to(x1, y1);
264        self.line_to(x2, y2);
265        self
266    }
267
268    /// Add text to the appearance stream.
269    pub fn text(
270        &mut self,
271        text: &str,
272        font_name: &str,
273        font_size: f64,
274        x: f64,
275        y: f64,
276        color: &AppearanceColor,
277    ) -> &mut Self {
278        self.save_state();
279        self.set_fill_color(color);
280        self.ops.push(Operation::new("BT", vec![]));
281        self.ops.push(Operation::new(
282            "Tf",
283            vec![
284                Object::Name(font_name.as_bytes().to_vec()),
285                Object::Real(font_size as f32),
286            ],
287        ));
288        self.ops.push(Operation::new(
289            "Td",
290            vec![Object::Real(x as f32), Object::Real(y as f32)],
291        ));
292        self.ops.push(Operation::new(
293            "Tj",
294            vec![Object::String(
295                text.as_bytes().to_vec(),
296                lopdf::StringFormat::Literal,
297            )],
298        ));
299        self.ops.push(Operation::new("ET", vec![]));
300        self.restore_state();
301        self
302    }
303
304    /// Push a raw operation (for advanced use cases like ExtGState references).
305    pub fn ops_push_raw(&mut self, op: Operation) -> &mut Self {
306        self.ops.push(op);
307        self
308    }
309
310    /// Encode the operations into content stream bytes.
311    pub fn encode(self) -> Result<Vec<u8>, String> {
312        lopdf::content::Content {
313            operations: self.ops,
314        }
315        .encode()
316        .map_err(|e| format!("{e}"))
317    }
318
319    /// Return the width of the appearance.
320    pub fn width(&self) -> f64 {
321        self.width
322    }
323
324    /// Return the height of the appearance.
325    pub fn height(&self) -> f64 {
326        self.height
327    }
328}
329
330#[cfg(all(test, feature = "write"))]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn encode_simple_rect() {
336        let mut builder = AppearanceStreamBuilder::new(100.0, 50.0);
337        let red = AppearanceColor::new(1.0, 0.0, 0.0);
338        builder.stroked_rect(&red, 1.0);
339        let bytes = builder.encode().unwrap();
340        let s = String::from_utf8_lossy(&bytes);
341        assert!(s.contains("RG"), "should contain stroke color");
342        assert!(s.contains("re"), "should contain rectangle");
343        assert!(s.contains("S"), "should contain stroke");
344    }
345
346    #[test]
347    fn encode_filled_rect() {
348        let mut builder = AppearanceStreamBuilder::new(80.0, 40.0);
349        let yellow = AppearanceColor::new(1.0, 1.0, 0.0);
350        builder.filled_rect(&yellow);
351        let bytes = builder.encode().unwrap();
352        let s = String::from_utf8_lossy(&bytes);
353        assert!(s.contains("rg"), "should contain fill color");
354        assert!(s.contains("f"), "should contain fill");
355    }
356
357    #[test]
358    fn encode_ellipse() {
359        let mut builder = AppearanceStreamBuilder::new(60.0, 60.0);
360        let blue = AppearanceColor::new(0.0, 0.0, 1.0);
361        builder.save_state();
362        builder.set_stroke_color(&blue);
363        builder.set_line_width(1.0);
364        builder.ellipse();
365        builder.stroke();
366        builder.restore_state();
367        let bytes = builder.encode().unwrap();
368        let s = String::from_utf8_lossy(&bytes);
369        assert!(s.contains("c"), "should contain bezier curves");
370    }
371
372    #[test]
373    fn encode_text() {
374        let mut builder = AppearanceStreamBuilder::new(200.0, 20.0);
375        let black = AppearanceColor::new(0.0, 0.0, 0.0);
376        builder.text("Hello World", "F1", 12.0, 2.0, 4.0, &black);
377        let bytes = builder.encode().unwrap();
378        let s = String::from_utf8_lossy(&bytes);
379        assert!(s.contains("BT"), "should contain begin text");
380        assert!(s.contains("Tj"), "should contain show text");
381        assert!(s.contains("ET"), "should contain end text");
382    }
383}