Skip to main content

oxidize_pdf/graphics/
clipping.rs

1//! Clipping path support according to ISO 32000-1 Section 8.5
2//!
3//! This module provides comprehensive support for PDF clipping paths
4//! as specified in ISO 32000-1:2008.
5
6use crate::error::Result;
7use crate::graphics::{PathCommand, WindingRule};
8use std::fmt::Write;
9
10/// Clipping path state
11#[derive(Debug, Clone)]
12pub struct ClippingPath {
13    /// Path commands that define the clipping region
14    commands: Vec<PathCommand>,
15    /// Winding rule for determining interior/exterior
16    winding_rule: WindingRule,
17    /// Whether this is a text clipping path
18    is_text_clip: bool,
19}
20
21impl ClippingPath {
22    /// Create a new empty clipping path
23    pub fn new() -> Self {
24        Self {
25            commands: Vec::new(),
26            winding_rule: WindingRule::NonZero,
27            is_text_clip: false,
28        }
29    }
30
31    /// Create a rectangular clipping path
32    pub fn rect(x: f64, y: f64, width: f64, height: f64) -> Self {
33        let mut path = Self::new();
34        path.add_rect(x, y, width, height);
35        path
36    }
37
38    /// Create a circular clipping path
39    pub fn circle(cx: f64, cy: f64, radius: f64) -> Self {
40        let mut path = Self::new();
41        path.add_circle(cx, cy, radius);
42        path
43    }
44
45    /// Create an elliptical clipping path
46    pub fn ellipse(cx: f64, cy: f64, rx: f64, ry: f64) -> Self {
47        let mut path = Self::new();
48        path.add_ellipse(cx, cy, rx, ry);
49        path
50    }
51
52    /// Set the winding rule
53    pub fn with_winding_rule(mut self, rule: WindingRule) -> Self {
54        self.winding_rule = rule;
55        self
56    }
57
58    /// Mark as text clipping path
59    pub fn as_text_clip(mut self) -> Self {
60        self.is_text_clip = true;
61        self
62    }
63
64    /// Add a move command
65    pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
66        self.commands.push(PathCommand::MoveTo { x, y });
67        self
68    }
69
70    /// Add a line command
71    pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
72        self.commands.push(PathCommand::LineTo { x, y });
73        self
74    }
75
76    /// Add a cubic Bézier curve
77    pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
78        self.commands.push(PathCommand::CurveTo {
79            x1,
80            y1,
81            x2,
82            y2,
83            x3,
84            y3,
85        });
86        self
87    }
88
89    /// Add a rectangle
90    pub fn add_rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
91        self.commands.push(PathCommand::Rectangle {
92            x,
93            y,
94            width,
95            height,
96        });
97        self
98    }
99
100    /// Add a circle using Bézier curves
101    pub fn add_circle(&mut self, cx: f64, cy: f64, radius: f64) -> &mut Self {
102        // Magic constant for approximating circle with cubic Bézier curves
103        const KAPPA: f64 = 0.5522847498307933;
104        let k = radius * KAPPA;
105
106        // Start at top of circle
107        self.move_to(cx, cy + radius);
108
109        // First quarter (top to right)
110        self.curve_to(cx + k, cy + radius, cx + radius, cy + k, cx + radius, cy);
111
112        // Second quarter (right to bottom)
113        self.curve_to(cx + radius, cy - k, cx + k, cy - radius, cx, cy - radius);
114
115        // Third quarter (bottom to left)
116        self.curve_to(cx - k, cy - radius, cx - radius, cy - k, cx - radius, cy);
117
118        // Fourth quarter (left to top)
119        self.curve_to(cx - radius, cy + k, cx - k, cy + radius, cx, cy + radius);
120
121        self.close_path();
122        self
123    }
124
125    /// Add an ellipse using Bézier curves
126    pub fn add_ellipse(&mut self, cx: f64, cy: f64, rx: f64, ry: f64) -> &mut Self {
127        const KAPPA: f64 = 0.5522847498307933;
128        let kx = rx * KAPPA;
129        let ky = ry * KAPPA;
130
131        // Start at top of ellipse
132        self.move_to(cx, cy + ry);
133
134        // First quarter
135        self.curve_to(cx + kx, cy + ry, cx + rx, cy + ky, cx + rx, cy);
136
137        // Second quarter
138        self.curve_to(cx + rx, cy - ky, cx + kx, cy - ry, cx, cy - ry);
139
140        // Third quarter
141        self.curve_to(cx - kx, cy - ry, cx - rx, cy - ky, cx - rx, cy);
142
143        // Fourth quarter
144        self.curve_to(cx - rx, cy + ky, cx - kx, cy + ry, cx, cy + ry);
145
146        self.close_path();
147        self
148    }
149
150    /// Add a rounded rectangle
151    pub fn add_rounded_rect(
152        &mut self,
153        x: f64,
154        y: f64,
155        width: f64,
156        height: f64,
157        radius: f64,
158    ) -> &mut Self {
159        let r = radius.min(width / 2.0).min(height / 2.0);
160        const KAPPA: f64 = 0.5522847498307933;
161        let k = r * KAPPA;
162
163        // Start at top-left corner (after radius)
164        self.move_to(x + r, y);
165
166        // Top edge
167        self.line_to(x + width - r, y);
168
169        // Top-right corner
170        self.curve_to(x + width - r + k, y, x + width, y + r - k, x + width, y + r);
171
172        // Right edge
173        self.line_to(x + width, y + height - r);
174
175        // Bottom-right corner
176        self.curve_to(
177            x + width,
178            y + height - r + k,
179            x + width - r + k,
180            y + height,
181            x + width - r,
182            y + height,
183        );
184
185        // Bottom edge
186        self.line_to(x + r, y + height);
187
188        // Bottom-left corner
189        self.curve_to(
190            x + r - k,
191            y + height,
192            x,
193            y + height - r + k,
194            x,
195            y + height - r,
196        );
197
198        // Left edge
199        self.line_to(x, y + r);
200
201        // Top-left corner
202        self.curve_to(x, y + r - k, x + r - k, y, x + r, y);
203
204        self.close_path();
205        self
206    }
207
208    /// Add a polygon
209    pub fn add_polygon(&mut self, points: &[(f64, f64)]) -> &mut Self {
210        if let Some((first, rest)) = points.split_first() {
211            self.move_to(first.0, first.1);
212            for point in rest {
213                self.line_to(point.0, point.1);
214            }
215            self.close_path();
216        }
217        self
218    }
219
220    /// Close the current subpath
221    pub fn close_path(&mut self) -> &mut Self {
222        self.commands.push(PathCommand::ClosePath);
223        self
224    }
225
226    /// Check if the path is empty
227    pub fn is_empty(&self) -> bool {
228        self.commands.is_empty()
229    }
230
231    /// Get the path commands
232    pub fn commands(&self) -> &[PathCommand] {
233        &self.commands
234    }
235
236    /// Generate PDF operations for this clipping path
237    pub fn to_pdf_operations(&self) -> Result<String> {
238        let mut ops = String::new();
239
240        // Generate path construction commands
241        for cmd in &self.commands {
242            match cmd {
243                PathCommand::MoveTo { x, y } => {
244                    writeln!(&mut ops, "{:.3} {:.3} m", x, y)
245                        .expect("Writing to string should never fail");
246                }
247                PathCommand::LineTo { x, y } => {
248                    writeln!(&mut ops, "{:.3} {:.3} l", x, y)
249                        .expect("Writing to string should never fail");
250                }
251                PathCommand::CurveTo {
252                    x1,
253                    y1,
254                    x2,
255                    y2,
256                    x3,
257                    y3,
258                } => {
259                    writeln!(
260                        &mut ops,
261                        "{:.3} {:.3} {:.3} {:.3} {:.3} {:.3} c",
262                        x1, y1, x2, y2, x3, y3
263                    )
264                    .expect("Writing to string should never fail");
265                }
266                PathCommand::Rectangle {
267                    x,
268                    y,
269                    width,
270                    height,
271                } => {
272                    writeln!(&mut ops, "{:.3} {:.3} {:.3} {:.3} re", x, y, width, height)
273                        .expect("Writing to string should never fail");
274                }
275                PathCommand::ClosePath => {
276                    writeln!(&mut ops, "h").expect("Writing to string should never fail");
277                }
278            }
279        }
280
281        // Apply clipping based on winding rule
282        match self.winding_rule {
283            WindingRule::NonZero => {
284                writeln!(&mut ops, "W").expect("Writing to string should never fail")
285            }
286            WindingRule::EvenOdd => {
287                writeln!(&mut ops, "W*").expect("Writing to string should never fail")
288            }
289        }
290
291        // End path without filling or stroking
292        writeln!(&mut ops, "n").expect("Writing to string should never fail");
293
294        Ok(ops)
295    }
296
297    /// Intersect with another clipping path
298    pub fn intersect(&mut self, other: &ClippingPath) -> &mut Self {
299        // In PDF, intersection is achieved by applying both clips sequentially
300        // Here we just append the commands
301        self.commands.extend_from_slice(&other.commands);
302        self
303    }
304
305    /// Clear all commands
306    pub fn clear(&mut self) {
307        self.commands.clear();
308    }
309}
310
311impl Default for ClippingPath {
312    fn default() -> Self {
313        Self::new()
314    }
315}
316
317/// Clipping region manager for handling multiple clipping paths
318#[derive(Debug, Clone)]
319pub struct ClippingRegion {
320    /// Stack of clipping paths (for save/restore operations)
321    stack: Vec<ClippingPath>,
322    /// Current active clipping path
323    current: Option<ClippingPath>,
324}
325
326impl ClippingRegion {
327    /// Create a new clipping region manager
328    pub fn new() -> Self {
329        Self {
330            stack: Vec::new(),
331            current: None,
332        }
333    }
334
335    /// Set the current clipping path
336    pub fn set_clip(&mut self, path: ClippingPath) {
337        self.current = Some(path);
338    }
339
340    /// Clear the current clipping path
341    pub fn clear_clip(&mut self) {
342        self.current = None;
343    }
344
345    /// Save the current clipping state
346    pub fn save(&mut self) {
347        if let Some(ref current) = self.current {
348            self.stack.push(current.clone());
349        }
350    }
351
352    /// Restore the previous clipping state
353    pub fn restore(&mut self) {
354        if let Some(saved) = self.stack.pop() {
355            self.current = Some(saved);
356        }
357    }
358
359    /// Get the current clipping path
360    pub fn current(&self) -> Option<&ClippingPath> {
361        self.current.as_ref()
362    }
363
364    /// Check if there's an active clipping path
365    pub fn has_clip(&self) -> bool {
366        self.current.is_some()
367    }
368
369    /// Generate PDF operations for the current clipping path
370    pub fn to_pdf_operations(&self) -> Result<Option<String>> {
371        if let Some(ref clip) = self.current {
372            Ok(Some(clip.to_pdf_operations()?))
373        } else {
374            Ok(None)
375        }
376    }
377}
378
379impl Default for ClippingRegion {
380    fn default() -> Self {
381        Self::new()
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    #[test]
390    fn test_clipping_path_creation() {
391        let path = ClippingPath::new();
392        assert!(path.is_empty());
393        assert!(!path.is_text_clip);
394        assert_eq!(path.winding_rule, WindingRule::NonZero);
395    }
396
397    #[test]
398    fn test_rect_clipping_path() {
399        let path = ClippingPath::rect(10.0, 20.0, 100.0, 50.0);
400        assert!(!path.is_empty());
401        assert_eq!(path.commands.len(), 1);
402    }
403
404    #[test]
405    fn test_circle_clipping_path() {
406        let path = ClippingPath::circle(50.0, 50.0, 25.0);
407        assert!(!path.is_empty());
408        // Circle is approximated with 4 cubic Bézier curves + move + close
409        assert!(path.commands.len() >= 6);
410    }
411
412    #[test]
413    fn test_ellipse_clipping_path() {
414        let path = ClippingPath::ellipse(50.0, 50.0, 30.0, 20.0);
415        assert!(!path.is_empty());
416        assert!(path.commands.len() >= 6);
417    }
418
419    #[test]
420    fn test_winding_rule() {
421        let path = ClippingPath::new().with_winding_rule(WindingRule::EvenOdd);
422        assert_eq!(path.winding_rule, WindingRule::EvenOdd);
423    }
424
425    #[test]
426    fn test_text_clip() {
427        let path = ClippingPath::new().as_text_clip();
428        assert!(path.is_text_clip);
429    }
430
431    #[test]
432    fn test_path_construction() {
433        let mut path = ClippingPath::new();
434        path.move_to(0.0, 0.0)
435            .line_to(100.0, 0.0)
436            .line_to(100.0, 100.0)
437            .line_to(0.0, 100.0)
438            .close_path();
439
440        assert_eq!(path.commands.len(), 5);
441    }
442
443    #[test]
444    fn test_curve_to() {
445        let mut path = ClippingPath::new();
446        path.move_to(0.0, 0.0)
447            .curve_to(10.0, 10.0, 20.0, 20.0, 30.0, 30.0);
448
449        assert_eq!(path.commands.len(), 2);
450    }
451
452    #[test]
453    fn test_polygon() {
454        let mut path = ClippingPath::new();
455        let points = vec![(0.0, 0.0), (50.0, 0.0), (25.0, 50.0)];
456        path.add_polygon(&points);
457
458        // move + 2 lines + close
459        assert_eq!(path.commands.len(), 4);
460    }
461
462    #[test]
463    fn test_rounded_rect() {
464        let mut path = ClippingPath::new();
465        path.add_rounded_rect(10.0, 10.0, 100.0, 50.0, 5.0);
466
467        // Rounded rect has 4 corners (curves) + 4 edges (lines) + move + close
468        assert!(path.commands.len() >= 10);
469    }
470
471    #[test]
472    fn test_pdf_operations_nonzero() {
473        let path = ClippingPath::rect(0.0, 0.0, 100.0, 100.0);
474        let ops = path
475            .to_pdf_operations()
476            .expect("Writing to string should never fail");
477
478        assert!(ops.contains("0.000 0.000 100.000 100.000 re"));
479        assert!(ops.contains("W")); // Non-zero winding
480        assert!(ops.contains("n")); // End path
481    }
482
483    #[test]
484    fn test_pdf_operations_evenodd() {
485        let path =
486            ClippingPath::rect(0.0, 0.0, 100.0, 100.0).with_winding_rule(WindingRule::EvenOdd);
487        let ops = path
488            .to_pdf_operations()
489            .expect("Writing to string should never fail");
490
491        assert!(ops.contains("W*")); // Even-odd winding
492    }
493
494    #[test]
495    fn test_intersect_paths() {
496        let mut path1 = ClippingPath::rect(0.0, 0.0, 100.0, 100.0);
497        let path2 = ClippingPath::rect(50.0, 50.0, 100.0, 100.0);
498
499        path1.intersect(&path2);
500        assert_eq!(path1.commands.len(), 2);
501    }
502
503    #[test]
504    fn test_clear_path() {
505        let mut path = ClippingPath::rect(0.0, 0.0, 100.0, 100.0);
506        assert!(!path.is_empty());
507
508        path.clear();
509        assert!(path.is_empty());
510    }
511
512    #[test]
513    fn test_clipping_region_creation() {
514        let region = ClippingRegion::new();
515        assert!(!region.has_clip());
516        assert!(region.current().is_none());
517    }
518
519    #[test]
520    fn test_clipping_region_set_clip() {
521        let mut region = ClippingRegion::new();
522        let path = ClippingPath::rect(0.0, 0.0, 100.0, 100.0);
523
524        region.set_clip(path);
525        assert!(region.has_clip());
526        assert!(region.current().is_some());
527    }
528
529    #[test]
530    fn test_clipping_region_clear() {
531        let mut region = ClippingRegion::new();
532        region.set_clip(ClippingPath::rect(0.0, 0.0, 100.0, 100.0));
533        assert!(region.has_clip());
534
535        region.clear_clip();
536        assert!(!region.has_clip());
537    }
538
539    #[test]
540    fn test_clipping_region_save_restore() {
541        let mut region = ClippingRegion::new();
542        let path1 = ClippingPath::rect(0.0, 0.0, 100.0, 100.0);
543        let path2 = ClippingPath::rect(50.0, 50.0, 50.0, 50.0);
544
545        region.set_clip(path1);
546        region.save();
547        region.set_clip(path2);
548
549        // Current should be path2
550        assert!(region.has_clip());
551
552        region.restore();
553        // Should be back to path1
554        assert!(region.has_clip());
555    }
556
557    #[test]
558    fn test_clipping_region_pdf_operations() {
559        let mut region = ClippingRegion::new();
560
561        // No clip set
562        let ops = region
563            .to_pdf_operations()
564            .expect("Writing to string should never fail");
565        assert!(ops.is_none());
566
567        // With clip set
568        region.set_clip(ClippingPath::rect(0.0, 0.0, 100.0, 100.0));
569        let ops = region
570            .to_pdf_operations()
571            .expect("Writing to string should never fail");
572        assert!(ops.is_some());
573        assert!(ops.unwrap().contains("re"));
574    }
575
576    #[test]
577    fn test_complex_clipping_path() {
578        let mut path = ClippingPath::new();
579        path.move_to(10.0, 10.0)
580            .line_to(50.0, 10.0)
581            .curve_to(60.0, 10.0, 70.0, 20.0, 70.0, 30.0)
582            .line_to(70.0, 50.0)
583            .close_path();
584
585        let ops = path
586            .to_pdf_operations()
587            .expect("Writing to string should never fail");
588        assert!(ops.contains("10.000 10.000 m"));
589        assert!(ops.contains("50.000 10.000 l"));
590        assert!(ops.contains("c")); // Curve
591        assert!(ops.contains("h")); // Close path
592        assert!(ops.contains("W")); // Clip
593        assert!(ops.contains("n")); // End path
594    }
595}