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