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