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 self.save_state();
181
182 writeln!(
185 &mut self.operations,
186 "{width:.2} 0 0 {height:.2} {x:.2} {y:.2} cm"
187 )
188 .unwrap();
189
190 writeln!(&mut self.operations, "/{image_name} Do").unwrap();
192
193 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 pub fn fill_color(&self) -> Color {
233 self.current_color
234 }
235
236 pub fn stroke_color(&self) -> Color {
238 self.stroke_color
239 }
240
241 pub fn line_width(&self) -> f64 {
243 self.line_width
244 }
245
246 pub fn operations(&self) -> &str {
248 &self.operations
249 }
250
251 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 assert!(ops.contains("75.00 50.00 m\n"));
321 assert!(ops.contains(" c\n"));
323 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; ctx.rotate(angle);
449
450 let ops = ctx.operations();
451 assert!(ops.contains(" cm\n"));
452 assert!(ops.contains("0.707107")); }
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")); assert!(ops.contains("100.00 0 0 150.00 10.00 20.00 cm\n")); assert!(ops.contains("/Image1 Do\n")); assert!(ops.contains("Q\n")); }
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")); assert!(ops.contains("0.700 g\n")); }
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")); assert!(ops.contains("0.500 0.600 0.700 0.800 k\n")); }
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}