oxidize_pdf/graphics/
mod.rs

1mod color;
2mod image;
3mod path;
4
5pub use color::Color;
6pub use image::{ColorSpace as ImageColorSpace, Image, ImageFormat};
7pub use path::{LineCap, LineJoin, PathBuilder};
8
9use crate::error::Result;
10use std::fmt::Write;
11
12#[derive(Clone)]
13pub struct GraphicsContext {
14    operations: String,
15    current_color: Color,
16    stroke_color: Color,
17    line_width: f64,
18}
19
20impl Default for GraphicsContext {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl GraphicsContext {
27    pub fn new() -> Self {
28        Self {
29            operations: String::new(),
30            current_color: Color::black(),
31            stroke_color: Color::black(),
32            line_width: 1.0,
33        }
34    }
35
36    pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
37        writeln!(&mut self.operations, "{x:.2} {y:.2} m").unwrap();
38        self
39    }
40
41    pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
42        writeln!(&mut self.operations, "{x:.2} {y:.2} l").unwrap();
43        self
44    }
45
46    pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
47        writeln!(
48            &mut self.operations,
49            "{x1:.2} {y1:.2} {x2:.2} {y2:.2} {x3:.2} {y3:.2} c"
50        )
51        .unwrap();
52        self
53    }
54
55    pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
56        writeln!(
57            &mut self.operations,
58            "{x:.2} {y:.2} {width:.2} {height:.2} re"
59        )
60        .unwrap();
61        self
62    }
63
64    pub fn circle(&mut self, cx: f64, cy: f64, radius: f64) -> &mut Self {
65        let k = 0.552284749831;
66        let r = radius;
67
68        self.move_to(cx + r, cy);
69        self.curve_to(cx + r, cy + k * r, cx + k * r, cy + r, cx, cy + r);
70        self.curve_to(cx - k * r, cy + r, cx - r, cy + k * r, cx - r, cy);
71        self.curve_to(cx - r, cy - k * r, cx - k * r, cy - r, cx, cy - r);
72        self.curve_to(cx + k * r, cy - r, cx + r, cy - k * r, cx + r, cy);
73        self.close_path()
74    }
75
76    pub fn close_path(&mut self) -> &mut Self {
77        self.operations.push_str("h\n");
78        self
79    }
80
81    pub fn stroke(&mut self) -> &mut Self {
82        self.apply_stroke_color();
83        self.operations.push_str("S\n");
84        self
85    }
86
87    pub fn fill(&mut self) -> &mut Self {
88        self.apply_fill_color();
89        self.operations.push_str("f\n");
90        self
91    }
92
93    pub fn fill_stroke(&mut self) -> &mut Self {
94        self.apply_fill_color();
95        self.apply_stroke_color();
96        self.operations.push_str("B\n");
97        self
98    }
99
100    pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
101        self.stroke_color = color;
102        self
103    }
104
105    pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
106        self.current_color = color;
107        self
108    }
109
110    pub fn set_line_width(&mut self, width: f64) -> &mut Self {
111        self.line_width = width;
112        writeln!(&mut self.operations, "{width:.2} w").unwrap();
113        self
114    }
115
116    pub fn set_line_cap(&mut self, cap: LineCap) -> &mut Self {
117        writeln!(&mut self.operations, "{} J", cap as u8).unwrap();
118        self
119    }
120
121    pub fn set_line_join(&mut self, join: LineJoin) -> &mut Self {
122        writeln!(&mut self.operations, "{} j", join as u8).unwrap();
123        self
124    }
125
126    pub fn save_state(&mut self) -> &mut Self {
127        self.operations.push_str("q\n");
128        self
129    }
130
131    pub fn restore_state(&mut self) -> &mut Self {
132        self.operations.push_str("Q\n");
133        self
134    }
135
136    pub fn translate(&mut self, tx: f64, ty: f64) -> &mut Self {
137        writeln!(&mut self.operations, "1 0 0 1 {tx:.2} {ty:.2} cm").unwrap();
138        self
139    }
140
141    pub fn scale(&mut self, sx: f64, sy: f64) -> &mut Self {
142        writeln!(&mut self.operations, "{sx:.2} 0 0 {sy:.2} 0 0 cm").unwrap();
143        self
144    }
145
146    pub fn rotate(&mut self, angle: f64) -> &mut Self {
147        let cos = angle.cos();
148        let sin = angle.sin();
149        writeln!(
150            &mut self.operations,
151            "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
152            cos, sin, -sin, cos
153        )
154        .unwrap();
155        self
156    }
157
158    pub fn transform(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> &mut Self {
159        writeln!(
160            &mut self.operations,
161            "{a:.2} {b:.2} {c:.2} {d:.2} {e:.2} {f:.2} cm"
162        )
163        .unwrap();
164        self
165    }
166
167    pub fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
168        self.rect(x, y, width, height)
169    }
170
171    pub fn draw_image(
172        &mut self,
173        image_name: &str,
174        x: f64,
175        y: f64,
176        width: f64,
177        height: f64,
178    ) -> &mut Self {
179        // Save graphics state
180        self.save_state();
181
182        // Set up transformation matrix for image placement
183        // PDF coordinate system has origin at bottom-left, so we need to translate and scale
184        writeln!(
185            &mut self.operations,
186            "{width:.2} 0 0 {height:.2} {x:.2} {y:.2} cm"
187        )
188        .unwrap();
189
190        // Draw the image XObject
191        writeln!(&mut self.operations, "/{image_name} Do").unwrap();
192
193        // Restore graphics state
194        self.restore_state();
195
196        self
197    }
198
199    fn apply_stroke_color(&mut self) {
200        match self.stroke_color {
201            Color::Rgb(r, g, b) => {
202                writeln!(&mut self.operations, "{r:.3} {g:.3} {b:.3} RG").unwrap();
203            }
204            Color::Gray(g) => {
205                writeln!(&mut self.operations, "{g:.3} G").unwrap();
206            }
207            Color::Cmyk(c, m, y, k) => {
208                writeln!(&mut self.operations, "{c:.3} {m:.3} {y:.3} {k:.3} K").unwrap();
209            }
210        }
211    }
212
213    fn apply_fill_color(&mut self) {
214        match self.current_color {
215            Color::Rgb(r, g, b) => {
216                writeln!(&mut self.operations, "{r:.3} {g:.3} {b:.3} rg").unwrap();
217            }
218            Color::Gray(g) => {
219                writeln!(&mut self.operations, "{g:.3} g").unwrap();
220            }
221            Color::Cmyk(c, m, y, k) => {
222                writeln!(&mut self.operations, "{c:.3} {m:.3} {y:.3} {k:.3} k").unwrap();
223            }
224        }
225    }
226
227    pub(crate) fn generate_operations(&self) -> Result<Vec<u8>> {
228        Ok(self.operations.as_bytes().to_vec())
229    }
230    
231    /// Get the current fill color
232    pub fn fill_color(&self) -> Color {
233        self.current_color
234    }
235    
236    /// Get the current stroke color
237    pub fn stroke_color(&self) -> Color {
238        self.stroke_color
239    }
240    
241    /// Get the current line width
242    pub fn line_width(&self) -> f64 {
243        self.line_width
244    }
245    
246    /// Get the operations string
247    pub fn operations(&self) -> &str {
248        &self.operations
249    }
250    
251    /// Clear all operations
252    pub fn clear(&mut self) {
253        self.operations.clear();
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    
261    #[test]
262    fn test_graphics_context_new() {
263        let ctx = GraphicsContext::new();
264        assert_eq!(ctx.fill_color(), Color::black());
265        assert_eq!(ctx.stroke_color(), Color::black());
266        assert_eq!(ctx.line_width(), 1.0);
267        assert!(ctx.operations().is_empty());
268    }
269    
270    #[test]
271    fn test_graphics_context_default() {
272        let ctx = GraphicsContext::default();
273        assert_eq!(ctx.fill_color(), Color::black());
274        assert_eq!(ctx.stroke_color(), Color::black());
275        assert_eq!(ctx.line_width(), 1.0);
276    }
277    
278    #[test]
279    fn test_move_to() {
280        let mut ctx = GraphicsContext::new();
281        ctx.move_to(10.0, 20.0);
282        assert!(ctx.operations().contains("10.00 20.00 m\n"));
283    }
284    
285    #[test]
286    fn test_line_to() {
287        let mut ctx = GraphicsContext::new();
288        ctx.line_to(30.0, 40.0);
289        assert!(ctx.operations().contains("30.00 40.00 l\n"));
290    }
291    
292    #[test]
293    fn test_curve_to() {
294        let mut ctx = GraphicsContext::new();
295        ctx.curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0);
296        assert!(ctx.operations().contains("10.00 20.00 30.00 40.00 50.00 60.00 c\n"));
297    }
298    
299    #[test]
300    fn test_rect() {
301        let mut ctx = GraphicsContext::new();
302        ctx.rect(10.0, 20.0, 100.0, 50.0);
303        assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
304    }
305    
306    #[test]
307    fn test_rectangle_alias() {
308        let mut ctx = GraphicsContext::new();
309        ctx.rectangle(10.0, 20.0, 100.0, 50.0);
310        assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
311    }
312    
313    #[test]
314    fn test_circle() {
315        let mut ctx = GraphicsContext::new();
316        ctx.circle(50.0, 50.0, 25.0);
317        
318        let ops = ctx.operations();
319        // Check that it starts with move to radius point
320        assert!(ops.contains("75.00 50.00 m\n"));
321        // Check that it contains curve operations
322        assert!(ops.contains(" c\n"));
323        // Check that it closes the path
324        assert!(ops.contains("h\n"));
325    }
326    
327    #[test]
328    fn test_close_path() {
329        let mut ctx = GraphicsContext::new();
330        ctx.close_path();
331        assert!(ctx.operations().contains("h\n"));
332    }
333    
334    #[test]
335    fn test_stroke() {
336        let mut ctx = GraphicsContext::new();
337        ctx.set_stroke_color(Color::red());
338        ctx.rect(0.0, 0.0, 10.0, 10.0);
339        ctx.stroke();
340        
341        let ops = ctx.operations();
342        assert!(ops.contains("1.000 0.000 0.000 RG\n"));
343        assert!(ops.contains("S\n"));
344    }
345    
346    #[test]
347    fn test_fill() {
348        let mut ctx = GraphicsContext::new();
349        ctx.set_fill_color(Color::blue());
350        ctx.rect(0.0, 0.0, 10.0, 10.0);
351        ctx.fill();
352        
353        let ops = ctx.operations();
354        assert!(ops.contains("0.000 0.000 1.000 rg\n"));
355        assert!(ops.contains("f\n"));
356    }
357    
358    #[test]
359    fn test_fill_stroke() {
360        let mut ctx = GraphicsContext::new();
361        ctx.set_fill_color(Color::green());
362        ctx.set_stroke_color(Color::red());
363        ctx.rect(0.0, 0.0, 10.0, 10.0);
364        ctx.fill_stroke();
365        
366        let ops = ctx.operations();
367        assert!(ops.contains("0.000 1.000 0.000 rg\n"));
368        assert!(ops.contains("1.000 0.000 0.000 RG\n"));
369        assert!(ops.contains("B\n"));
370    }
371    
372    #[test]
373    fn test_set_stroke_color() {
374        let mut ctx = GraphicsContext::new();
375        ctx.set_stroke_color(Color::rgb(0.5, 0.6, 0.7));
376        assert_eq!(ctx.stroke_color(), Color::Rgb(0.5, 0.6, 0.7));
377    }
378    
379    #[test]
380    fn test_set_fill_color() {
381        let mut ctx = GraphicsContext::new();
382        ctx.set_fill_color(Color::gray(0.5));
383        assert_eq!(ctx.fill_color(), Color::Gray(0.5));
384    }
385    
386    #[test]
387    fn test_set_line_width() {
388        let mut ctx = GraphicsContext::new();
389        ctx.set_line_width(2.5);
390        assert_eq!(ctx.line_width(), 2.5);
391        assert!(ctx.operations().contains("2.50 w\n"));
392    }
393    
394    #[test]
395    fn test_set_line_cap() {
396        let mut ctx = GraphicsContext::new();
397        ctx.set_line_cap(LineCap::Round);
398        assert!(ctx.operations().contains("1 J\n"));
399        
400        ctx.set_line_cap(LineCap::Butt);
401        assert!(ctx.operations().contains("0 J\n"));
402        
403        ctx.set_line_cap(LineCap::Square);
404        assert!(ctx.operations().contains("2 J\n"));
405    }
406    
407    #[test]
408    fn test_set_line_join() {
409        let mut ctx = GraphicsContext::new();
410        ctx.set_line_join(LineJoin::Round);
411        assert!(ctx.operations().contains("1 j\n"));
412        
413        ctx.set_line_join(LineJoin::Miter);
414        assert!(ctx.operations().contains("0 j\n"));
415        
416        ctx.set_line_join(LineJoin::Bevel);
417        assert!(ctx.operations().contains("2 j\n"));
418    }
419    
420    #[test]
421    fn test_save_restore_state() {
422        let mut ctx = GraphicsContext::new();
423        ctx.save_state();
424        assert!(ctx.operations().contains("q\n"));
425        
426        ctx.restore_state();
427        assert!(ctx.operations().contains("Q\n"));
428    }
429    
430    #[test]
431    fn test_translate() {
432        let mut ctx = GraphicsContext::new();
433        ctx.translate(50.0, 100.0);
434        assert!(ctx.operations().contains("1 0 0 1 50.00 100.00 cm\n"));
435    }
436    
437    #[test]
438    fn test_scale() {
439        let mut ctx = GraphicsContext::new();
440        ctx.scale(2.0, 3.0);
441        assert!(ctx.operations().contains("2.00 0 0 3.00 0 0 cm\n"));
442    }
443    
444    #[test]
445    fn test_rotate() {
446        let mut ctx = GraphicsContext::new();
447        let angle = std::f64::consts::PI / 4.0; // 45 degrees
448        ctx.rotate(angle);
449        
450        let ops = ctx.operations();
451        assert!(ops.contains(" cm\n"));
452        // Should contain cos and sin values
453        assert!(ops.contains("0.707107")); // Approximate cos(45°)
454    }
455    
456    #[test]
457    fn test_transform() {
458        let mut ctx = GraphicsContext::new();
459        ctx.transform(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
460        assert!(ctx.operations().contains("1.00 2.00 3.00 4.00 5.00 6.00 cm\n"));
461    }
462    
463    #[test]
464    fn test_draw_image() {
465        let mut ctx = GraphicsContext::new();
466        ctx.draw_image("Image1", 10.0, 20.0, 100.0, 150.0);
467        
468        let ops = ctx.operations();
469        assert!(ops.contains("q\n")); // Save state
470        assert!(ops.contains("100.00 0 0 150.00 10.00 20.00 cm\n")); // Transform
471        assert!(ops.contains("/Image1 Do\n")); // Draw image
472        assert!(ops.contains("Q\n")); // Restore state
473    }
474    
475    #[test]
476    fn test_gray_color_operations() {
477        let mut ctx = GraphicsContext::new();
478        ctx.set_stroke_color(Color::gray(0.5));
479        ctx.set_fill_color(Color::gray(0.7));
480        ctx.stroke();
481        ctx.fill();
482        
483        let ops = ctx.operations();
484        assert!(ops.contains("0.500 G\n")); // Stroke gray
485        assert!(ops.contains("0.700 g\n")); // Fill gray
486    }
487    
488    #[test]
489    fn test_cmyk_color_operations() {
490        let mut ctx = GraphicsContext::new();
491        ctx.set_stroke_color(Color::cmyk(0.1, 0.2, 0.3, 0.4));
492        ctx.set_fill_color(Color::cmyk(0.5, 0.6, 0.7, 0.8));
493        ctx.stroke();
494        ctx.fill();
495        
496        let ops = ctx.operations();
497        assert!(ops.contains("0.100 0.200 0.300 0.400 K\n")); // Stroke CMYK
498        assert!(ops.contains("0.500 0.600 0.700 0.800 k\n")); // Fill CMYK
499    }
500    
501    #[test]
502    fn test_method_chaining() {
503        let mut ctx = GraphicsContext::new();
504        ctx.move_to(0.0, 0.0)
505            .line_to(10.0, 0.0)
506            .line_to(10.0, 10.0)
507            .line_to(0.0, 10.0)
508            .close_path()
509            .set_fill_color(Color::red())
510            .fill();
511        
512        let ops = ctx.operations();
513        assert!(ops.contains("0.00 0.00 m\n"));
514        assert!(ops.contains("10.00 0.00 l\n"));
515        assert!(ops.contains("10.00 10.00 l\n"));
516        assert!(ops.contains("0.00 10.00 l\n"));
517        assert!(ops.contains("h\n"));
518        assert!(ops.contains("f\n"));
519    }
520    
521    #[test]
522    fn test_generate_operations() {
523        let mut ctx = GraphicsContext::new();
524        ctx.rect(0.0, 0.0, 10.0, 10.0);
525        
526        let result = ctx.generate_operations();
527        assert!(result.is_ok());
528        let bytes = result.unwrap();
529        let ops_string = String::from_utf8(bytes).unwrap();
530        assert!(ops_string.contains("0.00 0.00 10.00 10.00 re"));
531    }
532    
533    #[test]
534    fn test_clear_operations() {
535        let mut ctx = GraphicsContext::new();
536        ctx.rect(0.0, 0.0, 10.0, 10.0);
537        assert!(!ctx.operations().is_empty());
538        
539        ctx.clear();
540        assert!(ctx.operations().is_empty());
541    }
542    
543    #[test]
544    fn test_complex_path() {
545        let mut ctx = GraphicsContext::new();
546        ctx.save_state()
547            .translate(100.0, 100.0)
548            .rotate(std::f64::consts::PI / 6.0)
549            .scale(2.0, 2.0)
550            .set_line_width(2.0)
551            .set_stroke_color(Color::blue())
552            .move_to(0.0, 0.0)
553            .line_to(50.0, 0.0)
554            .curve_to(50.0, 25.0, 25.0, 50.0, 0.0, 50.0)
555            .close_path()
556            .stroke()
557            .restore_state();
558        
559        let ops = ctx.operations();
560        assert!(ops.contains("q\n"));
561        assert!(ops.contains("cm\n"));
562        assert!(ops.contains("2.00 w\n"));
563        assert!(ops.contains("0.000 0.000 1.000 RG\n"));
564        assert!(ops.contains("S\n"));
565        assert!(ops.contains("Q\n"));
566    }
567    
568    #[test]
569    fn test_graphics_context_clone() {
570        let mut ctx = GraphicsContext::new();
571        ctx.set_fill_color(Color::red());
572        ctx.set_stroke_color(Color::blue());
573        ctx.set_line_width(3.0);
574        ctx.rect(0.0, 0.0, 10.0, 10.0);
575        
576        let ctx_clone = ctx.clone();
577        assert_eq!(ctx_clone.fill_color(), Color::red());
578        assert_eq!(ctx_clone.stroke_color(), Color::blue());
579        assert_eq!(ctx_clone.line_width(), 3.0);
580        assert_eq!(ctx_clone.operations(), ctx.operations());
581    }
582}