1pub mod calibrated_color;
2pub mod clipping;
3pub(crate) mod color;
4mod color_profiles;
5pub mod devicen_color;
6pub mod extraction;
7pub mod form_xobject;
8mod indexed_color;
9pub mod lab_color;
10pub mod page_color_space;
11mod path;
12mod patterns;
13mod pdf_image;
14mod png_decoder;
15pub mod separation_color;
16mod shadings;
17pub mod soft_mask;
18pub mod state;
19pub mod transparency;
20
21pub use calibrated_color::{CalGrayColorSpace, CalRgbColorSpace, CalibratedColor};
22pub use clipping::{ClippingPath, ClippingRegion};
23pub use color::Color;
24pub use color_profiles::{IccColorSpace, IccProfile, IccProfileManager, StandardIccProfile};
25pub use devicen_color::{
26 AlternateColorSpace as DeviceNAlternateColorSpace, ColorantDefinition, ColorantType,
27 DeviceNAttributes, DeviceNColorSpace, LinearTransform, SampledFunction, TintTransformFunction,
28};
29pub use form_xobject::{
30 FormTemplates, FormXObject, FormXObjectBuilder, FormXObjectManager,
31 TransparencyGroup as FormTransparencyGroup,
32};
33pub use indexed_color::{BaseColorSpace, ColorLookupTable, IndexedColorManager, IndexedColorSpace};
34pub use lab_color::{LabColor, LabColorSpace};
35pub use page_color_space::{DeviceColorSpace, PageColorSpace, ParameterisedFamily};
36pub use path::{LineCap, LineJoin, PathBuilder, PathCommand, WindingRule};
37pub use patterns::{
38 PaintType, PatternGraphicsContext, PatternManager, PatternMatrix, PatternType, TilingPattern,
39 TilingType,
40};
41pub use pdf_image::{ColorSpace, Image, ImageFormat, MaskType};
42pub use separation_color::{
43 AlternateColorSpace, SeparationColor, SeparationColorSpace, SpotColors, TintTransform,
44};
45pub use shadings::{
46 AxialShading, ColorStop, FunctionBasedShading, Point, RadialShading, ShadingDefinition,
47 ShadingManager, ShadingPattern, ShadingType,
48};
49pub use soft_mask::{SoftMask, SoftMaskState, SoftMaskType};
50pub use state::{
51 BlendMode, ExtGState, ExtGStateFont, ExtGStateManager, Halftone, LineDashPattern,
52 RenderingIntent, TransferFunction,
53};
54pub use transparency::TransparencyGroup;
55use transparency::TransparencyGroupState;
56
57use crate::error::Result;
58use crate::text::{ColumnContent, ColumnLayout, Font, FontManager, ListElement, Table};
59use std::collections::{HashMap, HashSet};
60use std::fmt::Write;
61use std::sync::Arc;
62
63#[derive(Clone)]
66struct GraphicsState {
67 fill_color: Color,
68 stroke_color: Color,
69 font_name: Option<Arc<str>>,
70 font_size: f64,
71 is_custom_font: bool,
72}
73
74#[derive(Clone)]
75pub struct GraphicsContext {
76 operations: String,
77 current_color: Color,
78 stroke_color: Color,
79 line_width: f64,
80 fill_opacity: f64,
81 stroke_opacity: f64,
82 extgstate_manager: ExtGStateManager,
84 pending_extgstate: Option<ExtGState>,
85 current_dash_pattern: Option<LineDashPattern>,
86 current_miter_limit: f64,
87 current_line_cap: LineCap,
88 current_line_join: LineJoin,
89 current_rendering_intent: RenderingIntent,
90 current_flatness: f64,
91 current_smoothness: f64,
92 clipping_region: ClippingRegion,
94 font_manager: Option<Arc<FontManager>>,
96 state_stack: Vec<GraphicsState>,
98 current_font_name: Option<Arc<str>>,
99 current_font_size: f64,
100 is_custom_font: bool,
102 used_characters_by_font: HashMap<String, HashSet<char>>,
108 glyph_mapping: Option<HashMap<u32, u16>>,
110 transparency_stack: Vec<TransparencyGroupState>,
112}
113
114fn encode_char_as_cid(ch: char, buf: &mut String) {
118 let code = ch as u32;
119 if code <= 0xFFFF {
120 write!(buf, "{:04X}", code).expect("Writing to string should never fail");
121 } else {
122 let adjusted = code - 0x10000;
124 let high = ((adjusted >> 10) & 0x3FF) + 0xD800;
125 let low = (adjusted & 0x3FF) + 0xDC00;
126 write!(buf, "{:04X}{:04X}", high, low).expect("Writing to string should never fail");
127 }
128}
129
130impl Default for GraphicsContext {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136impl GraphicsContext {
137 pub fn new() -> Self {
138 Self {
139 operations: String::new(),
140 current_color: Color::black(),
141 stroke_color: Color::black(),
142 line_width: 1.0,
143 fill_opacity: 1.0,
144 stroke_opacity: 1.0,
145 extgstate_manager: ExtGStateManager::new(),
147 pending_extgstate: None,
148 current_dash_pattern: None,
149 current_miter_limit: 10.0,
150 current_line_cap: LineCap::Butt,
151 current_line_join: LineJoin::Miter,
152 current_rendering_intent: RenderingIntent::RelativeColorimetric,
153 current_flatness: 1.0,
154 current_smoothness: 0.0,
155 clipping_region: ClippingRegion::new(),
157 font_manager: None,
159 state_stack: Vec::new(),
160 current_font_name: None,
161 current_font_size: 12.0,
162 is_custom_font: false,
163 used_characters_by_font: HashMap::new(),
164 glyph_mapping: None,
165 transparency_stack: Vec::new(),
166 }
167 }
168
169 pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
170 writeln!(&mut self.operations, "{x:.2} {y:.2} m")
171 .expect("Writing to string should never fail");
172 self
173 }
174
175 pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
176 writeln!(&mut self.operations, "{x:.2} {y:.2} l")
177 .expect("Writing to string should never fail");
178 self
179 }
180
181 pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
182 writeln!(
183 &mut self.operations,
184 "{x1:.2} {y1:.2} {x2:.2} {y2:.2} {x3:.2} {y3:.2} c"
185 )
186 .expect("Writing to string should never fail");
187 self
188 }
189
190 pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
191 writeln!(
192 &mut self.operations,
193 "{x:.2} {y:.2} {width:.2} {height:.2} re"
194 )
195 .expect("Writing to string should never fail");
196 self
197 }
198
199 pub fn circle(&mut self, cx: f64, cy: f64, radius: f64) -> &mut Self {
200 let k = 0.552284749831;
201 let r = radius;
202
203 self.move_to(cx + r, cy);
204 self.curve_to(cx + r, cy + k * r, cx + k * r, cy + r, cx, cy + r);
205 self.curve_to(cx - k * r, cy + r, cx - r, cy + k * r, cx - r, cy);
206 self.curve_to(cx - r, cy - k * r, cx - k * r, cy - r, cx, cy - r);
207 self.curve_to(cx + k * r, cy - r, cx + r, cy - k * r, cx + r, cy);
208 self.close_path()
209 }
210
211 pub fn close_path(&mut self) -> &mut Self {
212 self.operations.push_str("h\n");
213 self
214 }
215
216 pub fn stroke(&mut self) -> &mut Self {
217 self.apply_pending_extgstate().unwrap_or_default();
218 self.apply_stroke_color();
219 self.operations.push_str("S\n");
220 self
221 }
222
223 pub fn fill(&mut self) -> &mut Self {
224 self.apply_pending_extgstate().unwrap_or_default();
225 self.apply_fill_color();
226 self.operations.push_str("f\n");
227 self
228 }
229
230 pub fn fill_stroke(&mut self) -> &mut Self {
231 self.apply_pending_extgstate().unwrap_or_default();
232 self.apply_fill_color();
233 self.apply_stroke_color();
234 self.operations.push_str("B\n");
235 self
236 }
237
238 pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
239 self.stroke_color = color;
240 self
241 }
242
243 pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
244 self.current_color = color;
245 self
246 }
247
248 pub fn set_fill_color_calibrated(&mut self, color: CalibratedColor) -> &mut Self {
250 let cs_name = match &color {
252 CalibratedColor::Gray(_, _) => "CalGray1",
253 CalibratedColor::Rgb(_, _) => "CalRGB1",
254 };
255
256 writeln!(&mut self.operations, "/{} cs", cs_name)
258 .expect("Writing to string should never fail");
259
260 let values = color.values();
262 for value in &values {
263 write!(&mut self.operations, "{:.4} ", value)
264 .expect("Writing to string should never fail");
265 }
266 writeln!(&mut self.operations, "sc").expect("Writing to string should never fail");
267
268 self
269 }
270
271 pub fn set_stroke_color_calibrated(&mut self, color: CalibratedColor) -> &mut Self {
273 let cs_name = match &color {
275 CalibratedColor::Gray(_, _) => "CalGray1",
276 CalibratedColor::Rgb(_, _) => "CalRGB1",
277 };
278
279 writeln!(&mut self.operations, "/{} CS", cs_name)
281 .expect("Writing to string should never fail");
282
283 let values = color.values();
285 for value in &values {
286 write!(&mut self.operations, "{:.4} ", value)
287 .expect("Writing to string should never fail");
288 }
289 writeln!(&mut self.operations, "SC").expect("Writing to string should never fail");
290
291 self
292 }
293
294 pub fn set_fill_color_lab(&mut self, color: LabColor) -> &mut Self {
296 writeln!(&mut self.operations, "/Lab1 cs").expect("Writing to string should never fail");
298
299 let values = color.values();
301 for value in &values {
302 write!(&mut self.operations, "{:.4} ", value)
303 .expect("Writing to string should never fail");
304 }
305 writeln!(&mut self.operations, "sc").expect("Writing to string should never fail");
306
307 self
308 }
309
310 pub fn set_stroke_color_lab(&mut self, color: LabColor) -> &mut Self {
312 writeln!(&mut self.operations, "/Lab1 CS").expect("Writing to string should never fail");
314
315 let values = color.values();
317 for value in &values {
318 write!(&mut self.operations, "{:.4} ", value)
319 .expect("Writing to string should never fail");
320 }
321 writeln!(&mut self.operations, "SC").expect("Writing to string should never fail");
322
323 self
324 }
325
326 pub fn set_line_width(&mut self, width: f64) -> &mut Self {
327 self.line_width = width;
328 writeln!(&mut self.operations, "{width:.2} w")
329 .expect("Writing to string should never fail");
330 self
331 }
332
333 pub fn set_line_cap(&mut self, cap: LineCap) -> &mut Self {
334 self.current_line_cap = cap;
335 writeln!(&mut self.operations, "{} J", cap as u8)
336 .expect("Writing to string should never fail");
337 self
338 }
339
340 pub fn set_line_join(&mut self, join: LineJoin) -> &mut Self {
341 self.current_line_join = join;
342 writeln!(&mut self.operations, "{} j", join as u8)
343 .expect("Writing to string should never fail");
344 self
345 }
346
347 pub fn set_opacity(&mut self, opacity: f64) -> &mut Self {
349 let opacity = opacity.clamp(0.0, 1.0);
350 self.fill_opacity = opacity;
351 self.stroke_opacity = opacity;
352
353 if opacity < 1.0 {
355 let mut state = ExtGState::new();
356 state.alpha_fill = Some(opacity);
357 state.alpha_stroke = Some(opacity);
358 self.pending_extgstate = Some(state);
359 }
360
361 self
362 }
363
364 pub fn set_fill_opacity(&mut self, opacity: f64) -> &mut Self {
366 self.fill_opacity = opacity.clamp(0.0, 1.0);
367
368 if opacity < 1.0 {
370 if let Some(ref mut state) = self.pending_extgstate {
371 state.alpha_fill = Some(opacity);
372 } else {
373 let mut state = ExtGState::new();
374 state.alpha_fill = Some(opacity);
375 self.pending_extgstate = Some(state);
376 }
377 }
378
379 self
380 }
381
382 pub fn set_stroke_opacity(&mut self, opacity: f64) -> &mut Self {
384 self.stroke_opacity = opacity.clamp(0.0, 1.0);
385
386 if opacity < 1.0 {
388 if let Some(ref mut state) = self.pending_extgstate {
389 state.alpha_stroke = Some(opacity);
390 } else {
391 let mut state = ExtGState::new();
392 state.alpha_stroke = Some(opacity);
393 self.pending_extgstate = Some(state);
394 }
395 }
396
397 self
398 }
399
400 pub fn save_state(&mut self) -> &mut Self {
401 self.operations.push_str("q\n");
402 self.save_clipping_state();
403 self.state_stack.push(GraphicsState {
405 fill_color: self.current_color,
406 stroke_color: self.stroke_color,
407 font_name: self.current_font_name.clone(),
408 font_size: self.current_font_size,
409 is_custom_font: self.is_custom_font,
410 });
411 self
412 }
413
414 pub fn restore_state(&mut self) -> &mut Self {
415 self.operations.push_str("Q\n");
416 self.restore_clipping_state();
417 if let Some(state) = self.state_stack.pop() {
419 self.current_color = state.fill_color;
420 self.stroke_color = state.stroke_color;
421 self.current_font_name = state.font_name;
422 self.current_font_size = state.font_size;
423 self.is_custom_font = state.is_custom_font;
424 }
425 self
426 }
427
428 pub fn begin_transparency_group(&mut self, group: TransparencyGroup) -> &mut Self {
431 self.save_state();
433
434 writeln!(&mut self.operations, "% Begin Transparency Group")
436 .expect("Writing to string should never fail");
437
438 let mut extgstate = ExtGState::new();
440 extgstate = extgstate.with_blend_mode(group.blend_mode.clone());
441 extgstate.alpha_fill = Some(group.opacity as f64);
442 extgstate.alpha_stroke = Some(group.opacity as f64);
443
444 self.pending_extgstate = Some(extgstate);
446 let _ = self.apply_pending_extgstate();
447
448 let mut group_state = TransparencyGroupState::new(group);
450 group_state.saved_state = self.operations.as_bytes().to_vec();
452 self.transparency_stack.push(group_state);
453
454 self
455 }
456
457 pub fn end_transparency_group(&mut self) -> &mut Self {
459 if let Some(_group_state) = self.transparency_stack.pop() {
460 writeln!(&mut self.operations, "% End Transparency Group")
462 .expect("Writing to string should never fail");
463
464 self.restore_state();
466 }
467 self
468 }
469
470 pub fn in_transparency_group(&self) -> bool {
472 !self.transparency_stack.is_empty()
473 }
474
475 pub fn current_transparency_group(&self) -> Option<&TransparencyGroup> {
477 self.transparency_stack.last().map(|state| &state.group)
478 }
479
480 pub fn translate(&mut self, tx: f64, ty: f64) -> &mut Self {
481 writeln!(&mut self.operations, "1 0 0 1 {tx:.2} {ty:.2} cm")
482 .expect("Writing to string should never fail");
483 self
484 }
485
486 pub fn scale(&mut self, sx: f64, sy: f64) -> &mut Self {
487 writeln!(&mut self.operations, "{sx:.2} 0 0 {sy:.2} 0 0 cm")
488 .expect("Writing to string should never fail");
489 self
490 }
491
492 pub fn rotate(&mut self, angle: f64) -> &mut Self {
493 let cos = angle.cos();
494 let sin = angle.sin();
495 writeln!(
496 &mut self.operations,
497 "{:.6} {:.6} {:.6} {:.6} 0 0 cm",
498 cos, sin, -sin, cos
499 )
500 .expect("Writing to string should never fail");
501 self
502 }
503
504 pub fn transform(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> &mut Self {
505 writeln!(
506 &mut self.operations,
507 "{a:.2} {b:.2} {c:.2} {d:.2} {e:.2} {f:.2} cm"
508 )
509 .expect("Writing to string should never fail");
510 self
511 }
512
513 pub fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
514 self.rect(x, y, width, height)
515 }
516
517 pub fn draw_image(
518 &mut self,
519 image_name: &str,
520 x: f64,
521 y: f64,
522 width: f64,
523 height: f64,
524 ) -> &mut Self {
525 self.save_state();
527
528 writeln!(
531 &mut self.operations,
532 "{width:.2} 0 0 {height:.2} {x:.2} {y:.2} cm"
533 )
534 .expect("Writing to string should never fail");
535
536 writeln!(&mut self.operations, "/{image_name} Do")
538 .expect("Writing to string should never fail");
539
540 self.restore_state();
542
543 self
544 }
545
546 pub fn draw_image_with_transparency(
549 &mut self,
550 image_name: &str,
551 x: f64,
552 y: f64,
553 width: f64,
554 height: f64,
555 mask_name: Option<&str>,
556 ) -> &mut Self {
557 self.save_state();
559
560 if let Some(mask) = mask_name {
562 let mut extgstate = ExtGState::new();
564 extgstate.set_soft_mask_name(mask.to_string());
565
566 let gs_name = self
568 .extgstate_manager
569 .add_state(extgstate)
570 .unwrap_or_else(|_| "GS1".to_string());
571 writeln!(&mut self.operations, "/{} gs", gs_name)
572 .expect("Writing to string should never fail");
573 }
574
575 writeln!(
577 &mut self.operations,
578 "{width:.2} 0 0 {height:.2} {x:.2} {y:.2} cm"
579 )
580 .expect("Writing to string should never fail");
581
582 writeln!(&mut self.operations, "/{image_name} Do")
584 .expect("Writing to string should never fail");
585
586 if mask_name.is_some() {
588 let mut reset_extgstate = ExtGState::new();
590 reset_extgstate.set_soft_mask_none();
591
592 let gs_name = self
593 .extgstate_manager
594 .add_state(reset_extgstate)
595 .unwrap_or_else(|_| "GS2".to_string());
596 writeln!(&mut self.operations, "/{} gs", gs_name)
597 .expect("Writing to string should never fail");
598 }
599
600 self.restore_state();
602
603 self
604 }
605
606 fn apply_stroke_color(&mut self) {
607 color::write_stroke_color(&mut self.operations, self.stroke_color);
611 }
612
613 fn apply_fill_color(&mut self) {
614 color::write_fill_color(&mut self.operations, self.current_color);
617 }
618
619 pub(crate) fn generate_operations(&self) -> Result<Vec<u8>> {
620 Ok(self.operations.as_bytes().to_vec())
621 }
622
623 pub fn uses_transparency(&self) -> bool {
625 self.fill_opacity < 1.0 || self.stroke_opacity < 1.0
626 }
627
628 pub fn generate_graphics_state_dict(&self) -> Option<String> {
630 if !self.uses_transparency() {
631 return None;
632 }
633
634 let mut dict = String::from("<< /Type /ExtGState");
635
636 if self.fill_opacity < 1.0 {
637 write!(&mut dict, " /ca {:.3}", self.fill_opacity)
638 .expect("Writing to string should never fail");
639 }
640
641 if self.stroke_opacity < 1.0 {
642 write!(&mut dict, " /CA {:.3}", self.stroke_opacity)
643 .expect("Writing to string should never fail");
644 }
645
646 dict.push_str(" >>");
647 Some(dict)
648 }
649
650 pub fn fill_color(&self) -> Color {
652 self.current_color
653 }
654
655 pub fn stroke_color(&self) -> Color {
657 self.stroke_color
658 }
659
660 pub fn line_width(&self) -> f64 {
662 self.line_width
663 }
664
665 pub fn fill_opacity(&self) -> f64 {
667 self.fill_opacity
668 }
669
670 pub fn stroke_opacity(&self) -> f64 {
672 self.stroke_opacity
673 }
674
675 pub fn operations(&self) -> &str {
677 &self.operations
678 }
679
680 pub fn get_operations(&self) -> &str {
682 &self.operations
683 }
684
685 pub fn clear(&mut self) {
687 self.operations.clear();
688 }
689
690 pub fn begin_text(&mut self) -> &mut Self {
692 self.operations.push_str("BT\n");
693 self
694 }
695
696 pub fn end_text(&mut self) -> &mut Self {
698 self.operations.push_str("ET\n");
699 self
700 }
701
702 pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
704 writeln!(&mut self.operations, "/{} {} Tf", font.pdf_name(), size)
705 .expect("Writing to string should never fail");
706
707 match &font {
709 Font::Custom(name) => {
710 self.current_font_name = Some(Arc::from(name.as_str()));
711 self.current_font_size = size;
712 self.is_custom_font = true;
713 }
714 _ => {
715 self.current_font_name = Some(Arc::from(font.pdf_name().as_str()));
716 self.current_font_size = size;
717 self.is_custom_font = false;
718 }
719 }
720
721 self
722 }
723
724 pub fn set_text_position(&mut self, x: f64, y: f64) -> &mut Self {
726 writeln!(&mut self.operations, "{x:.2} {y:.2} Td")
727 .expect("Writing to string should never fail");
728 self
729 }
730
731 pub fn show_text(&mut self, text: &str) -> Result<&mut Self> {
738 self.record_used_chars(text);
742
743 if self.is_custom_font {
744 self.operations.push('<');
746 for ch in text.chars() {
747 encode_char_as_cid(ch, &mut self.operations);
748 }
749 self.operations.push_str("> Tj\n");
750 } else {
751 self.operations.push('(');
753 for ch in text.chars() {
754 match ch {
755 '(' => self.operations.push_str("\\("),
756 ')' => self.operations.push_str("\\)"),
757 '\\' => self.operations.push_str("\\\\"),
758 '\n' => self.operations.push_str("\\n"),
759 '\r' => self.operations.push_str("\\r"),
760 '\t' => self.operations.push_str("\\t"),
761 _ => self.operations.push(ch),
762 }
763 }
764 self.operations.push_str(") Tj\n");
765 }
766 Ok(self)
767 }
768
769 pub fn set_word_spacing(&mut self, spacing: f64) -> &mut Self {
771 writeln!(&mut self.operations, "{spacing:.2} Tw")
772 .expect("Writing to string should never fail");
773 self
774 }
775
776 pub fn set_character_spacing(&mut self, spacing: f64) -> &mut Self {
778 writeln!(&mut self.operations, "{spacing:.2} Tc")
779 .expect("Writing to string should never fail");
780 self
781 }
782
783 pub fn show_justified_text(&mut self, text: &str, target_width: f64) -> Result<&mut Self> {
785 let words: Vec<&str> = text.split_whitespace().collect();
787 if words.len() <= 1 {
788 return self.show_text(text);
790 }
791
792 let text_without_spaces = words.join("");
794 let natural_text_width = self.estimate_text_width_simple(&text_without_spaces);
795 let space_width = self.estimate_text_width_simple(" ");
796 let natural_width = natural_text_width + (space_width * (words.len() - 1) as f64);
797
798 let extra_space_needed = target_width - natural_width;
800 let word_gaps = (words.len() - 1) as f64;
801
802 if word_gaps > 0.0 && extra_space_needed > 0.0 {
803 let extra_word_spacing = extra_space_needed / word_gaps;
804
805 self.set_word_spacing(extra_word_spacing);
807
808 self.show_text(text)?;
810
811 self.set_word_spacing(0.0);
813 } else {
814 self.show_text(text)?;
816 }
817
818 Ok(self)
819 }
820
821 fn estimate_text_width_simple(&self, text: &str) -> f64 {
823 let font_size = self.current_font_size;
826 text.len() as f64 * font_size * 0.6 }
828
829 pub fn render_table(&mut self, table: &Table) -> Result<()> {
831 table.render(self)
832 }
833
834 pub fn render_list(&mut self, list: &ListElement) -> Result<()> {
836 match list {
837 ListElement::Ordered(ordered) => ordered.render(self),
838 ListElement::Unordered(unordered) => unordered.render(self),
839 }
840 }
841
842 pub fn render_column_layout(
844 &mut self,
845 layout: &ColumnLayout,
846 content: &ColumnContent,
847 x: f64,
848 y: f64,
849 height: f64,
850 ) -> Result<()> {
851 layout.render(self, content, x, y, height)
852 }
853
854 pub fn set_line_dash_pattern(&mut self, pattern: LineDashPattern) -> &mut Self {
858 self.current_dash_pattern = Some(pattern.clone());
859 writeln!(&mut self.operations, "{} d", pattern.to_pdf_string())
860 .expect("Writing to string should never fail");
861 self
862 }
863
864 pub fn set_line_solid(&mut self) -> &mut Self {
866 self.current_dash_pattern = None;
867 self.operations.push_str("[] 0 d\n");
868 self
869 }
870
871 pub fn set_miter_limit(&mut self, limit: f64) -> &mut Self {
873 self.current_miter_limit = limit.max(1.0);
874 writeln!(&mut self.operations, "{:.2} M", self.current_miter_limit)
875 .expect("Writing to string should never fail");
876 self
877 }
878
879 pub fn set_rendering_intent(&mut self, intent: RenderingIntent) -> &mut Self {
881 self.current_rendering_intent = intent;
882 writeln!(&mut self.operations, "/{} ri", intent.pdf_name())
883 .expect("Writing to string should never fail");
884 self
885 }
886
887 pub fn set_flatness(&mut self, flatness: f64) -> &mut Self {
889 self.current_flatness = flatness.clamp(0.0, 100.0);
890 writeln!(&mut self.operations, "{:.2} i", self.current_flatness)
891 .expect("Writing to string should never fail");
892 self
893 }
894
895 pub fn apply_extgstate(&mut self, state: ExtGState) -> Result<&mut Self> {
897 let state_name = self.extgstate_manager.add_state(state)?;
898 writeln!(&mut self.operations, "/{state_name} gs")
899 .expect("Writing to string should never fail");
900 Ok(self)
901 }
902
903 #[allow(dead_code)]
905 fn set_pending_extgstate(&mut self, state: ExtGState) {
906 self.pending_extgstate = Some(state);
907 }
908
909 fn apply_pending_extgstate(&mut self) -> Result<()> {
911 if let Some(state) = self.pending_extgstate.take() {
912 let state_name = self.extgstate_manager.add_state(state)?;
913 writeln!(&mut self.operations, "/{state_name} gs")
914 .expect("Writing to string should never fail");
915 }
916 Ok(())
917 }
918
919 pub fn with_extgstate<F>(&mut self, builder: F) -> Result<&mut Self>
921 where
922 F: FnOnce(ExtGState) -> ExtGState,
923 {
924 let state = builder(ExtGState::new());
925 self.apply_extgstate(state)
926 }
927
928 pub fn set_blend_mode(&mut self, mode: BlendMode) -> Result<&mut Self> {
930 let state = ExtGState::new().with_blend_mode(mode);
931 self.apply_extgstate(state)
932 }
933
934 pub fn set_alpha(&mut self, alpha: f64) -> Result<&mut Self> {
936 let state = ExtGState::new().with_alpha(alpha);
937 self.apply_extgstate(state)
938 }
939
940 pub fn set_alpha_stroke(&mut self, alpha: f64) -> Result<&mut Self> {
942 let state = ExtGState::new().with_alpha_stroke(alpha);
943 self.apply_extgstate(state)
944 }
945
946 pub fn set_alpha_fill(&mut self, alpha: f64) -> Result<&mut Self> {
948 let state = ExtGState::new().with_alpha_fill(alpha);
949 self.apply_extgstate(state)
950 }
951
952 pub fn set_overprint_stroke(&mut self, overprint: bool) -> Result<&mut Self> {
954 let state = ExtGState::new().with_overprint_stroke(overprint);
955 self.apply_extgstate(state)
956 }
957
958 pub fn set_overprint_fill(&mut self, overprint: bool) -> Result<&mut Self> {
960 let state = ExtGState::new().with_overprint_fill(overprint);
961 self.apply_extgstate(state)
962 }
963
964 pub fn set_stroke_adjustment(&mut self, adjustment: bool) -> Result<&mut Self> {
966 let state = ExtGState::new().with_stroke_adjustment(adjustment);
967 self.apply_extgstate(state)
968 }
969
970 pub fn set_smoothness(&mut self, smoothness: f64) -> Result<&mut Self> {
972 self.current_smoothness = smoothness.clamp(0.0, 1.0);
973 let state = ExtGState::new().with_smoothness(self.current_smoothness);
974 self.apply_extgstate(state)
975 }
976
977 pub fn line_dash_pattern(&self) -> Option<&LineDashPattern> {
981 self.current_dash_pattern.as_ref()
982 }
983
984 pub fn miter_limit(&self) -> f64 {
986 self.current_miter_limit
987 }
988
989 pub fn line_cap(&self) -> LineCap {
991 self.current_line_cap
992 }
993
994 pub fn line_join(&self) -> LineJoin {
996 self.current_line_join
997 }
998
999 pub fn rendering_intent(&self) -> RenderingIntent {
1001 self.current_rendering_intent
1002 }
1003
1004 pub fn flatness(&self) -> f64 {
1006 self.current_flatness
1007 }
1008
1009 pub fn smoothness(&self) -> f64 {
1011 self.current_smoothness
1012 }
1013
1014 pub fn extgstate_manager(&self) -> &ExtGStateManager {
1016 &self.extgstate_manager
1017 }
1018
1019 pub fn extgstate_manager_mut(&mut self) -> &mut ExtGStateManager {
1021 &mut self.extgstate_manager
1022 }
1023
1024 pub fn generate_extgstate_resources(&self) -> Result<String> {
1026 self.extgstate_manager.to_resource_dictionary()
1027 }
1028
1029 pub fn has_extgstates(&self) -> bool {
1031 self.extgstate_manager.count() > 0
1032 }
1033
1034 pub fn add_command(&mut self, command: &str) {
1036 self.operations.push_str(command);
1037 self.operations.push('\n');
1038 }
1039
1040 pub fn clip(&mut self) -> &mut Self {
1042 self.operations.push_str("W\n");
1043 self
1044 }
1045
1046 pub fn clip_even_odd(&mut self) -> &mut Self {
1048 self.operations.push_str("W*\n");
1049 self
1050 }
1051
1052 pub fn clip_stroke(&mut self) -> &mut Self {
1054 self.apply_stroke_color();
1055 self.operations.push_str("W S\n");
1056 self
1057 }
1058
1059 pub fn set_clipping_path(&mut self, path: ClippingPath) -> Result<&mut Self> {
1061 let ops = path.to_pdf_operations()?;
1062 self.operations.push_str(&ops);
1063 self.clipping_region.set_clip(path);
1064 Ok(self)
1065 }
1066
1067 pub fn clear_clipping(&mut self) -> &mut Self {
1069 self.clipping_region.clear_clip();
1070 self
1071 }
1072
1073 fn save_clipping_state(&mut self) {
1075 self.clipping_region.save();
1076 }
1077
1078 fn restore_clipping_state(&mut self) {
1080 self.clipping_region.restore();
1081 }
1082
1083 pub fn clip_rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> Result<&mut Self> {
1085 let path = ClippingPath::rect(x, y, width, height);
1086 self.set_clipping_path(path)
1087 }
1088
1089 pub fn clip_circle(&mut self, cx: f64, cy: f64, radius: f64) -> Result<&mut Self> {
1091 let path = ClippingPath::circle(cx, cy, radius);
1092 self.set_clipping_path(path)
1093 }
1094
1095 pub fn clip_ellipse(&mut self, cx: f64, cy: f64, rx: f64, ry: f64) -> Result<&mut Self> {
1097 let path = ClippingPath::ellipse(cx, cy, rx, ry);
1098 self.set_clipping_path(path)
1099 }
1100
1101 pub fn has_clipping(&self) -> bool {
1103 self.clipping_region.has_clip()
1104 }
1105
1106 pub fn clipping_path(&self) -> Option<&ClippingPath> {
1108 self.clipping_region.current()
1109 }
1110
1111 pub fn set_font_manager(&mut self, font_manager: Arc<FontManager>) -> &mut Self {
1113 self.font_manager = Some(font_manager);
1114 self
1115 }
1116
1117 pub fn set_custom_font(&mut self, font_name: &str, size: f64) -> &mut Self {
1119 writeln!(&mut self.operations, "/{} {} Tf", font_name, size)
1121 .expect("Writing to string should never fail");
1122
1123 self.current_font_name = Some(Arc::from(font_name));
1124 self.current_font_size = size;
1125 self.is_custom_font = true;
1126
1127 if let Some(ref font_manager) = self.font_manager {
1129 if let Some(mapping) = font_manager.get_font_glyph_mapping(font_name) {
1130 self.glyph_mapping = Some(mapping);
1131 }
1132 }
1133
1134 self
1135 }
1136
1137 pub fn set_glyph_mapping(&mut self, mapping: HashMap<u32, u16>) -> &mut Self {
1139 self.glyph_mapping = Some(mapping);
1140 self
1141 }
1142
1143 pub fn draw_text(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1145 self.record_used_chars(text);
1148
1149 let needs_unicode = self.is_custom_font || text.chars().any(|c| c as u32 > 255);
1152
1153 if needs_unicode {
1155 self.draw_with_unicode_encoding(text, x, y)
1156 } else {
1157 self.draw_with_simple_encoding(text, x, y)
1158 }
1159 }
1160
1161 fn draw_with_simple_encoding(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1163 let has_unicode = text.chars().any(|c| c as u32 > 255);
1165
1166 if has_unicode {
1167 tracing::debug!("Warning: Text contains Unicode characters but using Latin-1 font. Characters will be replaced with '?'");
1169 }
1170
1171 self.operations.push_str("BT\n");
1173
1174 self.apply_fill_color();
1176
1177 if let Some(font_name) = &self.current_font_name {
1179 writeln!(
1180 &mut self.operations,
1181 "/{} {} Tf",
1182 font_name, self.current_font_size
1183 )
1184 .expect("Writing to string should never fail");
1185 } else {
1186 writeln!(
1187 &mut self.operations,
1188 "/Helvetica {} Tf",
1189 self.current_font_size
1190 )
1191 .expect("Writing to string should never fail");
1192 }
1193
1194 writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1196 .expect("Writing to string should never fail");
1197
1198 self.operations.push('(');
1201 for ch in text.chars() {
1202 let code = ch as u32;
1203 if code <= 127 {
1204 match ch {
1206 '(' => self.operations.push_str("\\("),
1207 ')' => self.operations.push_str("\\)"),
1208 '\\' => self.operations.push_str("\\\\"),
1209 '\n' => self.operations.push_str("\\n"),
1210 '\r' => self.operations.push_str("\\r"),
1211 '\t' => self.operations.push_str("\\t"),
1212 _ => self.operations.push(ch),
1213 }
1214 } else if code <= 255 {
1215 write!(&mut self.operations, "\\{:03o}", code)
1218 .expect("Writing to string should never fail");
1219 } else {
1220 self.operations.push('?');
1222 }
1223 }
1224 self.operations.push_str(") Tj\n");
1225
1226 self.operations.push_str("ET\n");
1228
1229 Ok(self)
1230 }
1231
1232 fn draw_with_unicode_encoding(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1234 self.operations.push_str("BT\n");
1236
1237 self.apply_fill_color();
1239
1240 if let Some(font_name) = &self.current_font_name {
1242 writeln!(
1244 &mut self.operations,
1245 "/{} {} Tf",
1246 font_name, self.current_font_size
1247 )
1248 .expect("Writing to string should never fail");
1249 } else {
1250 writeln!(
1251 &mut self.operations,
1252 "/Helvetica {} Tf",
1253 self.current_font_size
1254 )
1255 .expect("Writing to string should never fail");
1256 }
1257
1258 writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1260 .expect("Writing to string should never fail");
1261
1262 self.operations.push('<');
1265 for ch in text.chars() {
1266 encode_char_as_cid(ch, &mut self.operations);
1267 }
1268 self.operations.push_str("> Tj\n");
1269
1270 self.operations.push_str("ET\n");
1272
1273 Ok(self)
1274 }
1275
1276 #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1278 pub fn draw_text_hex(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1279 self.operations.push_str("BT\n");
1281
1282 self.apply_fill_color();
1284
1285 if let Some(font_name) = &self.current_font_name {
1287 writeln!(
1288 &mut self.operations,
1289 "/{} {} Tf",
1290 font_name, self.current_font_size
1291 )
1292 .expect("Writing to string should never fail");
1293 } else {
1294 writeln!(
1296 &mut self.operations,
1297 "/Helvetica {} Tf",
1298 self.current_font_size
1299 )
1300 .expect("Writing to string should never fail");
1301 }
1302
1303 writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1305 .expect("Writing to string should never fail");
1306
1307 self.operations.push('<');
1311 for ch in text.chars() {
1312 if ch as u32 <= 255 {
1313 write!(&mut self.operations, "{:02X}", ch as u8)
1315 .expect("Writing to string should never fail");
1316 } else {
1317 write!(&mut self.operations, "3F").expect("Writing to string should never fail");
1320 }
1322 }
1323 self.operations.push_str("> Tj\n");
1324
1325 self.operations.push_str("ET\n");
1327
1328 Ok(self)
1329 }
1330
1331 #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1333 pub fn draw_text_cid(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1334 use crate::fonts::needs_type0_font;
1335
1336 self.operations.push_str("BT\n");
1338
1339 self.apply_fill_color();
1341
1342 if let Some(font_name) = &self.current_font_name {
1344 writeln!(
1345 &mut self.operations,
1346 "/{} {} Tf",
1347 font_name, self.current_font_size
1348 )
1349 .expect("Writing to string should never fail");
1350 } else {
1351 writeln!(
1352 &mut self.operations,
1353 "/Helvetica {} Tf",
1354 self.current_font_size
1355 )
1356 .expect("Writing to string should never fail");
1357 }
1358
1359 writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1361 .expect("Writing to string should never fail");
1362
1363 if needs_type0_font(text) {
1365 self.operations.push('<');
1367 for ch in text.chars() {
1368 encode_char_as_cid(ch, &mut self.operations);
1369 }
1370 self.operations.push_str("> Tj\n");
1371 } else {
1372 self.operations.push('<');
1374 for ch in text.chars() {
1375 if ch as u32 <= 255 {
1376 write!(&mut self.operations, "{:02X}", ch as u8)
1377 .expect("Writing to string should never fail");
1378 } else {
1379 write!(&mut self.operations, "3F")
1380 .expect("Writing to string should never fail");
1381 }
1382 }
1383 self.operations.push_str("> Tj\n");
1384 }
1385
1386 self.operations.push_str("ET\n");
1388 Ok(self)
1389 }
1390
1391 #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1393 pub fn draw_text_unicode(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1394 self.operations.push_str("BT\n");
1396
1397 self.apply_fill_color();
1399
1400 if let Some(font_name) = &self.current_font_name {
1402 writeln!(
1403 &mut self.operations,
1404 "/{} {} Tf",
1405 font_name, self.current_font_size
1406 )
1407 .expect("Writing to string should never fail");
1408 } else {
1409 writeln!(
1411 &mut self.operations,
1412 "/Helvetica {} Tf",
1413 self.current_font_size
1414 )
1415 .expect("Writing to string should never fail");
1416 }
1417
1418 writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
1420 .expect("Writing to string should never fail");
1421
1422 self.operations.push('<');
1424 let mut utf16_buffer = [0u16; 2];
1425 for ch in text.chars() {
1426 let encoded = ch.encode_utf16(&mut utf16_buffer);
1427 for unit in encoded {
1428 write!(&mut self.operations, "{:04X}", unit)
1430 .expect("Writing to string should never fail");
1431 }
1432 }
1433 self.operations.push_str("> Tj\n");
1434
1435 self.operations.push_str("ET\n");
1437
1438 Ok(self)
1439 }
1440
1441 fn record_used_chars(&mut self, text: &str) {
1452 let bucket = self.current_font_name.as_deref().unwrap_or("").to_string();
1453 self.used_characters_by_font
1454 .entry(bucket)
1455 .or_default()
1456 .extend(text.chars());
1457 }
1458
1459 #[cfg(test)]
1465 pub(crate) fn get_used_characters(&self) -> Option<HashSet<char>> {
1466 let merged: HashSet<char> = self
1467 .used_characters_by_font
1468 .values()
1469 .flat_map(|s| s.iter().copied())
1470 .collect();
1471 if merged.is_empty() {
1472 None
1473 } else {
1474 Some(merged)
1475 }
1476 }
1477
1478 pub(crate) fn get_used_characters_by_font(&self) -> &HashMap<String, HashSet<char>> {
1486 &self.used_characters_by_font
1487 }
1488
1489 pub(crate) fn merge_font_usage(&mut self, usage: &HashMap<String, HashSet<char>>) {
1495 for (name, chars) in usage {
1496 self.used_characters_by_font
1497 .entry(name.clone())
1498 .or_default()
1499 .extend(chars);
1500 }
1501 }
1502}
1503
1504#[cfg(test)]
1505mod tests {
1506 use super::*;
1507
1508 #[test]
1509 fn test_graphics_context_new() {
1510 let ctx = GraphicsContext::new();
1511 assert_eq!(ctx.fill_color(), Color::black());
1512 assert_eq!(ctx.stroke_color(), Color::black());
1513 assert_eq!(ctx.line_width(), 1.0);
1514 assert_eq!(ctx.fill_opacity(), 1.0);
1515 assert_eq!(ctx.stroke_opacity(), 1.0);
1516 assert!(ctx.operations().is_empty());
1517 }
1518
1519 #[test]
1520 fn test_graphics_context_default() {
1521 let ctx = GraphicsContext::default();
1522 assert_eq!(ctx.fill_color(), Color::black());
1523 assert_eq!(ctx.stroke_color(), Color::black());
1524 assert_eq!(ctx.line_width(), 1.0);
1525 }
1526
1527 #[test]
1528 fn test_move_to() {
1529 let mut ctx = GraphicsContext::new();
1530 ctx.move_to(10.0, 20.0);
1531 assert!(ctx.operations().contains("10.00 20.00 m\n"));
1532 }
1533
1534 #[test]
1535 fn test_line_to() {
1536 let mut ctx = GraphicsContext::new();
1537 ctx.line_to(30.0, 40.0);
1538 assert!(ctx.operations().contains("30.00 40.00 l\n"));
1539 }
1540
1541 #[test]
1542 fn test_curve_to() {
1543 let mut ctx = GraphicsContext::new();
1544 ctx.curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0);
1545 assert!(ctx
1546 .operations()
1547 .contains("10.00 20.00 30.00 40.00 50.00 60.00 c\n"));
1548 }
1549
1550 #[test]
1551 fn test_rect() {
1552 let mut ctx = GraphicsContext::new();
1553 ctx.rect(10.0, 20.0, 100.0, 50.0);
1554 assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
1555 }
1556
1557 #[test]
1558 fn test_rectangle_alias() {
1559 let mut ctx = GraphicsContext::new();
1560 ctx.rectangle(10.0, 20.0, 100.0, 50.0);
1561 assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
1562 }
1563
1564 #[test]
1565 fn test_circle() {
1566 let mut ctx = GraphicsContext::new();
1567 ctx.circle(50.0, 50.0, 25.0);
1568
1569 let ops = ctx.operations();
1570 assert!(ops.contains("75.00 50.00 m\n"));
1572 assert!(ops.contains(" c\n"));
1574 assert!(ops.contains("h\n"));
1576 }
1577
1578 #[test]
1579 fn test_close_path() {
1580 let mut ctx = GraphicsContext::new();
1581 ctx.close_path();
1582 assert!(ctx.operations().contains("h\n"));
1583 }
1584
1585 #[test]
1586 fn test_stroke() {
1587 let mut ctx = GraphicsContext::new();
1588 ctx.set_stroke_color(Color::red());
1589 ctx.rect(0.0, 0.0, 10.0, 10.0);
1590 ctx.stroke();
1591
1592 let ops = ctx.operations();
1593 assert!(ops.contains("1.000 0.000 0.000 RG\n"));
1594 assert!(ops.contains("S\n"));
1595 }
1596
1597 #[test]
1598 fn test_fill() {
1599 let mut ctx = GraphicsContext::new();
1600 ctx.set_fill_color(Color::blue());
1601 ctx.rect(0.0, 0.0, 10.0, 10.0);
1602 ctx.fill();
1603
1604 let ops = ctx.operations();
1605 assert!(ops.contains("0.000 0.000 1.000 rg\n"));
1606 assert!(ops.contains("f\n"));
1607 }
1608
1609 #[test]
1610 fn test_fill_stroke() {
1611 let mut ctx = GraphicsContext::new();
1612 ctx.set_fill_color(Color::green());
1613 ctx.set_stroke_color(Color::red());
1614 ctx.rect(0.0, 0.0, 10.0, 10.0);
1615 ctx.fill_stroke();
1616
1617 let ops = ctx.operations();
1618 assert!(ops.contains("0.000 1.000 0.000 rg\n"));
1619 assert!(ops.contains("1.000 0.000 0.000 RG\n"));
1620 assert!(ops.contains("B\n"));
1621 }
1622
1623 #[test]
1624 fn test_set_stroke_color() {
1625 let mut ctx = GraphicsContext::new();
1626 ctx.set_stroke_color(Color::rgb(0.5, 0.6, 0.7));
1627 assert_eq!(ctx.stroke_color(), Color::Rgb(0.5, 0.6, 0.7));
1628 }
1629
1630 #[test]
1631 fn test_set_fill_color() {
1632 let mut ctx = GraphicsContext::new();
1633 ctx.set_fill_color(Color::gray(0.5));
1634 assert_eq!(ctx.fill_color(), Color::Gray(0.5));
1635 }
1636
1637 #[test]
1638 fn test_set_line_width() {
1639 let mut ctx = GraphicsContext::new();
1640 ctx.set_line_width(2.5);
1641 assert_eq!(ctx.line_width(), 2.5);
1642 assert!(ctx.operations().contains("2.50 w\n"));
1643 }
1644
1645 #[test]
1646 fn test_set_line_cap() {
1647 let mut ctx = GraphicsContext::new();
1648 ctx.set_line_cap(LineCap::Round);
1649 assert!(ctx.operations().contains("1 J\n"));
1650
1651 ctx.set_line_cap(LineCap::Butt);
1652 assert!(ctx.operations().contains("0 J\n"));
1653
1654 ctx.set_line_cap(LineCap::Square);
1655 assert!(ctx.operations().contains("2 J\n"));
1656 }
1657
1658 #[test]
1659 fn test_set_line_join() {
1660 let mut ctx = GraphicsContext::new();
1661 ctx.set_line_join(LineJoin::Round);
1662 assert!(ctx.operations().contains("1 j\n"));
1663
1664 ctx.set_line_join(LineJoin::Miter);
1665 assert!(ctx.operations().contains("0 j\n"));
1666
1667 ctx.set_line_join(LineJoin::Bevel);
1668 assert!(ctx.operations().contains("2 j\n"));
1669 }
1670
1671 #[test]
1672 fn test_save_restore_state() {
1673 let mut ctx = GraphicsContext::new();
1674 ctx.save_state();
1675 assert!(ctx.operations().contains("q\n"));
1676
1677 ctx.restore_state();
1678 assert!(ctx.operations().contains("Q\n"));
1679 }
1680
1681 #[test]
1682 fn test_translate() {
1683 let mut ctx = GraphicsContext::new();
1684 ctx.translate(50.0, 100.0);
1685 assert!(ctx.operations().contains("1 0 0 1 50.00 100.00 cm\n"));
1686 }
1687
1688 #[test]
1689 fn test_scale() {
1690 let mut ctx = GraphicsContext::new();
1691 ctx.scale(2.0, 3.0);
1692 assert!(ctx.operations().contains("2.00 0 0 3.00 0 0 cm\n"));
1693 }
1694
1695 #[test]
1696 fn test_rotate() {
1697 let mut ctx = GraphicsContext::new();
1698 let angle = std::f64::consts::PI / 4.0; ctx.rotate(angle);
1700
1701 let ops = ctx.operations();
1702 assert!(ops.contains(" cm\n"));
1703 assert!(ops.contains("0.707107")); }
1706
1707 #[test]
1708 fn test_transform() {
1709 let mut ctx = GraphicsContext::new();
1710 ctx.transform(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
1711 assert!(ctx
1712 .operations()
1713 .contains("1.00 2.00 3.00 4.00 5.00 6.00 cm\n"));
1714 }
1715
1716 #[test]
1717 fn test_draw_image() {
1718 let mut ctx = GraphicsContext::new();
1719 ctx.draw_image("Image1", 10.0, 20.0, 100.0, 150.0);
1720
1721 let ops = ctx.operations();
1722 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")); }
1727
1728 #[test]
1729 fn test_gray_color_operations() {
1730 let mut ctx = GraphicsContext::new();
1731 ctx.set_stroke_color(Color::gray(0.5));
1732 ctx.set_fill_color(Color::gray(0.7));
1733 ctx.stroke();
1734 ctx.fill();
1735
1736 let ops = ctx.operations();
1737 assert!(ops.contains("0.500 G\n")); assert!(ops.contains("0.700 g\n")); }
1740
1741 #[test]
1742 fn test_cmyk_color_operations() {
1743 let mut ctx = GraphicsContext::new();
1744 ctx.set_stroke_color(Color::cmyk(0.1, 0.2, 0.3, 0.4));
1745 ctx.set_fill_color(Color::cmyk(0.5, 0.6, 0.7, 0.8));
1746 ctx.stroke();
1747 ctx.fill();
1748
1749 let ops = ctx.operations();
1750 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")); }
1753
1754 #[test]
1755 fn test_method_chaining() {
1756 let mut ctx = GraphicsContext::new();
1757 ctx.move_to(0.0, 0.0)
1758 .line_to(10.0, 0.0)
1759 .line_to(10.0, 10.0)
1760 .line_to(0.0, 10.0)
1761 .close_path()
1762 .set_fill_color(Color::red())
1763 .fill();
1764
1765 let ops = ctx.operations();
1766 assert!(ops.contains("0.00 0.00 m\n"));
1767 assert!(ops.contains("10.00 0.00 l\n"));
1768 assert!(ops.contains("10.00 10.00 l\n"));
1769 assert!(ops.contains("0.00 10.00 l\n"));
1770 assert!(ops.contains("h\n"));
1771 assert!(ops.contains("f\n"));
1772 }
1773
1774 #[test]
1775 fn test_generate_operations() {
1776 let mut ctx = GraphicsContext::new();
1777 ctx.rect(0.0, 0.0, 10.0, 10.0);
1778
1779 let result = ctx.generate_operations();
1780 assert!(result.is_ok());
1781 let bytes = result.expect("Writing to string should never fail");
1782 let ops_string = String::from_utf8(bytes).expect("Writing to string should never fail");
1783 assert!(ops_string.contains("0.00 0.00 10.00 10.00 re"));
1784 }
1785
1786 #[test]
1787 fn test_clear_operations() {
1788 let mut ctx = GraphicsContext::new();
1789 ctx.rect(0.0, 0.0, 10.0, 10.0);
1790 assert!(!ctx.operations().is_empty());
1791
1792 ctx.clear();
1793 assert!(ctx.operations().is_empty());
1794 }
1795
1796 #[test]
1797 fn test_complex_path() {
1798 let mut ctx = GraphicsContext::new();
1799 ctx.save_state()
1800 .translate(100.0, 100.0)
1801 .rotate(std::f64::consts::PI / 6.0)
1802 .scale(2.0, 2.0)
1803 .set_line_width(2.0)
1804 .set_stroke_color(Color::blue())
1805 .move_to(0.0, 0.0)
1806 .line_to(50.0, 0.0)
1807 .curve_to(50.0, 25.0, 25.0, 50.0, 0.0, 50.0)
1808 .close_path()
1809 .stroke()
1810 .restore_state();
1811
1812 let ops = ctx.operations();
1813 assert!(ops.contains("q\n"));
1814 assert!(ops.contains("cm\n"));
1815 assert!(ops.contains("2.00 w\n"));
1816 assert!(ops.contains("0.000 0.000 1.000 RG\n"));
1817 assert!(ops.contains("S\n"));
1818 assert!(ops.contains("Q\n"));
1819 }
1820
1821 #[test]
1822 fn test_graphics_context_clone() {
1823 let mut ctx = GraphicsContext::new();
1824 ctx.set_fill_color(Color::red());
1825 ctx.set_stroke_color(Color::blue());
1826 ctx.set_line_width(3.0);
1827 ctx.set_opacity(0.5);
1828 ctx.rect(0.0, 0.0, 10.0, 10.0);
1829
1830 let ctx_clone = ctx.clone();
1831 assert_eq!(ctx_clone.fill_color(), Color::red());
1832 assert_eq!(ctx_clone.stroke_color(), Color::blue());
1833 assert_eq!(ctx_clone.line_width(), 3.0);
1834 assert_eq!(ctx_clone.fill_opacity(), 0.5);
1835 assert_eq!(ctx_clone.stroke_opacity(), 0.5);
1836 assert_eq!(ctx_clone.operations(), ctx.operations());
1837 }
1838
1839 #[test]
1840 fn test_set_opacity() {
1841 let mut ctx = GraphicsContext::new();
1842
1843 ctx.set_opacity(0.5);
1845 assert_eq!(ctx.fill_opacity(), 0.5);
1846 assert_eq!(ctx.stroke_opacity(), 0.5);
1847
1848 ctx.set_opacity(1.5);
1850 assert_eq!(ctx.fill_opacity(), 1.0);
1851 assert_eq!(ctx.stroke_opacity(), 1.0);
1852
1853 ctx.set_opacity(-0.5);
1854 assert_eq!(ctx.fill_opacity(), 0.0);
1855 assert_eq!(ctx.stroke_opacity(), 0.0);
1856 }
1857
1858 #[test]
1859 fn test_set_fill_opacity() {
1860 let mut ctx = GraphicsContext::new();
1861
1862 ctx.set_fill_opacity(0.3);
1863 assert_eq!(ctx.fill_opacity(), 0.3);
1864 assert_eq!(ctx.stroke_opacity(), 1.0); ctx.set_fill_opacity(2.0);
1868 assert_eq!(ctx.fill_opacity(), 1.0);
1869 }
1870
1871 #[test]
1872 fn test_set_stroke_opacity() {
1873 let mut ctx = GraphicsContext::new();
1874
1875 ctx.set_stroke_opacity(0.7);
1876 assert_eq!(ctx.stroke_opacity(), 0.7);
1877 assert_eq!(ctx.fill_opacity(), 1.0); ctx.set_stroke_opacity(-1.0);
1881 assert_eq!(ctx.stroke_opacity(), 0.0);
1882 }
1883
1884 #[test]
1885 fn test_uses_transparency() {
1886 let mut ctx = GraphicsContext::new();
1887
1888 assert!(!ctx.uses_transparency());
1890
1891 ctx.set_fill_opacity(0.5);
1893 assert!(ctx.uses_transparency());
1894
1895 ctx.set_fill_opacity(1.0);
1897 assert!(!ctx.uses_transparency());
1898 ctx.set_stroke_opacity(0.8);
1899 assert!(ctx.uses_transparency());
1900
1901 ctx.set_fill_opacity(0.5);
1903 assert!(ctx.uses_transparency());
1904 }
1905
1906 #[test]
1907 fn test_generate_graphics_state_dict() {
1908 let mut ctx = GraphicsContext::new();
1909
1910 assert_eq!(ctx.generate_graphics_state_dict(), None);
1912
1913 ctx.set_fill_opacity(0.5);
1915 let dict = ctx
1916 .generate_graphics_state_dict()
1917 .expect("Writing to string should never fail");
1918 assert!(dict.contains("/Type /ExtGState"));
1919 assert!(dict.contains("/ca 0.500"));
1920 assert!(!dict.contains("/CA"));
1921
1922 ctx.set_fill_opacity(1.0);
1924 ctx.set_stroke_opacity(0.75);
1925 let dict = ctx
1926 .generate_graphics_state_dict()
1927 .expect("Writing to string should never fail");
1928 assert!(dict.contains("/Type /ExtGState"));
1929 assert!(dict.contains("/CA 0.750"));
1930 assert!(!dict.contains("/ca"));
1931
1932 ctx.set_fill_opacity(0.25);
1934 let dict = ctx
1935 .generate_graphics_state_dict()
1936 .expect("Writing to string should never fail");
1937 assert!(dict.contains("/Type /ExtGState"));
1938 assert!(dict.contains("/ca 0.250"));
1939 assert!(dict.contains("/CA 0.750"));
1940 }
1941
1942 #[test]
1943 fn test_opacity_with_graphics_operations() {
1944 let mut ctx = GraphicsContext::new();
1945
1946 ctx.set_fill_color(Color::red())
1947 .set_opacity(0.5)
1948 .rect(10.0, 10.0, 100.0, 100.0)
1949 .fill();
1950
1951 assert_eq!(ctx.fill_opacity(), 0.5);
1952 assert_eq!(ctx.stroke_opacity(), 0.5);
1953
1954 let ops = ctx.operations();
1955 assert!(ops.contains("10.00 10.00 100.00 100.00 re"));
1956 assert!(ops.contains("1.000 0.000 0.000 rg")); assert!(ops.contains("f")); }
1959
1960 #[test]
1961 fn test_begin_end_text() {
1962 let mut ctx = GraphicsContext::new();
1963 ctx.begin_text();
1964 assert!(ctx.operations().contains("BT\n"));
1965
1966 ctx.end_text();
1967 assert!(ctx.operations().contains("ET\n"));
1968 }
1969
1970 #[test]
1971 fn test_set_font() {
1972 let mut ctx = GraphicsContext::new();
1973 ctx.set_font(Font::Helvetica, 12.0);
1974 assert!(ctx.operations().contains("/Helvetica 12 Tf\n"));
1975
1976 ctx.set_font(Font::TimesBold, 14.5);
1977 assert!(ctx.operations().contains("/Times-Bold 14.5 Tf\n"));
1978 }
1979
1980 #[test]
1981 fn test_set_text_position() {
1982 let mut ctx = GraphicsContext::new();
1983 ctx.set_text_position(100.0, 200.0);
1984 assert!(ctx.operations().contains("100.00 200.00 Td\n"));
1985 }
1986
1987 #[test]
1988 fn test_show_text() {
1989 let mut ctx = GraphicsContext::new();
1990 ctx.show_text("Hello World")
1991 .expect("Writing to string should never fail");
1992 assert!(ctx.operations().contains("(Hello World) Tj\n"));
1993 }
1994
1995 #[test]
1996 fn test_show_text_with_escaping() {
1997 let mut ctx = GraphicsContext::new();
1998 ctx.show_text("Test (parentheses)")
1999 .expect("Writing to string should never fail");
2000 assert!(ctx.operations().contains("(Test \\(parentheses\\)) Tj\n"));
2001
2002 ctx.clear();
2003 ctx.show_text("Back\\slash")
2004 .expect("Writing to string should never fail");
2005 assert!(ctx.operations().contains("(Back\\\\slash) Tj\n"));
2006
2007 ctx.clear();
2008 ctx.show_text("Line\nBreak")
2009 .expect("Writing to string should never fail");
2010 assert!(ctx.operations().contains("(Line\\nBreak) Tj\n"));
2011 }
2012
2013 #[test]
2014 fn test_text_operations_chaining() {
2015 let mut ctx = GraphicsContext::new();
2016 ctx.begin_text()
2017 .set_font(Font::Courier, 10.0)
2018 .set_text_position(50.0, 100.0)
2019 .show_text("Test")
2020 .unwrap()
2021 .end_text();
2022
2023 let ops = ctx.operations();
2024 assert!(ops.contains("BT\n"));
2025 assert!(ops.contains("/Courier 10 Tf\n"));
2026 assert!(ops.contains("50.00 100.00 Td\n"));
2027 assert!(ops.contains("(Test) Tj\n"));
2028 assert!(ops.contains("ET\n"));
2029 }
2030
2031 #[test]
2032 fn test_clip() {
2033 let mut ctx = GraphicsContext::new();
2034 ctx.clip();
2035 assert!(ctx.operations().contains("W\n"));
2036 }
2037
2038 #[test]
2039 fn test_clip_even_odd() {
2040 let mut ctx = GraphicsContext::new();
2041 ctx.clip_even_odd();
2042 assert!(ctx.operations().contains("W*\n"));
2043 }
2044
2045 #[test]
2046 fn test_clipping_with_path() {
2047 let mut ctx = GraphicsContext::new();
2048
2049 ctx.rect(10.0, 10.0, 100.0, 50.0).clip();
2051
2052 let ops = ctx.operations();
2053 assert!(ops.contains("10.00 10.00 100.00 50.00 re\n"));
2054 assert!(ops.contains("W\n"));
2055 }
2056
2057 #[test]
2058 fn test_clipping_even_odd_with_path() {
2059 let mut ctx = GraphicsContext::new();
2060
2061 ctx.move_to(0.0, 0.0)
2063 .line_to(100.0, 0.0)
2064 .line_to(100.0, 100.0)
2065 .line_to(0.0, 100.0)
2066 .close_path()
2067 .clip_even_odd();
2068
2069 let ops = ctx.operations();
2070 assert!(ops.contains("0.00 0.00 m\n"));
2071 assert!(ops.contains("100.00 0.00 l\n"));
2072 assert!(ops.contains("100.00 100.00 l\n"));
2073 assert!(ops.contains("0.00 100.00 l\n"));
2074 assert!(ops.contains("h\n"));
2075 assert!(ops.contains("W*\n"));
2076 }
2077
2078 #[test]
2079 fn test_clipping_chaining() {
2080 let mut ctx = GraphicsContext::new();
2081
2082 ctx.save_state()
2084 .rect(20.0, 20.0, 60.0, 60.0)
2085 .clip()
2086 .set_fill_color(Color::red())
2087 .rect(0.0, 0.0, 100.0, 100.0)
2088 .fill()
2089 .restore_state();
2090
2091 let ops = ctx.operations();
2092 assert!(ops.contains("q\n"));
2093 assert!(ops.contains("20.00 20.00 60.00 60.00 re\n"));
2094 assert!(ops.contains("W\n"));
2095 assert!(ops.contains("1.000 0.000 0.000 rg\n"));
2096 assert!(ops.contains("0.00 0.00 100.00 100.00 re\n"));
2097 assert!(ops.contains("f\n"));
2098 assert!(ops.contains("Q\n"));
2099 }
2100
2101 #[test]
2102 fn test_multiple_clipping_regions() {
2103 let mut ctx = GraphicsContext::new();
2104
2105 ctx.save_state()
2107 .rect(0.0, 0.0, 200.0, 200.0)
2108 .clip()
2109 .save_state()
2110 .circle(100.0, 100.0, 50.0)
2111 .clip_even_odd()
2112 .set_fill_color(Color::blue())
2113 .rect(50.0, 50.0, 100.0, 100.0)
2114 .fill()
2115 .restore_state()
2116 .restore_state();
2117
2118 let ops = ctx.operations();
2119 let q_count = ops.matches("q\n").count();
2121 let q_restore_count = ops.matches("Q\n").count();
2122 assert_eq!(q_count, 2);
2123 assert_eq!(q_restore_count, 2);
2124
2125 assert!(ops.contains("W\n"));
2127 assert!(ops.contains("W*\n"));
2128 }
2129
2130 #[test]
2133 fn test_move_to_and_line_to() {
2134 let mut ctx = GraphicsContext::new();
2135 ctx.move_to(100.0, 200.0).line_to(300.0, 400.0).stroke();
2136
2137 let ops = ctx
2138 .generate_operations()
2139 .expect("Writing to string should never fail");
2140 let ops_str = String::from_utf8_lossy(&ops);
2141 assert!(ops_str.contains("100.00 200.00 m"));
2142 assert!(ops_str.contains("300.00 400.00 l"));
2143 assert!(ops_str.contains("S"));
2144 }
2145
2146 #[test]
2147 fn test_bezier_curve() {
2148 let mut ctx = GraphicsContext::new();
2149 ctx.move_to(0.0, 0.0)
2150 .curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0)
2151 .stroke();
2152
2153 let ops = ctx
2154 .generate_operations()
2155 .expect("Writing to string should never fail");
2156 let ops_str = String::from_utf8_lossy(&ops);
2157 assert!(ops_str.contains("0.00 0.00 m"));
2158 assert!(ops_str.contains("10.00 20.00 30.00 40.00 50.00 60.00 c"));
2159 assert!(ops_str.contains("S"));
2160 }
2161
2162 #[test]
2163 fn test_circle_path() {
2164 let mut ctx = GraphicsContext::new();
2165 ctx.circle(100.0, 100.0, 50.0).fill();
2166
2167 let ops = ctx
2168 .generate_operations()
2169 .expect("Writing to string should never fail");
2170 let ops_str = String::from_utf8_lossy(&ops);
2171 assert!(ops_str.contains(" c"));
2173 assert!(ops_str.contains("f"));
2174 }
2175
2176 #[test]
2177 fn test_path_closing() {
2178 let mut ctx = GraphicsContext::new();
2179 ctx.move_to(0.0, 0.0)
2180 .line_to(100.0, 0.0)
2181 .line_to(100.0, 100.0)
2182 .close_path()
2183 .stroke();
2184
2185 let ops = ctx
2186 .generate_operations()
2187 .expect("Writing to string should never fail");
2188 let ops_str = String::from_utf8_lossy(&ops);
2189 assert!(ops_str.contains("h")); assert!(ops_str.contains("S"));
2191 }
2192
2193 #[test]
2194 fn test_fill_and_stroke() {
2195 let mut ctx = GraphicsContext::new();
2196 ctx.rect(10.0, 10.0, 50.0, 50.0).fill_stroke();
2197
2198 let ops = ctx
2199 .generate_operations()
2200 .expect("Writing to string should never fail");
2201 let ops_str = String::from_utf8_lossy(&ops);
2202 assert!(ops_str.contains("10.00 10.00 50.00 50.00 re"));
2203 assert!(ops_str.contains("B")); }
2205
2206 #[test]
2207 fn test_color_settings() {
2208 let mut ctx = GraphicsContext::new();
2209 ctx.set_fill_color(Color::rgb(1.0, 0.0, 0.0))
2210 .set_stroke_color(Color::rgb(0.0, 1.0, 0.0))
2211 .rect(10.0, 10.0, 50.0, 50.0)
2212 .fill_stroke(); assert_eq!(ctx.fill_color(), Color::rgb(1.0, 0.0, 0.0));
2215 assert_eq!(ctx.stroke_color(), Color::rgb(0.0, 1.0, 0.0));
2216
2217 let ops = ctx
2218 .generate_operations()
2219 .expect("Writing to string should never fail");
2220 let ops_str = String::from_utf8_lossy(&ops);
2221 assert!(ops_str.contains("1.000 0.000 0.000 rg")); assert!(ops_str.contains("0.000 1.000 0.000 RG")); }
2224
2225 #[test]
2226 fn test_line_styles() {
2227 let mut ctx = GraphicsContext::new();
2228 ctx.set_line_width(2.5)
2229 .set_line_cap(LineCap::Round)
2230 .set_line_join(LineJoin::Bevel);
2231
2232 assert_eq!(ctx.line_width(), 2.5);
2233
2234 let ops = ctx
2235 .generate_operations()
2236 .expect("Writing to string should never fail");
2237 let ops_str = String::from_utf8_lossy(&ops);
2238 assert!(ops_str.contains("2.50 w")); assert!(ops_str.contains("1 J")); assert!(ops_str.contains("2 j")); }
2242
2243 #[test]
2244 fn test_opacity_settings() {
2245 let mut ctx = GraphicsContext::new();
2246 ctx.set_opacity(0.5);
2247
2248 assert_eq!(ctx.fill_opacity(), 0.5);
2249 assert_eq!(ctx.stroke_opacity(), 0.5);
2250 assert!(ctx.uses_transparency());
2251
2252 ctx.set_fill_opacity(0.7).set_stroke_opacity(0.3);
2253
2254 assert_eq!(ctx.fill_opacity(), 0.7);
2255 assert_eq!(ctx.stroke_opacity(), 0.3);
2256 }
2257
2258 #[test]
2259 fn test_state_save_restore() {
2260 let mut ctx = GraphicsContext::new();
2261 ctx.save_state()
2262 .set_fill_color(Color::rgb(1.0, 0.0, 0.0))
2263 .restore_state();
2264
2265 let ops = ctx
2266 .generate_operations()
2267 .expect("Writing to string should never fail");
2268 let ops_str = String::from_utf8_lossy(&ops);
2269 assert!(ops_str.contains("q")); assert!(ops_str.contains("Q")); }
2272
2273 #[test]
2274 fn test_transformations() {
2275 let mut ctx = GraphicsContext::new();
2276 ctx.translate(100.0, 200.0).scale(2.0, 3.0).rotate(45.0);
2277
2278 let ops = ctx
2279 .generate_operations()
2280 .expect("Writing to string should never fail");
2281 let ops_str = String::from_utf8_lossy(&ops);
2282 assert!(ops_str.contains("1 0 0 1 100.00 200.00 cm")); assert!(ops_str.contains("2.00 0 0 3.00 0 0 cm")); assert!(ops_str.contains("cm")); }
2286
2287 #[test]
2288 fn test_custom_transform() {
2289 let mut ctx = GraphicsContext::new();
2290 ctx.transform(1.0, 0.5, 0.5, 1.0, 10.0, 20.0);
2291
2292 let ops = ctx
2293 .generate_operations()
2294 .expect("Writing to string should never fail");
2295 let ops_str = String::from_utf8_lossy(&ops);
2296 assert!(ops_str.contains("1.00 0.50 0.50 1.00 10.00 20.00 cm"));
2297 }
2298
2299 #[test]
2300 fn test_rectangle_path() {
2301 let mut ctx = GraphicsContext::new();
2302 ctx.rectangle(25.0, 25.0, 150.0, 100.0).stroke();
2303
2304 let ops = ctx
2305 .generate_operations()
2306 .expect("Writing to string should never fail");
2307 let ops_str = String::from_utf8_lossy(&ops);
2308 assert!(ops_str.contains("25.00 25.00 150.00 100.00 re"));
2309 assert!(ops_str.contains("S"));
2310 }
2311
2312 #[test]
2313 fn test_empty_operations() {
2314 let ctx = GraphicsContext::new();
2315 let ops = ctx
2316 .generate_operations()
2317 .expect("Writing to string should never fail");
2318 assert!(ops.is_empty());
2319 }
2320
2321 #[test]
2322 fn test_complex_path_operations() {
2323 let mut ctx = GraphicsContext::new();
2324 ctx.move_to(50.0, 50.0)
2325 .line_to(100.0, 50.0)
2326 .curve_to(125.0, 50.0, 150.0, 75.0, 150.0, 100.0)
2327 .line_to(150.0, 150.0)
2328 .close_path()
2329 .fill();
2330
2331 let ops = ctx
2332 .generate_operations()
2333 .expect("Writing to string should never fail");
2334 let ops_str = String::from_utf8_lossy(&ops);
2335 assert!(ops_str.contains("50.00 50.00 m"));
2336 assert!(ops_str.contains("100.00 50.00 l"));
2337 assert!(ops_str.contains("125.00 50.00 150.00 75.00 150.00 100.00 c"));
2338 assert!(ops_str.contains("150.00 150.00 l"));
2339 assert!(ops_str.contains("h"));
2340 assert!(ops_str.contains("f"));
2341 }
2342
2343 #[test]
2344 fn test_graphics_state_dict_generation() {
2345 let mut ctx = GraphicsContext::new();
2346
2347 assert!(ctx.generate_graphics_state_dict().is_none());
2349
2350 ctx.set_opacity(0.5);
2352 let dict = ctx.generate_graphics_state_dict();
2353 assert!(dict.is_some());
2354 let dict_str = dict.expect("Writing to string should never fail");
2355 assert!(dict_str.contains("/ca 0.5"));
2356 assert!(dict_str.contains("/CA 0.5"));
2357 }
2358
2359 #[test]
2360 fn test_line_dash_pattern() {
2361 let mut ctx = GraphicsContext::new();
2362 let pattern = LineDashPattern {
2363 array: vec![3.0, 2.0],
2364 phase: 0.0,
2365 };
2366 ctx.set_line_dash_pattern(pattern);
2367
2368 let ops = ctx
2369 .generate_operations()
2370 .expect("Writing to string should never fail");
2371 let ops_str = String::from_utf8_lossy(&ops);
2372 assert!(ops_str.contains("[3.00 2.00] 0.00 d"));
2373 }
2374
2375 #[test]
2376 fn test_miter_limit_setting() {
2377 let mut ctx = GraphicsContext::new();
2378 ctx.set_miter_limit(4.0);
2379
2380 let ops = ctx
2381 .generate_operations()
2382 .expect("Writing to string should never fail");
2383 let ops_str = String::from_utf8_lossy(&ops);
2384 assert!(ops_str.contains("4.00 M"));
2385 }
2386
2387 #[test]
2388 fn test_line_cap_styles() {
2389 let mut ctx = GraphicsContext::new();
2390
2391 ctx.set_line_cap(LineCap::Butt);
2392 let ops = ctx
2393 .generate_operations()
2394 .expect("Writing to string should never fail");
2395 let ops_str = String::from_utf8_lossy(&ops);
2396 assert!(ops_str.contains("0 J"));
2397
2398 let mut ctx = GraphicsContext::new();
2399 ctx.set_line_cap(LineCap::Round);
2400 let ops = ctx
2401 .generate_operations()
2402 .expect("Writing to string should never fail");
2403 let ops_str = String::from_utf8_lossy(&ops);
2404 assert!(ops_str.contains("1 J"));
2405
2406 let mut ctx = GraphicsContext::new();
2407 ctx.set_line_cap(LineCap::Square);
2408 let ops = ctx
2409 .generate_operations()
2410 .expect("Writing to string should never fail");
2411 let ops_str = String::from_utf8_lossy(&ops);
2412 assert!(ops_str.contains("2 J"));
2413 }
2414
2415 #[test]
2416 fn test_transparency_groups() {
2417 let mut ctx = GraphicsContext::new();
2418
2419 let group = TransparencyGroup::new()
2421 .with_isolated(true)
2422 .with_opacity(0.5);
2423
2424 ctx.begin_transparency_group(group);
2425 assert!(ctx.in_transparency_group());
2426
2427 ctx.rect(10.0, 10.0, 100.0, 100.0);
2429 ctx.fill();
2430
2431 ctx.end_transparency_group();
2432 assert!(!ctx.in_transparency_group());
2433
2434 let ops = ctx.operations();
2436 assert!(ops.contains("% Begin Transparency Group"));
2437 assert!(ops.contains("% End Transparency Group"));
2438 }
2439
2440 #[test]
2441 fn test_nested_transparency_groups() {
2442 let mut ctx = GraphicsContext::new();
2443
2444 let group1 = TransparencyGroup::isolated().with_opacity(0.8);
2446 ctx.begin_transparency_group(group1);
2447 assert!(ctx.in_transparency_group());
2448
2449 let group2 = TransparencyGroup::knockout().with_blend_mode(BlendMode::Multiply);
2451 ctx.begin_transparency_group(group2);
2452
2453 ctx.circle(50.0, 50.0, 25.0);
2455 ctx.fill();
2456
2457 ctx.end_transparency_group();
2459 assert!(ctx.in_transparency_group()); ctx.end_transparency_group();
2463 assert!(!ctx.in_transparency_group());
2464 }
2465
2466 #[test]
2467 fn test_line_join_styles() {
2468 let mut ctx = GraphicsContext::new();
2469
2470 ctx.set_line_join(LineJoin::Miter);
2471 let ops = ctx
2472 .generate_operations()
2473 .expect("Writing to string should never fail");
2474 let ops_str = String::from_utf8_lossy(&ops);
2475 assert!(ops_str.contains("0 j"));
2476
2477 let mut ctx = GraphicsContext::new();
2478 ctx.set_line_join(LineJoin::Round);
2479 let ops = ctx
2480 .generate_operations()
2481 .expect("Writing to string should never fail");
2482 let ops_str = String::from_utf8_lossy(&ops);
2483 assert!(ops_str.contains("1 j"));
2484
2485 let mut ctx = GraphicsContext::new();
2486 ctx.set_line_join(LineJoin::Bevel);
2487 let ops = ctx
2488 .generate_operations()
2489 .expect("Writing to string should never fail");
2490 let ops_str = String::from_utf8_lossy(&ops);
2491 assert!(ops_str.contains("2 j"));
2492 }
2493
2494 #[test]
2495 fn test_rendering_intent() {
2496 let mut ctx = GraphicsContext::new();
2497
2498 ctx.set_rendering_intent(RenderingIntent::AbsoluteColorimetric);
2499 assert_eq!(
2500 ctx.rendering_intent(),
2501 RenderingIntent::AbsoluteColorimetric
2502 );
2503
2504 ctx.set_rendering_intent(RenderingIntent::Perceptual);
2505 assert_eq!(ctx.rendering_intent(), RenderingIntent::Perceptual);
2506
2507 ctx.set_rendering_intent(RenderingIntent::Saturation);
2508 assert_eq!(ctx.rendering_intent(), RenderingIntent::Saturation);
2509 }
2510
2511 #[test]
2512 fn test_flatness_tolerance() {
2513 let mut ctx = GraphicsContext::new();
2514
2515 ctx.set_flatness(0.5);
2516 assert_eq!(ctx.flatness(), 0.5);
2517
2518 let ops = ctx
2519 .generate_operations()
2520 .expect("Writing to string should never fail");
2521 let ops_str = String::from_utf8_lossy(&ops);
2522 assert!(ops_str.contains("0.50 i"));
2523 }
2524
2525 #[test]
2526 fn test_smoothness_tolerance() {
2527 let mut ctx = GraphicsContext::new();
2528
2529 let _ = ctx.set_smoothness(0.1);
2530 assert_eq!(ctx.smoothness(), 0.1);
2531 }
2532
2533 #[test]
2534 fn test_bezier_curves() {
2535 let mut ctx = GraphicsContext::new();
2536
2537 ctx.move_to(10.0, 10.0);
2539 ctx.curve_to(20.0, 10.0, 30.0, 20.0, 30.0, 30.0);
2540
2541 let ops = ctx
2542 .generate_operations()
2543 .expect("Writing to string should never fail");
2544 let ops_str = String::from_utf8_lossy(&ops);
2545 assert!(ops_str.contains("10.00 10.00 m"));
2546 assert!(ops_str.contains("c")); }
2548
2549 #[test]
2550 fn test_clipping_path() {
2551 let mut ctx = GraphicsContext::new();
2552
2553 ctx.rectangle(10.0, 10.0, 100.0, 100.0);
2554 ctx.clip();
2555
2556 let ops = ctx
2557 .generate_operations()
2558 .expect("Writing to string should never fail");
2559 let ops_str = String::from_utf8_lossy(&ops);
2560 assert!(ops_str.contains("W"));
2561 }
2562
2563 #[test]
2564 fn test_even_odd_clipping() {
2565 let mut ctx = GraphicsContext::new();
2566
2567 ctx.rectangle(10.0, 10.0, 100.0, 100.0);
2568 ctx.clip_even_odd();
2569
2570 let ops = ctx
2571 .generate_operations()
2572 .expect("Writing to string should never fail");
2573 let ops_str = String::from_utf8_lossy(&ops);
2574 assert!(ops_str.contains("W*"));
2575 }
2576
2577 #[test]
2578 fn test_color_creation() {
2579 let gray = Color::gray(0.5);
2581 assert_eq!(gray, Color::Gray(0.5));
2582
2583 let rgb = Color::rgb(0.2, 0.4, 0.6);
2584 assert_eq!(rgb, Color::Rgb(0.2, 0.4, 0.6));
2585
2586 let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
2587 assert_eq!(cmyk, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
2588
2589 assert_eq!(Color::black(), Color::Gray(0.0));
2591 assert_eq!(Color::white(), Color::Gray(1.0));
2592 assert_eq!(Color::red(), Color::Rgb(1.0, 0.0, 0.0));
2593 }
2594
2595 #[test]
2596 fn test_extended_graphics_state() {
2597 let ctx = GraphicsContext::new();
2598
2599 let _extgstate = ExtGState::new();
2601
2602 assert!(ctx.generate_operations().is_ok());
2604 }
2605
2606 #[test]
2607 fn test_path_construction_methods() {
2608 let mut ctx = GraphicsContext::new();
2609
2610 ctx.move_to(10.0, 10.0);
2612 ctx.line_to(20.0, 20.0);
2613 ctx.curve_to(30.0, 30.0, 40.0, 40.0, 50.0, 50.0);
2614 ctx.rect(60.0, 60.0, 30.0, 30.0);
2615 ctx.circle(100.0, 100.0, 25.0);
2616 ctx.close_path();
2617
2618 let ops = ctx
2619 .generate_operations()
2620 .expect("Writing to string should never fail");
2621 assert!(!ops.is_empty());
2622 }
2623
2624 #[test]
2625 fn test_graphics_context_clone_advanced() {
2626 let mut ctx = GraphicsContext::new();
2627 ctx.set_fill_color(Color::rgb(1.0, 0.0, 0.0));
2628 ctx.set_line_width(5.0);
2629
2630 let cloned = ctx.clone();
2631 assert_eq!(cloned.fill_color(), Color::rgb(1.0, 0.0, 0.0));
2632 assert_eq!(cloned.line_width(), 5.0);
2633 }
2634
2635 #[test]
2636 fn test_basic_drawing_operations() {
2637 let mut ctx = GraphicsContext::new();
2638
2639 ctx.move_to(50.0, 50.0);
2641 ctx.line_to(100.0, 100.0);
2642 ctx.stroke();
2643
2644 let ops = ctx
2645 .generate_operations()
2646 .expect("Writing to string should never fail");
2647 let ops_str = String::from_utf8_lossy(&ops);
2648 assert!(ops_str.contains("m")); assert!(ops_str.contains("l")); assert!(ops_str.contains("S")); }
2652
2653 #[test]
2654 fn test_graphics_state_stack() {
2655 let mut ctx = GraphicsContext::new();
2656
2657 ctx.set_fill_color(Color::black());
2659
2660 ctx.save_state();
2662 ctx.set_fill_color(Color::red());
2663 assert_eq!(ctx.fill_color(), Color::red());
2664
2665 ctx.save_state();
2667 ctx.set_fill_color(Color::blue());
2668 assert_eq!(ctx.fill_color(), Color::blue());
2669
2670 ctx.restore_state();
2672 assert_eq!(ctx.fill_color(), Color::red());
2673
2674 ctx.restore_state();
2676 assert_eq!(ctx.fill_color(), Color::black());
2677 }
2678
2679 #[test]
2680 fn test_word_spacing() {
2681 let mut ctx = GraphicsContext::new();
2682 ctx.set_word_spacing(2.5);
2683
2684 let ops = ctx.generate_operations().unwrap();
2685 let ops_str = String::from_utf8_lossy(&ops);
2686 assert!(ops_str.contains("2.50 Tw"));
2687 }
2688
2689 #[test]
2690 fn test_character_spacing() {
2691 let mut ctx = GraphicsContext::new();
2692 ctx.set_character_spacing(1.0);
2693
2694 let ops = ctx.generate_operations().unwrap();
2695 let ops_str = String::from_utf8_lossy(&ops);
2696 assert!(ops_str.contains("1.00 Tc"));
2697 }
2698
2699 #[test]
2700 fn test_justified_text() {
2701 let mut ctx = GraphicsContext::new();
2702 ctx.begin_text();
2703 ctx.set_text_position(100.0, 200.0);
2704 ctx.show_justified_text("Hello world from PDF", 200.0)
2705 .unwrap();
2706 ctx.end_text();
2707
2708 let ops = ctx.generate_operations().unwrap();
2709 let ops_str = String::from_utf8_lossy(&ops);
2710
2711 assert!(ops_str.contains("BT")); assert!(ops_str.contains("ET")); assert!(ops_str.contains("100.00 200.00 Td")); assert!(ops_str.contains("(Hello world from PDF) Tj")); assert!(ops_str.contains("Tw")); }
2720
2721 #[test]
2722 fn test_justified_text_single_word() {
2723 let mut ctx = GraphicsContext::new();
2724 ctx.begin_text();
2725 ctx.show_justified_text("Hello", 200.0).unwrap();
2726 ctx.end_text();
2727
2728 let ops = ctx.generate_operations().unwrap();
2729 let ops_str = String::from_utf8_lossy(&ops);
2730
2731 assert!(ops_str.contains("(Hello) Tj"));
2733 assert_eq!(ops_str.matches("Tw").count(), 0);
2735 }
2736
2737 #[test]
2738 fn test_text_width_estimation() {
2739 let ctx = GraphicsContext::new();
2740 let width = ctx.estimate_text_width_simple("Hello");
2741
2742 assert!(width > 0.0);
2744 assert_eq!(width, 5.0 * 12.0 * 0.6); }
2746
2747 #[test]
2748 fn test_set_alpha_methods() {
2749 let mut ctx = GraphicsContext::new();
2750
2751 assert!(ctx.set_alpha(0.5).is_ok());
2753 assert!(ctx.set_alpha_fill(0.3).is_ok());
2754 assert!(ctx.set_alpha_stroke(0.7).is_ok());
2755
2756 assert!(ctx.set_alpha(1.5).is_ok()); assert!(ctx.set_alpha(-0.2).is_ok()); assert!(ctx.set_alpha_fill(2.0).is_ok()); assert!(ctx.set_alpha_stroke(-1.0).is_ok()); let result = ctx
2764 .set_alpha(0.5)
2765 .and_then(|c| c.set_alpha_fill(0.3))
2766 .and_then(|c| c.set_alpha_stroke(0.7));
2767 assert!(result.is_ok());
2768 }
2769
2770 #[test]
2771 fn test_alpha_methods_generate_extgstate() {
2772 let mut ctx = GraphicsContext::new();
2773
2774 ctx.set_alpha(0.5).unwrap();
2776
2777 ctx.rect(10.0, 10.0, 50.0, 50.0).fill();
2779
2780 let ops = ctx.generate_operations().unwrap();
2781 let ops_str = String::from_utf8_lossy(&ops);
2782
2783 assert!(ops_str.contains("/GS")); assert!(ops_str.contains(" gs\n")); ctx.clear();
2789 ctx.set_alpha_fill(0.3).unwrap();
2790 ctx.set_alpha_stroke(0.8).unwrap();
2791 ctx.rect(20.0, 20.0, 60.0, 60.0).fill_stroke();
2792
2793 let ops2 = ctx.generate_operations().unwrap();
2794 let ops_str2 = String::from_utf8_lossy(&ops2);
2795
2796 assert!(ops_str2.contains("/GS")); assert!(ops_str2.contains(" gs\n")); }
2800
2801 #[test]
2802 fn test_add_command() {
2803 let mut ctx = GraphicsContext::new();
2804
2805 ctx.add_command("1 0 0 1 100 200 cm");
2807 let ops = ctx.operations();
2808 assert!(ops.contains("1 0 0 1 100 200 cm\n"));
2809
2810 ctx.clear();
2812 ctx.add_command("q");
2813 assert_eq!(ctx.operations(), "q\n");
2814
2815 ctx.clear();
2817 ctx.add_command("");
2818 assert_eq!(ctx.operations(), "\n");
2819
2820 ctx.clear();
2822 ctx.add_command("Q\n");
2823 assert_eq!(ctx.operations(), "Q\n\n"); ctx.clear();
2827 ctx.add_command("q");
2828 ctx.add_command("1 0 0 1 50 50 cm");
2829 ctx.add_command("Q");
2830 assert_eq!(ctx.operations(), "q\n1 0 0 1 50 50 cm\nQ\n");
2831 }
2832
2833 #[test]
2834 fn test_get_operations() {
2835 let mut ctx = GraphicsContext::new();
2836 ctx.rect(10.0, 10.0, 50.0, 50.0);
2837 let ops1 = ctx.operations();
2838 let ops2 = ctx.get_operations();
2839 assert_eq!(ops1, ops2);
2840 }
2841
2842 #[test]
2843 fn test_set_line_solid() {
2844 let mut ctx = GraphicsContext::new();
2845 ctx.set_line_dash_pattern(LineDashPattern::new(vec![5.0, 3.0], 0.0));
2846 ctx.set_line_solid();
2847 let ops = ctx.operations();
2848 assert!(ops.contains("[] 0 d\n"));
2849 }
2850
2851 #[test]
2852 fn test_set_custom_font() {
2853 let mut ctx = GraphicsContext::new();
2854 ctx.set_custom_font("CustomFont", 14.0);
2855 assert_eq!(ctx.current_font_name.as_deref(), Some("CustomFont"));
2856 assert_eq!(ctx.current_font_size, 14.0);
2857 assert!(ctx.is_custom_font);
2858 }
2859
2860 #[test]
2861 fn test_show_text_standard_font_uses_literal_string() {
2862 let mut ctx = GraphicsContext::new();
2863 ctx.set_font(Font::Helvetica, 12.0);
2864 assert!(!ctx.is_custom_font);
2865
2866 ctx.begin_text();
2867 ctx.set_text_position(10.0, 20.0);
2868 ctx.show_text("Hello World").unwrap();
2869 ctx.end_text();
2870
2871 let ops = ctx.operations();
2872 assert!(ops.contains("(Hello World) Tj"));
2873 assert!(!ops.contains("<"));
2874 }
2875
2876 #[test]
2877 fn test_show_text_custom_font_uses_hex_encoding() {
2878 let mut ctx = GraphicsContext::new();
2879 ctx.set_font(Font::Custom("NotoSansCJK".to_string()), 12.0);
2880 assert!(ctx.is_custom_font);
2881
2882 ctx.begin_text();
2883 ctx.set_text_position(10.0, 20.0);
2884 ctx.show_text("你好").unwrap();
2886 ctx.end_text();
2887
2888 let ops = ctx.operations();
2889 assert!(
2891 ops.contains("<4F60597D> Tj"),
2892 "Expected hex encoding for CJK text, got: {}",
2893 ops
2894 );
2895 assert!(!ops.contains("(你好)"));
2896 }
2897
2898 #[test]
2899 fn test_show_text_custom_font_ascii_still_hex() {
2900 let mut ctx = GraphicsContext::new();
2901 ctx.set_font(Font::Custom("MyFont".to_string()), 10.0);
2902
2903 ctx.begin_text();
2904 ctx.set_text_position(0.0, 0.0);
2905 ctx.show_text("AB").unwrap();
2907 ctx.end_text();
2908
2909 let ops = ctx.operations();
2910 assert!(
2912 ops.contains("<00410042> Tj"),
2913 "Expected hex encoding for ASCII in custom font, got: {}",
2914 ops
2915 );
2916 }
2917
2918 #[test]
2919 fn test_show_text_tracks_used_characters() {
2920 let mut ctx = GraphicsContext::new();
2921 ctx.set_font(Font::Custom("CJKFont".to_string()), 12.0);
2922
2923 ctx.begin_text();
2924 ctx.show_text("你好A").unwrap();
2925 ctx.end_text();
2926
2927 let chars = ctx
2928 .get_used_characters()
2929 .expect("show_text with a custom font must record characters");
2930 assert!(chars.contains(&'你'));
2931 assert!(chars.contains(&'好'));
2932 assert!(chars.contains(&'A'));
2933 }
2934
2935 #[test]
2936 fn test_is_custom_font_toggles_correctly() {
2937 let mut ctx = GraphicsContext::new();
2938 assert!(!ctx.is_custom_font);
2939
2940 ctx.set_font(Font::Custom("CJK".to_string()), 12.0);
2941 assert!(ctx.is_custom_font);
2942
2943 ctx.set_font(Font::Helvetica, 12.0);
2944 assert!(!ctx.is_custom_font);
2945
2946 ctx.set_custom_font("AnotherCJK", 14.0);
2947 assert!(ctx.is_custom_font);
2948
2949 ctx.set_font(Font::CourierBold, 10.0);
2950 assert!(!ctx.is_custom_font);
2951 }
2952
2953 #[test]
2954 fn test_set_glyph_mapping() {
2955 let mut ctx = GraphicsContext::new();
2956
2957 assert!(ctx.glyph_mapping.is_none());
2959
2960 let mut mapping = HashMap::new();
2962 mapping.insert(65u32, 1u16); mapping.insert(66u32, 2u16); ctx.set_glyph_mapping(mapping.clone());
2965 assert!(ctx.glyph_mapping.is_some());
2966 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().len(), 2);
2967 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().get(&65), Some(&1));
2968 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().get(&66), Some(&2));
2969
2970 ctx.set_glyph_mapping(HashMap::new());
2972 assert!(ctx.glyph_mapping.is_some());
2973 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().len(), 0);
2974
2975 let mut new_mapping = HashMap::new();
2977 new_mapping.insert(67u32, 3u16); ctx.set_glyph_mapping(new_mapping);
2979 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().len(), 1);
2980 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().get(&67), Some(&3));
2981 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().get(&65), None); }
2983
2984 #[test]
2985 fn test_draw_text_basic() {
2986 let mut ctx = GraphicsContext::new();
2987 ctx.set_font(Font::Helvetica, 12.0);
2988
2989 let result = ctx.draw_text("Hello", 100.0, 200.0);
2990 assert!(result.is_ok());
2991
2992 let ops = ctx.operations();
2993 assert!(ops.contains("BT\n"));
2995 assert!(ops.contains("ET\n"));
2996
2997 assert!(ops.contains("/Helvetica"));
2999 assert!(ops.contains("12"));
3000 assert!(ops.contains("Tf\n"));
3001
3002 assert!(ops.contains("100"));
3004 assert!(ops.contains("200"));
3005 assert!(ops.contains("Td\n"));
3006
3007 assert!(ops.contains("(Hello)") || ops.contains("<48656c6c6f>")); }
3010
3011 #[test]
3012 fn test_draw_text_with_special_characters() {
3013 let mut ctx = GraphicsContext::new();
3014 ctx.set_font(Font::Helvetica, 12.0);
3015
3016 let result = ctx.draw_text("Test (with) parens", 50.0, 100.0);
3018 assert!(result.is_ok());
3019
3020 let ops = ctx.operations();
3021 assert!(ops.contains("\\(") || ops.contains("\\)") || ops.contains("<"));
3023 }
3025
3026 #[test]
3027 fn test_draw_text_unicode_detection() {
3028 let mut ctx = GraphicsContext::new();
3029 ctx.set_font(Font::Helvetica, 12.0);
3030
3031 ctx.draw_text("ASCII", 0.0, 0.0).unwrap();
3033 let _ops_ascii = ctx.operations();
3034
3035 ctx.clear();
3036
3037 ctx.set_font(Font::Helvetica, 12.0);
3039 ctx.draw_text("中文", 0.0, 0.0).unwrap();
3040 let ops_unicode = ctx.operations();
3041
3042 assert!(ops_unicode.contains("<") && ops_unicode.contains(">"));
3044 }
3045
3046 #[test]
3047 #[allow(deprecated)]
3048 fn test_draw_text_hex_encoding() {
3049 let mut ctx = GraphicsContext::new();
3050 ctx.set_font(Font::Helvetica, 12.0);
3051 let result = ctx.draw_text_hex("Test", 50.0, 100.0);
3052 assert!(result.is_ok());
3053 let ops = ctx.operations();
3054 assert!(ops.contains("<"));
3055 assert!(ops.contains(">"));
3056 }
3057
3058 #[test]
3059 #[allow(deprecated)]
3060 fn test_draw_text_cid() {
3061 let mut ctx = GraphicsContext::new();
3062 ctx.set_custom_font("CustomCIDFont", 12.0);
3063 let result = ctx.draw_text_cid("Test", 50.0, 100.0);
3064 assert!(result.is_ok());
3065 let ops = ctx.operations();
3066 assert!(ops.contains("BT\n"));
3067 assert!(ops.contains("ET\n"));
3068 }
3069
3070 #[test]
3071 #[allow(deprecated)]
3072 fn test_draw_text_unicode() {
3073 let mut ctx = GraphicsContext::new();
3074 ctx.set_custom_font("UnicodeFont", 12.0);
3075 let result = ctx.draw_text_unicode("Test \u{4E2D}\u{6587}", 50.0, 100.0);
3076 assert!(result.is_ok());
3077 let ops = ctx.operations();
3078 assert!(ops.contains("BT\n"));
3079 assert!(ops.contains("ET\n"));
3080 }
3081
3082 #[test]
3083 fn test_begin_end_transparency_group() {
3084 let mut ctx = GraphicsContext::new();
3085
3086 assert!(!ctx.in_transparency_group());
3088 assert!(ctx.current_transparency_group().is_none());
3089
3090 let group = TransparencyGroup::new();
3092 ctx.begin_transparency_group(group);
3093 assert!(ctx.in_transparency_group());
3094 assert!(ctx.current_transparency_group().is_some());
3095
3096 let ops = ctx.operations();
3098 assert!(ops.contains("% Begin Transparency Group"));
3099
3100 ctx.end_transparency_group();
3102 assert!(!ctx.in_transparency_group());
3103 assert!(ctx.current_transparency_group().is_none());
3104
3105 let ops_after = ctx.operations();
3107 assert!(ops_after.contains("% End Transparency Group"));
3108 }
3109
3110 #[test]
3111 fn test_transparency_group_nesting() {
3112 let mut ctx = GraphicsContext::new();
3113
3114 let group1 = TransparencyGroup::new();
3116 let group2 = TransparencyGroup::new();
3117 let group3 = TransparencyGroup::new();
3118
3119 ctx.begin_transparency_group(group1);
3120 assert_eq!(ctx.transparency_stack.len(), 1);
3121
3122 ctx.begin_transparency_group(group2);
3123 assert_eq!(ctx.transparency_stack.len(), 2);
3124
3125 ctx.begin_transparency_group(group3);
3126 assert_eq!(ctx.transparency_stack.len(), 3);
3127
3128 ctx.end_transparency_group();
3130 assert_eq!(ctx.transparency_stack.len(), 2);
3131
3132 ctx.end_transparency_group();
3133 assert_eq!(ctx.transparency_stack.len(), 1);
3134
3135 ctx.end_transparency_group();
3136 assert_eq!(ctx.transparency_stack.len(), 0);
3137 assert!(!ctx.in_transparency_group());
3138 }
3139
3140 #[test]
3141 fn test_transparency_group_without_begin() {
3142 let mut ctx = GraphicsContext::new();
3143
3144 assert!(!ctx.in_transparency_group());
3146 ctx.end_transparency_group();
3147 assert!(!ctx.in_transparency_group());
3148 }
3149
3150 #[test]
3151 fn test_extgstate_manager_access() {
3152 let ctx = GraphicsContext::new();
3153 let manager = ctx.extgstate_manager();
3154 assert_eq!(manager.count(), 0);
3155 }
3156
3157 #[test]
3158 fn test_extgstate_manager_mut_access() {
3159 let mut ctx = GraphicsContext::new();
3160 let manager = ctx.extgstate_manager_mut();
3161 assert_eq!(manager.count(), 0);
3162 }
3163
3164 #[test]
3165 fn test_has_extgstates() {
3166 let mut ctx = GraphicsContext::new();
3167
3168 assert!(!ctx.has_extgstates());
3170 assert_eq!(ctx.extgstate_manager().count(), 0);
3171
3172 ctx.set_alpha(0.5).unwrap();
3174 ctx.rect(10.0, 10.0, 50.0, 50.0).fill();
3175 let result = ctx.generate_operations().unwrap();
3176
3177 assert!(ctx.has_extgstates());
3178 assert!(ctx.extgstate_manager().count() > 0);
3179
3180 let output = String::from_utf8_lossy(&result);
3182 assert!(output.contains("/GS")); assert!(output.contains(" gs\n")); }
3185
3186 #[test]
3187 fn test_generate_extgstate_resources() {
3188 let mut ctx = GraphicsContext::new();
3189 ctx.set_alpha(0.5).unwrap();
3190 ctx.rect(10.0, 10.0, 50.0, 50.0).fill();
3191 ctx.generate_operations().unwrap();
3192
3193 let resources = ctx.generate_extgstate_resources();
3194 assert!(resources.is_ok());
3195 }
3196
3197 #[test]
3198 fn test_apply_extgstate() {
3199 let mut ctx = GraphicsContext::new();
3200
3201 let mut state = ExtGState::new();
3203 state.alpha_fill = Some(0.5);
3204 state.alpha_stroke = Some(0.8);
3205 state.blend_mode = Some(BlendMode::Multiply);
3206
3207 let result = ctx.apply_extgstate(state);
3208 assert!(result.is_ok());
3209
3210 assert!(ctx.has_extgstates());
3212 assert_eq!(ctx.extgstate_manager().count(), 1);
3213
3214 let mut state2 = ExtGState::new();
3216 state2.alpha_fill = Some(0.3);
3217 ctx.apply_extgstate(state2).unwrap();
3218
3219 assert_eq!(ctx.extgstate_manager().count(), 2);
3221 }
3222
3223 #[test]
3224 fn test_with_extgstate() {
3225 let mut ctx = GraphicsContext::new();
3226 let result = ctx.with_extgstate(|mut state| {
3227 state.alpha_fill = Some(0.5);
3228 state.alpha_stroke = Some(0.8);
3229 state
3230 });
3231 assert!(result.is_ok());
3232 }
3233
3234 #[test]
3235 fn test_set_blend_mode() {
3236 let mut ctx = GraphicsContext::new();
3237
3238 let result = ctx.set_blend_mode(BlendMode::Multiply);
3240 assert!(result.is_ok());
3241 assert!(ctx.has_extgstates());
3242
3243 ctx.clear();
3245 ctx.set_blend_mode(BlendMode::Screen).unwrap();
3246 ctx.rect(0.0, 0.0, 10.0, 10.0).fill();
3247 let ops = ctx.generate_operations().unwrap();
3248 let output = String::from_utf8_lossy(&ops);
3249
3250 assert!(output.contains("/GS"));
3252 assert!(output.contains(" gs\n"));
3253 }
3254
3255 #[test]
3256 fn test_render_table() {
3257 let mut ctx = GraphicsContext::new();
3258 let table = Table::with_equal_columns(2, 200.0);
3259 let result = ctx.render_table(&table);
3260 assert!(result.is_ok());
3261 }
3262
3263 #[test]
3264 fn test_render_list() {
3265 let mut ctx = GraphicsContext::new();
3266 use crate::text::{OrderedList, OrderedListStyle};
3267 let ordered = OrderedList::new(OrderedListStyle::Decimal);
3268 let list = ListElement::Ordered(ordered);
3269 let result = ctx.render_list(&list);
3270 assert!(result.is_ok());
3271 }
3272
3273 #[test]
3274 fn test_render_column_layout() {
3275 let mut ctx = GraphicsContext::new();
3276 use crate::text::ColumnContent;
3277 let layout = ColumnLayout::new(2, 100.0, 200.0);
3278 let content = ColumnContent::new("Test content");
3279 let result = ctx.render_column_layout(&layout, &content, 50.0, 50.0, 400.0);
3280 assert!(result.is_ok());
3281 }
3282
3283 #[test]
3284 fn test_clip_ellipse() {
3285 let mut ctx = GraphicsContext::new();
3286
3287 assert!(!ctx.has_clipping());
3289 assert!(ctx.clipping_path().is_none());
3290
3291 let result = ctx.clip_ellipse(100.0, 100.0, 50.0, 30.0);
3293 assert!(result.is_ok());
3294 assert!(ctx.has_clipping());
3295 assert!(ctx.clipping_path().is_some());
3296
3297 let ops = ctx.operations();
3299 assert!(ops.contains("W\n") || ops.contains("W*\n")); ctx.clear_clipping();
3303 assert!(!ctx.has_clipping());
3304 }
3305
3306 #[test]
3307 fn test_clipping_path_access() {
3308 let mut ctx = GraphicsContext::new();
3309
3310 assert!(ctx.clipping_path().is_none());
3312
3313 ctx.clip_rect(10.0, 10.0, 50.0, 50.0).unwrap();
3315 assert!(ctx.clipping_path().is_some());
3316
3317 ctx.clip_circle(100.0, 100.0, 25.0).unwrap();
3319 assert!(ctx.clipping_path().is_some());
3320
3321 ctx.save_state();
3323 ctx.clear_clipping();
3324 assert!(!ctx.has_clipping());
3325
3326 ctx.restore_state();
3327 assert!(ctx.has_clipping());
3329 }
3330
3331 #[test]
3334 fn test_edge_case_move_to_negative() {
3335 let mut ctx = GraphicsContext::new();
3336 ctx.move_to(-100.5, -200.25);
3337 assert!(ctx.operations().contains("-100.50 -200.25 m\n"));
3338 }
3339
3340 #[test]
3341 fn test_edge_case_opacity_out_of_range() {
3342 let mut ctx = GraphicsContext::new();
3343
3344 let _ = ctx.set_opacity(2.5);
3346 assert_eq!(ctx.fill_opacity(), 1.0);
3347
3348 let _ = ctx.set_opacity(-0.5);
3350 assert_eq!(ctx.fill_opacity(), 0.0);
3351 }
3352
3353 #[test]
3354 fn test_edge_case_line_width_extremes() {
3355 let mut ctx = GraphicsContext::new();
3356
3357 ctx.set_line_width(0.0);
3358 assert_eq!(ctx.line_width(), 0.0);
3359
3360 ctx.set_line_width(10000.0);
3361 assert_eq!(ctx.line_width(), 10000.0);
3362 }
3363
3364 #[test]
3367 fn test_interaction_transparency_plus_clipping() {
3368 let mut ctx = GraphicsContext::new();
3369
3370 ctx.set_alpha(0.5).unwrap();
3371 ctx.clip_rect(10.0, 10.0, 100.0, 100.0).unwrap();
3372 ctx.rect(20.0, 20.0, 80.0, 80.0).fill();
3373
3374 let ops = ctx.generate_operations().unwrap();
3375 let output = String::from_utf8_lossy(&ops);
3376
3377 assert!(output.contains("W\n") || output.contains("W*\n"));
3379 assert!(output.contains("/GS"));
3380 }
3381
3382 #[test]
3383 fn test_interaction_extgstate_plus_text() {
3384 let mut ctx = GraphicsContext::new();
3385
3386 let mut state = ExtGState::new();
3387 state.alpha_fill = Some(0.7);
3388 ctx.apply_extgstate(state).unwrap();
3389
3390 ctx.set_font(Font::Helvetica, 14.0);
3391 ctx.draw_text("Test", 100.0, 200.0).unwrap();
3392
3393 let ops = ctx.generate_operations().unwrap();
3394 let output = String::from_utf8_lossy(&ops);
3395
3396 assert!(output.contains("/GS"));
3397 assert!(output.contains("BT\n"));
3398 }
3399
3400 #[test]
3401 fn test_interaction_chained_transformations() {
3402 let mut ctx = GraphicsContext::new();
3403
3404 ctx.translate(50.0, 100.0);
3405 ctx.rotate(45.0);
3406 ctx.scale(2.0, 2.0);
3407
3408 let ops = ctx.operations();
3409 assert_eq!(ops.matches("cm\n").count(), 3);
3410 }
3411
3412 #[test]
3415 fn test_e2e_complete_page_with_header() {
3416 use crate::{Document, Page};
3417
3418 let mut doc = Document::new();
3419 let mut page = Page::a4();
3420 let ctx = page.graphics();
3421
3422 ctx.save_state();
3424 let _ = ctx.set_fill_opacity(0.3);
3425 ctx.set_fill_color(Color::rgb(200.0, 200.0, 255.0));
3426 ctx.rect(0.0, 750.0, 595.0, 42.0).fill();
3427 ctx.restore_state();
3428
3429 ctx.save_state();
3431 ctx.clip_rect(50.0, 50.0, 495.0, 692.0).unwrap();
3432 ctx.rect(60.0, 60.0, 100.0, 100.0).fill();
3433 ctx.restore_state();
3434
3435 let ops = ctx.generate_operations().unwrap();
3436 let output = String::from_utf8_lossy(&ops);
3437
3438 assert!(output.contains("q\n"));
3439 assert!(output.contains("Q\n"));
3440 assert!(output.contains("f\n"));
3441
3442 doc.add_page(page);
3443 assert!(doc.to_bytes().unwrap().len() > 0);
3444 }
3445
3446 #[test]
3447 fn test_e2e_watermark_workflow() {
3448 let mut ctx = GraphicsContext::new();
3449
3450 ctx.save_state();
3451 let _ = ctx.set_fill_opacity(0.2);
3452 ctx.translate(300.0, 400.0);
3453 ctx.rotate(45.0);
3454 ctx.set_font(Font::HelveticaBold, 72.0);
3455 ctx.draw_text("DRAFT", 0.0, 0.0).unwrap();
3456 ctx.restore_state();
3457
3458 let ops = ctx.generate_operations().unwrap();
3459 let output = String::from_utf8_lossy(&ops);
3460
3461 assert!(output.contains("q\n")); assert!(output.contains("Q\n")); assert!(output.contains("cm\n")); assert!(output.contains("BT\n")); assert!(output.contains("ET\n")); }
3468
3469 #[test]
3472 fn test_set_custom_font_emits_tf_operator() {
3473 let mut ctx = GraphicsContext::new();
3474 ctx.set_custom_font("NotoSansCJK", 14.0);
3475
3476 let ops = ctx.operations();
3477 assert!(
3478 ops.contains("/NotoSansCJK 14 Tf"),
3479 "set_custom_font should emit Tf operator, got: {}",
3480 ops
3481 );
3482 }
3483
3484 #[test]
3487 fn test_draw_text_uses_is_custom_font_flag() {
3488 let mut ctx = GraphicsContext::new();
3489 ctx.set_custom_font("Helvetica", 12.0);
3491 ctx.clear(); ctx.draw_text("A", 10.0, 20.0).unwrap();
3494 let ops = ctx.operations();
3495 assert!(
3497 ops.contains("<0041> Tj"),
3498 "draw_text with is_custom_font=true should use hex, got: {}",
3499 ops
3500 );
3501 }
3502
3503 #[test]
3504 fn test_draw_text_standard_font_uses_literal() {
3505 let mut ctx = GraphicsContext::new();
3506 ctx.set_font(Font::Helvetica, 12.0);
3507 ctx.clear();
3508
3509 ctx.draw_text("Hello", 10.0, 20.0).unwrap();
3510 let ops = ctx.operations();
3511 assert!(
3512 ops.contains("(Hello) Tj"),
3513 "draw_text with standard font should use literal, got: {}",
3514 ops
3515 );
3516 }
3517
3518 #[test]
3521 fn test_show_text_smp_character_uses_surrogate_pairs() {
3522 let mut ctx = GraphicsContext::new();
3523 ctx.set_font(Font::Custom("Emoji".to_string()), 12.0);
3524
3525 ctx.begin_text();
3526 ctx.set_text_position(0.0, 0.0);
3527 ctx.show_text("\u{1F600}").unwrap();
3529 ctx.end_text();
3530
3531 let ops = ctx.operations();
3532 assert!(
3533 ops.contains("<D83DDE00> Tj"),
3534 "SMP character should use UTF-16BE surrogate pair, got: {}",
3535 ops
3536 );
3537 assert!(
3538 !ops.contains("FFFD"),
3539 "SMP character must NOT be replaced with FFFD"
3540 );
3541 }
3542
3543 #[test]
3546 fn test_save_restore_preserves_font_state() {
3547 let mut ctx = GraphicsContext::new();
3548 ctx.set_font(Font::Custom("CJK".to_string()), 12.0);
3549 assert!(ctx.is_custom_font);
3550 assert_eq!(ctx.current_font_name.as_deref(), Some("CJK"));
3551 assert_eq!(ctx.current_font_size, 12.0);
3552
3553 ctx.save_state();
3554 ctx.set_font(Font::Helvetica, 10.0);
3555 assert!(!ctx.is_custom_font);
3556 assert_eq!(ctx.current_font_name.as_deref(), Some("Helvetica"));
3557
3558 ctx.restore_state();
3559 assert!(
3560 ctx.is_custom_font,
3561 "is_custom_font must be restored after restore_state"
3562 );
3563 assert_eq!(ctx.current_font_name.as_deref(), Some("CJK"));
3564 assert_eq!(ctx.current_font_size, 12.0);
3565 }
3566
3567 #[test]
3568 fn test_save_restore_mixed_font_encoding() {
3569 let mut ctx = GraphicsContext::new();
3570 ctx.set_font(Font::Custom("CJK".to_string()), 12.0);
3571
3572 ctx.save_state();
3574 ctx.set_font(Font::Helvetica, 10.0);
3575 ctx.begin_text();
3576 ctx.show_text("Hello").unwrap();
3577 ctx.end_text();
3578 ctx.restore_state();
3579
3580 ctx.begin_text();
3582 ctx.show_text("你好").unwrap();
3583 ctx.end_text();
3584
3585 let ops = ctx.operations();
3586 assert!(
3588 ops.contains("<4F60597D> Tj"),
3589 "After restore_state, CJK text should use hex encoding, got: {}",
3590 ops
3591 );
3592 }
3593
3594 #[test]
3595 fn test_graphics_state_arc_str_save_restore() {
3596 let mut ctx = GraphicsContext::new();
3599
3600 ctx.set_font(Font::Custom("TestFont".to_string()), 14.0);
3602 assert_eq!(ctx.current_font_name.as_deref(), Some("TestFont"));
3603 assert!(ctx.is_custom_font);
3604
3605 ctx.save_state();
3607 ctx.set_font(Font::Custom("Other".to_string()), 10.0);
3608 assert_eq!(ctx.current_font_name.as_deref(), Some("Other"));
3609
3610 ctx.restore_state();
3612 assert_eq!(
3613 ctx.current_font_name.as_deref(),
3614 Some("TestFont"),
3615 "Font name must be restored to TestFont after restore_state"
3616 );
3617 assert_eq!(ctx.current_font_size, 14.0);
3618 assert!(
3619 ctx.is_custom_font,
3620 "is_custom_font must be restored to true"
3621 );
3622
3623 if let Some(ref arc) = ctx.current_font_name {
3625 let cloned = arc.clone();
3626 assert_eq!(arc.as_ref(), cloned.as_ref());
3627 assert!(Arc::ptr_eq(arc, &cloned));
3629 }
3630 }
3631}