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(crate) mod ops;
11pub mod page_color_space;
12mod path;
13mod patterns;
14mod pdf_image;
15mod png_decoder;
16pub mod separation_color;
17mod shadings;
18pub mod soft_mask;
19pub mod state;
20pub mod transparency;
21
22pub use calibrated_color::{CalGrayColorSpace, CalRgbColorSpace, CalibratedColor};
23pub use clipping::{ClippingPath, ClippingRegion};
24pub use color::Color;
25pub use color_profiles::{IccColorSpace, IccProfile, IccProfileManager, StandardIccProfile};
26pub use devicen_color::{
27 AlternateColorSpace as DeviceNAlternateColorSpace, ColorantDefinition, ColorantType,
28 DeviceNAttributes, DeviceNColorSpace, LinearTransform, SampledFunction, TintTransformFunction,
29};
30pub use form_xobject::{
31 FormTemplates, FormXObject, FormXObjectBuilder, FormXObjectManager,
32 TransparencyGroup as FormTransparencyGroup,
33};
34pub use indexed_color::{BaseColorSpace, ColorLookupTable, IndexedColorManager, IndexedColorSpace};
35pub use lab_color::{LabColor, LabColorSpace};
36pub use page_color_space::{DeviceColorSpace, PageColorSpace, ParameterisedFamily};
37pub use path::{LineCap, LineJoin, PathBuilder, PathCommand, WindingRule};
38pub use patterns::{
39 PaintType, PatternGraphicsContext, PatternManager, PatternMatrix, PatternType, TilingPattern,
40 TilingType,
41};
42pub use pdf_image::{ColorSpace, Image, ImageFormat, MaskType};
43pub use separation_color::{
44 AlternateColorSpace, SeparationColor, SeparationColorSpace, SpotColors, TintTransform,
45};
46pub use shadings::{
47 AxialShading, ColorStop, FunctionBasedShading, Point, RadialShading, ShadingDefinition,
48 ShadingManager, ShadingPattern, ShadingType,
49};
50pub use soft_mask::{SoftMask, SoftMaskState, SoftMaskType};
51pub use state::{
52 BlendMode, ExtGState, ExtGStateFont, ExtGStateManager, Halftone, LineDashPattern,
53 RenderingIntent, TransferFunction,
54};
55pub use transparency::TransparencyGroup;
56use transparency::TransparencyGroupState;
57
58use crate::error::Result;
59use crate::text::{ColumnContent, ColumnLayout, Font, FontManager, ListElement, Table};
60use std::collections::{HashMap, HashSet};
61use std::fmt::Write;
62use std::sync::Arc;
63
64#[derive(Clone)]
67struct GraphicsState {
68 fill_color: Color,
69 stroke_color: Color,
70 font_name: Option<Arc<str>>,
71 font_size: f64,
72 is_custom_font: bool,
73}
74
75#[derive(Clone)]
76pub struct GraphicsContext {
77 operations: Vec<ops::Op>,
78 current_color: Color,
79 stroke_color: Color,
80 line_width: f64,
81 fill_opacity: f64,
82 stroke_opacity: f64,
83 extgstate_manager: ExtGStateManager,
85 pending_extgstate: Option<ExtGState>,
86 current_dash_pattern: Option<LineDashPattern>,
87 current_miter_limit: f64,
88 current_line_cap: LineCap,
89 current_line_join: LineJoin,
90 current_rendering_intent: RenderingIntent,
91 current_flatness: f64,
92 current_smoothness: f64,
93 clipping_region: ClippingRegion,
95 font_manager: Option<Arc<FontManager>>,
97 state_stack: Vec<GraphicsState>,
99 current_font_name: Option<Arc<str>>,
100 current_font_size: f64,
101 is_custom_font: bool,
103 used_characters_by_font: HashMap<String, HashSet<char>>,
109 glyph_mapping: Option<HashMap<u32, u16>>,
111 transparency_stack: Vec<TransparencyGroupState>,
113}
114
115fn encode_char_as_cid(ch: char, buf: &mut String) {
119 let code = ch as u32;
120 if code <= 0xFFFF {
121 write!(buf, "{:04X}", code).expect("Writing to string should never fail");
122 } else {
123 let adjusted = code - 0x10000;
125 let high = ((adjusted >> 10) & 0x3FF) + 0xD800;
126 let low = (adjusted & 0x3FF) + 0xDC00;
127 write!(buf, "{:04X}{:04X}", high, low).expect("Writing to string should never fail");
128 }
129}
130
131impl Default for GraphicsContext {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137impl GraphicsContext {
138 pub fn new() -> Self {
139 Self {
140 operations: Vec::new(),
141 current_color: Color::black(),
142 stroke_color: Color::black(),
143 line_width: 1.0,
144 fill_opacity: 1.0,
145 stroke_opacity: 1.0,
146 extgstate_manager: ExtGStateManager::new(),
148 pending_extgstate: None,
149 current_dash_pattern: None,
150 current_miter_limit: 10.0,
151 current_line_cap: LineCap::Butt,
152 current_line_join: LineJoin::Miter,
153 current_rendering_intent: RenderingIntent::RelativeColorimetric,
154 current_flatness: 1.0,
155 current_smoothness: 0.0,
156 clipping_region: ClippingRegion::new(),
158 font_manager: None,
160 state_stack: Vec::new(),
161 current_font_name: None,
162 current_font_size: 12.0,
163 is_custom_font: false,
164 used_characters_by_font: HashMap::new(),
165 glyph_mapping: None,
166 transparency_stack: Vec::new(),
167 }
168 }
169
170 pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
171 self.operations.push(ops::Op::MoveTo { x, y });
172 self
173 }
174
175 pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
176 self.operations.push(ops::Op::LineTo { x, y });
177 self
178 }
179
180 pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
181 self.operations.push(ops::Op::CurveTo {
182 x1,
183 y1,
184 x2,
185 y2,
186 x3,
187 y3,
188 });
189 self
190 }
191
192 pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
193 self.operations.push(ops::Op::Rect {
194 x,
195 y,
196 w: width,
197 h: height,
198 });
199 self
200 }
201
202 pub fn circle(&mut self, cx: f64, cy: f64, radius: f64) -> &mut Self {
203 let k = 0.552284749831;
204 let r = radius;
205
206 self.move_to(cx + r, cy);
207 self.curve_to(cx + r, cy + k * r, cx + k * r, cy + r, cx, cy + r);
208 self.curve_to(cx - k * r, cy + r, cx - r, cy + k * r, cx - r, cy);
209 self.curve_to(cx - r, cy - k * r, cx - k * r, cy - r, cx, cy - r);
210 self.curve_to(cx + k * r, cy - r, cx + r, cy - k * r, cx + r, cy);
211 self.close_path()
212 }
213
214 pub fn close_path(&mut self) -> &mut Self {
215 self.operations.push(ops::Op::ClosePath);
216 self
217 }
218
219 pub fn stroke(&mut self) -> &mut Self {
220 self.apply_pending_extgstate().unwrap_or_default();
221 self.apply_stroke_color();
222 self.operations.push(ops::Op::Stroke);
223 self
224 }
225
226 pub fn fill(&mut self) -> &mut Self {
227 self.apply_pending_extgstate().unwrap_or_default();
228 self.apply_fill_color();
229 self.operations.push(ops::Op::FillNonZero);
230 self
231 }
232
233 pub fn fill_stroke(&mut self) -> &mut Self {
234 self.apply_pending_extgstate().unwrap_or_default();
235 self.apply_fill_color();
236 self.apply_stroke_color();
237 self.operations.push(ops::Op::FillStroke);
238 self
239 }
240
241 pub fn paint_shading(&mut self, name: impl Into<String>) -> &mut Self {
250 self.apply_pending_extgstate().unwrap_or_default();
251 self.operations.push(ops::Op::PaintShading(name.into()));
252 self
253 }
254
255 pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
256 self.stroke_color = color;
257 self
258 }
259
260 pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
261 self.current_color = color;
262 self
263 }
264
265 fn push_color_space_and_components(
271 &mut self,
272 space: ops::Op,
273 components: ops::Op,
274 ) -> &mut Self {
275 self.operations.push(space);
276 self.operations.push(components);
277 self
278 }
279
280 pub fn set_fill_color_icc(
291 &mut self,
292 name: impl Into<String>,
293 components: Vec<f64>,
294 ) -> &mut Self {
295 debug_assert!(
296 !components.is_empty(),
297 "ICC fill colour components must not be empty"
298 );
299 self.push_color_space_and_components(
300 ops::Op::SetFillColorSpace(name.into()),
301 ops::Op::SetFillColorComponents(components),
302 )
303 }
304
305 pub fn set_stroke_color_icc(
311 &mut self,
312 name: impl Into<String>,
313 components: Vec<f64>,
314 ) -> &mut Self {
315 debug_assert!(
316 !components.is_empty(),
317 "ICC stroke colour components must not be empty"
318 );
319 self.push_color_space_and_components(
320 ops::Op::SetStrokeColorSpace(name.into()),
321 ops::Op::SetStrokeColorComponents(components),
322 )
323 }
324
325 pub fn set_fill_color_calibrated_named(
333 &mut self,
334 name: impl Into<String>,
335 color: CalibratedColor,
336 ) -> &mut Self {
337 self.push_color_space_and_components(
338 ops::Op::SetFillColorSpace(name.into()),
339 ops::Op::SetFillColorComponents(color.values()),
340 )
341 }
342
343 pub fn set_stroke_color_calibrated_named(
346 &mut self,
347 name: impl Into<String>,
348 color: CalibratedColor,
349 ) -> &mut Self {
350 self.push_color_space_and_components(
351 ops::Op::SetStrokeColorSpace(name.into()),
352 ops::Op::SetStrokeColorComponents(color.values()),
353 )
354 }
355
356 pub fn set_fill_color_lab_named(
363 &mut self,
364 name: impl Into<String>,
365 color: LabColor,
366 ) -> &mut Self {
367 self.push_color_space_and_components(
368 ops::Op::SetFillColorSpace(name.into()),
369 ops::Op::SetFillColorComponents(color.values()),
370 )
371 }
372
373 pub fn set_stroke_color_lab_named(
376 &mut self,
377 name: impl Into<String>,
378 color: LabColor,
379 ) -> &mut Self {
380 self.push_color_space_and_components(
381 ops::Op::SetStrokeColorSpace(name.into()),
382 ops::Op::SetStrokeColorComponents(color.values()),
383 )
384 }
385
386 pub fn set_fill_color_calibrated(&mut self, color: CalibratedColor) -> &mut Self {
392 let cs_name = match &color {
393 CalibratedColor::Gray(_, _) => "CalGray1",
394 CalibratedColor::Rgb(_, _) => "CalRGB1",
395 };
396 self.set_fill_color_calibrated_named(cs_name, color)
397 }
398
399 pub fn set_stroke_color_calibrated(&mut self, color: CalibratedColor) -> &mut Self {
405 let cs_name = match &color {
406 CalibratedColor::Gray(_, _) => "CalGray1",
407 CalibratedColor::Rgb(_, _) => "CalRGB1",
408 };
409 self.set_stroke_color_calibrated_named(cs_name, color)
410 }
411
412 pub fn set_fill_color_lab(&mut self, color: LabColor) -> &mut Self {
418 self.set_fill_color_lab_named("Lab1", color)
419 }
420
421 pub fn set_stroke_color_lab(&mut self, color: LabColor) -> &mut Self {
427 self.set_stroke_color_lab_named("Lab1", color)
428 }
429
430 pub fn set_line_width(&mut self, width: f64) -> &mut Self {
431 self.line_width = width;
432 self.operations.push(ops::Op::SetLineWidth(width));
433 self
434 }
435
436 pub fn set_line_cap(&mut self, cap: LineCap) -> &mut Self {
437 self.current_line_cap = cap;
438 self.operations.push(ops::Op::SetLineCap(cap as u8));
439 self
440 }
441
442 pub fn set_line_join(&mut self, join: LineJoin) -> &mut Self {
443 self.current_line_join = join;
444 self.operations.push(ops::Op::SetLineJoin(join as u8));
445 self
446 }
447
448 pub fn set_opacity(&mut self, opacity: f64) -> &mut Self {
450 let opacity = opacity.clamp(0.0, 1.0);
451 self.fill_opacity = opacity;
452 self.stroke_opacity = opacity;
453
454 if opacity < 1.0 {
456 let mut state = ExtGState::new();
457 state.alpha_fill = Some(opacity);
458 state.alpha_stroke = Some(opacity);
459 self.pending_extgstate = Some(state);
460 }
461
462 self
463 }
464
465 pub fn set_fill_opacity(&mut self, opacity: f64) -> &mut Self {
467 self.fill_opacity = opacity.clamp(0.0, 1.0);
468
469 if opacity < 1.0 {
471 if let Some(ref mut state) = self.pending_extgstate {
472 state.alpha_fill = Some(opacity);
473 } else {
474 let mut state = ExtGState::new();
475 state.alpha_fill = Some(opacity);
476 self.pending_extgstate = Some(state);
477 }
478 }
479
480 self
481 }
482
483 pub fn set_stroke_opacity(&mut self, opacity: f64) -> &mut Self {
485 self.stroke_opacity = opacity.clamp(0.0, 1.0);
486
487 if opacity < 1.0 {
489 if let Some(ref mut state) = self.pending_extgstate {
490 state.alpha_stroke = Some(opacity);
491 } else {
492 let mut state = ExtGState::new();
493 state.alpha_stroke = Some(opacity);
494 self.pending_extgstate = Some(state);
495 }
496 }
497
498 self
499 }
500
501 pub fn save_state(&mut self) -> &mut Self {
502 self.operations.push(ops::Op::SaveState);
503 self.save_clipping_state();
504 self.state_stack.push(GraphicsState {
506 fill_color: self.current_color,
507 stroke_color: self.stroke_color,
508 font_name: self.current_font_name.clone(),
509 font_size: self.current_font_size,
510 is_custom_font: self.is_custom_font,
511 });
512 self
513 }
514
515 pub fn restore_state(&mut self) -> &mut Self {
516 self.operations.push(ops::Op::RestoreState);
517 self.restore_clipping_state();
518 if let Some(state) = self.state_stack.pop() {
520 self.current_color = state.fill_color;
521 self.stroke_color = state.stroke_color;
522 self.current_font_name = state.font_name;
523 self.current_font_size = state.font_size;
524 self.is_custom_font = state.is_custom_font;
525 }
526 self
527 }
528
529 pub fn begin_transparency_group(&mut self, group: TransparencyGroup) -> &mut Self {
532 self.save_state();
534
535 self.operations
537 .push(ops::Op::Comment("Begin Transparency Group".to_string()));
538
539 let mut extgstate = ExtGState::new();
541 extgstate = extgstate.with_blend_mode(group.blend_mode.clone());
542 extgstate.alpha_fill = Some(group.opacity as f64);
543 extgstate.alpha_stroke = Some(group.opacity as f64);
544
545 self.pending_extgstate = Some(extgstate);
547 let _ = self.apply_pending_extgstate();
548
549 self.transparency_stack
554 .push(TransparencyGroupState::new(group));
555
556 self
557 }
558
559 pub fn end_transparency_group(&mut self) -> &mut Self {
561 if let Some(_group_state) = self.transparency_stack.pop() {
562 self.operations
564 .push(ops::Op::Comment("End Transparency Group".to_string()));
565
566 self.restore_state();
568 }
569 self
570 }
571
572 pub fn in_transparency_group(&self) -> bool {
574 !self.transparency_stack.is_empty()
575 }
576
577 pub fn current_transparency_group(&self) -> Option<&TransparencyGroup> {
579 self.transparency_stack.last().map(|state| &state.group)
580 }
581
582 pub fn translate(&mut self, tx: f64, ty: f64) -> &mut Self {
583 self.operations.push(ops::Op::Cm {
584 a: 1.0,
585 b: 0.0,
586 c: 0.0,
587 d: 1.0,
588 e: tx,
589 f: ty,
590 });
591 self
592 }
593
594 pub fn scale(&mut self, sx: f64, sy: f64) -> &mut Self {
595 self.operations.push(ops::Op::Cm {
596 a: sx,
597 b: 0.0,
598 c: 0.0,
599 d: sy,
600 e: 0.0,
601 f: 0.0,
602 });
603 self
604 }
605
606 pub fn rotate(&mut self, angle: f64) -> &mut Self {
607 let cos = angle.cos();
608 let sin = angle.sin();
609 self.operations.push(ops::Op::Cm {
613 a: cos,
614 b: sin,
615 c: -sin,
616 d: cos,
617 e: 0.0,
618 f: 0.0,
619 });
620 self
621 }
622
623 pub fn transform(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> &mut Self {
624 self.operations.push(ops::Op::Cm { a, b, c, d, e, f });
625 self
626 }
627
628 pub fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
629 self.rect(x, y, width, height)
630 }
631
632 pub fn draw_image(
633 &mut self,
634 image_name: impl Into<String>,
635 x: f64,
636 y: f64,
637 width: f64,
638 height: f64,
639 ) -> &mut Self {
640 self.save_state();
642
643 self.operations.push(ops::Op::Cm {
646 a: width,
647 b: 0.0,
648 c: 0.0,
649 d: height,
650 e: x,
651 f: y,
652 });
653
654 self.operations
656 .push(ops::Op::InvokeXObject(image_name.into()));
657
658 self.restore_state();
660
661 self
662 }
663
664 pub fn draw_image_with_transparency(
667 &mut self,
668 image_name: impl Into<String>,
669 x: f64,
670 y: f64,
671 width: f64,
672 height: f64,
673 mask_name: Option<&str>,
674 ) -> &mut Self {
675 self.save_state();
677
678 if let Some(mask) = mask_name {
680 let mut extgstate = ExtGState::new();
682 extgstate.set_soft_mask_name(mask.to_string());
683
684 let gs_name = self
686 .extgstate_manager
687 .add_state(extgstate)
688 .unwrap_or_else(|_| "GS1".to_string());
689 self.operations.push(ops::Op::SetExtGState(gs_name));
690 }
691
692 self.operations.push(ops::Op::Cm {
694 a: width,
695 b: 0.0,
696 c: 0.0,
697 d: height,
698 e: x,
699 f: y,
700 });
701
702 self.operations
704 .push(ops::Op::InvokeXObject(image_name.into()));
705
706 if mask_name.is_some() {
708 let mut reset_extgstate = ExtGState::new();
710 reset_extgstate.set_soft_mask_none();
711
712 let gs_name = self
713 .extgstate_manager
714 .add_state(reset_extgstate)
715 .unwrap_or_else(|_| "GS2".to_string());
716 self.operations.push(ops::Op::SetExtGState(gs_name));
717 }
718
719 self.restore_state();
721
722 self
723 }
724
725 fn apply_stroke_color(&mut self) {
726 self.operations
732 .push(ops::Op::SetStrokeColor(self.stroke_color));
733 }
734
735 fn apply_fill_color(&mut self) {
736 self.operations
741 .push(ops::Op::SetFillColor(self.current_color));
742 }
743
744 pub(crate) fn generate_operations(&self) -> Result<Vec<u8>> {
745 let mut buf = Vec::new();
746 ops::serialize_ops(&mut buf, &self.operations);
747 Ok(buf)
748 }
749
750 pub(crate) fn drain_ops(&mut self) -> Vec<ops::Op> {
760 std::mem::take(&mut self.operations)
761 }
762
763 pub(crate) fn ops_slice(&self) -> &[ops::Op] {
766 &self.operations
767 }
768
769 pub fn uses_transparency(&self) -> bool {
771 self.fill_opacity < 1.0 || self.stroke_opacity < 1.0
772 }
773
774 pub fn generate_graphics_state_dict(&self) -> Option<String> {
776 if !self.uses_transparency() {
777 return None;
778 }
779
780 let mut dict = String::from("<< /Type /ExtGState");
781
782 if self.fill_opacity < 1.0 {
783 write!(&mut dict, " /ca {:.3}", self.fill_opacity)
784 .expect("Writing to string should never fail");
785 }
786
787 if self.stroke_opacity < 1.0 {
788 write!(&mut dict, " /CA {:.3}", self.stroke_opacity)
789 .expect("Writing to string should never fail");
790 }
791
792 dict.push_str(" >>");
793 Some(dict)
794 }
795
796 pub fn fill_color(&self) -> Color {
798 self.current_color
799 }
800
801 pub fn stroke_color(&self) -> Color {
803 self.stroke_color
804 }
805
806 pub fn line_width(&self) -> f64 {
808 self.line_width
809 }
810
811 pub fn fill_opacity(&self) -> f64 {
813 self.fill_opacity
814 }
815
816 pub fn stroke_opacity(&self) -> f64 {
818 self.stroke_opacity
819 }
820
821 pub fn operations(&self) -> String {
828 ops::ops_to_string(&self.operations)
829 }
830
831 pub fn get_operations(&self) -> String {
834 ops::ops_to_string(&self.operations)
835 }
836
837 pub fn clear(&mut self) {
839 self.operations.clear();
840 }
841
842 pub fn begin_text(&mut self) -> &mut Self {
844 self.operations.push(ops::Op::BeginText);
845 self
846 }
847
848 pub fn end_text(&mut self) -> &mut Self {
850 self.operations.push(ops::Op::EndText);
851 self
852 }
853
854 pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
856 self.operations.push(ops::Op::SetFont {
857 name: font.pdf_name(),
858 size,
859 });
860
861 match &font {
863 Font::Custom(name) => {
864 self.current_font_name = Some(Arc::from(name.as_str()));
865 self.current_font_size = size;
866 self.is_custom_font = true;
867 }
868 _ => {
869 self.current_font_name = Some(Arc::from(font.pdf_name().as_str()));
870 self.current_font_size = size;
871 self.is_custom_font = false;
872 }
873 }
874
875 self
876 }
877
878 pub fn set_text_position(&mut self, x: f64, y: f64) -> &mut Self {
880 self.operations.push(ops::Op::SetTextPosition { x, y });
881 self
882 }
883
884 pub fn show_text(&mut self, text: &str) -> Result<&mut Self> {
891 self.record_used_chars(text);
895
896 if self.is_custom_font {
897 let mut hex = String::new();
899 for ch in text.chars() {
900 encode_char_as_cid(ch, &mut hex);
901 }
902 self.operations.push(ops::Op::ShowTextHex(hex.into_bytes()));
903 } else {
904 let mut escaped = String::new();
906 for ch in text.chars() {
907 match ch {
908 '(' => escaped.push_str("\\("),
909 ')' => escaped.push_str("\\)"),
910 '\\' => escaped.push_str("\\\\"),
911 '\n' => escaped.push_str("\\n"),
912 '\r' => escaped.push_str("\\r"),
913 '\t' => escaped.push_str("\\t"),
914 _ => escaped.push(ch),
915 }
916 }
917 self.operations
918 .push(ops::Op::ShowText(escaped.into_bytes()));
919 }
920 Ok(self)
921 }
922
923 pub fn set_word_spacing(&mut self, spacing: f64) -> &mut Self {
925 self.operations.push(ops::Op::SetWordSpacing(spacing));
926 self
927 }
928
929 pub fn set_character_spacing(&mut self, spacing: f64) -> &mut Self {
931 self.operations.push(ops::Op::SetCharSpacing(spacing));
932 self
933 }
934
935 pub fn show_justified_text(&mut self, text: &str, target_width: f64) -> Result<&mut Self> {
937 let words: Vec<&str> = text.split_whitespace().collect();
939 if words.len() <= 1 {
940 return self.show_text(text);
942 }
943
944 let text_without_spaces = words.join("");
946 let natural_text_width = self.estimate_text_width_simple(&text_without_spaces);
947 let space_width = self.estimate_text_width_simple(" ");
948 let natural_width = natural_text_width + (space_width * (words.len() - 1) as f64);
949
950 let extra_space_needed = target_width - natural_width;
952 let word_gaps = (words.len() - 1) as f64;
953
954 if word_gaps > 0.0 && extra_space_needed > 0.0 {
955 let extra_word_spacing = extra_space_needed / word_gaps;
956
957 self.set_word_spacing(extra_word_spacing);
959
960 self.show_text(text)?;
962
963 self.set_word_spacing(0.0);
965 } else {
966 self.show_text(text)?;
968 }
969
970 Ok(self)
971 }
972
973 fn estimate_text_width_simple(&self, text: &str) -> f64 {
975 let font_size = self.current_font_size;
978 text.len() as f64 * font_size * 0.6 }
980
981 pub fn render_table(&mut self, table: &Table) -> Result<()> {
983 table.render(self)
984 }
985
986 pub fn render_list(&mut self, list: &ListElement) -> Result<()> {
988 match list {
989 ListElement::Ordered(ordered) => ordered.render(self),
990 ListElement::Unordered(unordered) => unordered.render(self),
991 }
992 }
993
994 pub fn render_column_layout(
996 &mut self,
997 layout: &ColumnLayout,
998 content: &ColumnContent,
999 x: f64,
1000 y: f64,
1001 height: f64,
1002 ) -> Result<()> {
1003 layout.render(self, content, x, y, height)
1004 }
1005
1006 pub fn set_line_dash_pattern(&mut self, pattern: LineDashPattern) -> &mut Self {
1010 self.current_dash_pattern = Some(pattern.clone());
1011 self.operations
1012 .push(ops::Op::SetDashPatternRaw(pattern.to_pdf_string()));
1013 self
1014 }
1015
1016 pub fn set_line_solid(&mut self) -> &mut Self {
1018 self.current_dash_pattern = None;
1019 self.operations
1020 .push(ops::Op::SetDashPatternRaw("[] 0".to_string()));
1021 self
1022 }
1023
1024 pub fn set_miter_limit(&mut self, limit: f64) -> &mut Self {
1026 self.current_miter_limit = limit.max(1.0);
1027 self.operations
1028 .push(ops::Op::SetMiterLimit(self.current_miter_limit));
1029 self
1030 }
1031
1032 pub fn set_rendering_intent(&mut self, intent: RenderingIntent) -> &mut Self {
1034 self.current_rendering_intent = intent;
1035 self.operations
1036 .push(ops::Op::SetRenderingIntent(intent.pdf_name().to_string()));
1037 self
1038 }
1039
1040 pub fn set_flatness(&mut self, flatness: f64) -> &mut Self {
1042 self.current_flatness = flatness.clamp(0.0, 100.0);
1043 self.operations
1044 .push(ops::Op::SetFlatness(self.current_flatness));
1045 self
1046 }
1047
1048 pub fn apply_extgstate(&mut self, state: ExtGState) -> Result<&mut Self> {
1050 let state_name = self.extgstate_manager.add_state(state)?;
1051 self.operations.push(ops::Op::SetExtGState(state_name));
1052 Ok(self)
1053 }
1054
1055 #[allow(dead_code)]
1057 fn set_pending_extgstate(&mut self, state: ExtGState) {
1058 self.pending_extgstate = Some(state);
1059 }
1060
1061 fn apply_pending_extgstate(&mut self) -> Result<()> {
1063 if let Some(state) = self.pending_extgstate.take() {
1064 let state_name = self.extgstate_manager.add_state(state)?;
1065 self.operations.push(ops::Op::SetExtGState(state_name));
1066 }
1067 Ok(())
1068 }
1069
1070 pub fn with_extgstate<F>(&mut self, builder: F) -> Result<&mut Self>
1072 where
1073 F: FnOnce(ExtGState) -> ExtGState,
1074 {
1075 let state = builder(ExtGState::new());
1076 self.apply_extgstate(state)
1077 }
1078
1079 pub fn set_blend_mode(&mut self, mode: BlendMode) -> Result<&mut Self> {
1081 let state = ExtGState::new().with_blend_mode(mode);
1082 self.apply_extgstate(state)
1083 }
1084
1085 pub fn set_alpha(&mut self, alpha: f64) -> Result<&mut Self> {
1087 let state = ExtGState::new().with_alpha(alpha);
1088 self.apply_extgstate(state)
1089 }
1090
1091 pub fn set_alpha_stroke(&mut self, alpha: f64) -> Result<&mut Self> {
1093 let state = ExtGState::new().with_alpha_stroke(alpha);
1094 self.apply_extgstate(state)
1095 }
1096
1097 pub fn set_alpha_fill(&mut self, alpha: f64) -> Result<&mut Self> {
1099 let state = ExtGState::new().with_alpha_fill(alpha);
1100 self.apply_extgstate(state)
1101 }
1102
1103 pub fn set_overprint_stroke(&mut self, overprint: bool) -> Result<&mut Self> {
1105 let state = ExtGState::new().with_overprint_stroke(overprint);
1106 self.apply_extgstate(state)
1107 }
1108
1109 pub fn set_overprint_fill(&mut self, overprint: bool) -> Result<&mut Self> {
1111 let state = ExtGState::new().with_overprint_fill(overprint);
1112 self.apply_extgstate(state)
1113 }
1114
1115 pub fn set_stroke_adjustment(&mut self, adjustment: bool) -> Result<&mut Self> {
1117 let state = ExtGState::new().with_stroke_adjustment(adjustment);
1118 self.apply_extgstate(state)
1119 }
1120
1121 pub fn set_smoothness(&mut self, smoothness: f64) -> Result<&mut Self> {
1123 self.current_smoothness = smoothness.clamp(0.0, 1.0);
1124 let state = ExtGState::new().with_smoothness(self.current_smoothness);
1125 self.apply_extgstate(state)
1126 }
1127
1128 pub fn line_dash_pattern(&self) -> Option<&LineDashPattern> {
1132 self.current_dash_pattern.as_ref()
1133 }
1134
1135 pub fn miter_limit(&self) -> f64 {
1137 self.current_miter_limit
1138 }
1139
1140 pub fn line_cap(&self) -> LineCap {
1142 self.current_line_cap
1143 }
1144
1145 pub fn line_join(&self) -> LineJoin {
1147 self.current_line_join
1148 }
1149
1150 pub fn rendering_intent(&self) -> RenderingIntent {
1152 self.current_rendering_intent
1153 }
1154
1155 pub fn flatness(&self) -> f64 {
1157 self.current_flatness
1158 }
1159
1160 pub fn smoothness(&self) -> f64 {
1162 self.current_smoothness
1163 }
1164
1165 pub fn extgstate_manager(&self) -> &ExtGStateManager {
1167 &self.extgstate_manager
1168 }
1169
1170 pub fn extgstate_manager_mut(&mut self) -> &mut ExtGStateManager {
1172 &mut self.extgstate_manager
1173 }
1174
1175 pub fn generate_extgstate_resources(&self) -> Result<String> {
1177 self.extgstate_manager.to_resource_dictionary()
1178 }
1179
1180 pub fn has_extgstates(&self) -> bool {
1182 self.extgstate_manager.count() > 0
1183 }
1184
1185 pub fn add_command(&mut self, command: &str) {
1192 let mut bytes = command.as_bytes().to_vec();
1193 bytes.push(b'\n');
1194 self.operations.push(ops::Op::Raw(bytes));
1195 }
1196
1197 pub fn clip(&mut self) -> &mut Self {
1199 self.operations.push(ops::Op::ClipNonZero);
1200 self
1201 }
1202
1203 pub fn end_path(&mut self) -> &mut Self {
1209 self.operations.push(ops::Op::EndPath);
1212 self
1213 }
1214
1215 pub fn clip_even_odd(&mut self) -> &mut Self {
1217 self.operations.push(ops::Op::ClipEvenOdd);
1218 self
1219 }
1220
1221 pub fn clip_stroke(&mut self) -> &mut Self {
1223 self.apply_stroke_color();
1224 self.operations.push(ops::Op::ClipStroke);
1225 self
1226 }
1227
1228 pub fn set_clipping_path(&mut self, path: ClippingPath) -> Result<&mut Self> {
1230 let ops_str = path.to_pdf_operations()?;
1231 self.operations.push(ops::Op::Raw(ops_str.into_bytes()));
1232 self.clipping_region.set_clip(path);
1233 Ok(self)
1234 }
1235
1236 pub fn clear_clipping(&mut self) -> &mut Self {
1238 self.clipping_region.clear_clip();
1239 self
1240 }
1241
1242 fn save_clipping_state(&mut self) {
1244 self.clipping_region.save();
1245 }
1246
1247 fn restore_clipping_state(&mut self) {
1249 self.clipping_region.restore();
1250 }
1251
1252 pub fn clip_rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> Result<&mut Self> {
1254 let path = ClippingPath::rect(x, y, width, height);
1255 self.set_clipping_path(path)
1256 }
1257
1258 pub fn clip_circle(&mut self, cx: f64, cy: f64, radius: f64) -> Result<&mut Self> {
1260 let path = ClippingPath::circle(cx, cy, radius);
1261 self.set_clipping_path(path)
1262 }
1263
1264 pub fn clip_ellipse(&mut self, cx: f64, cy: f64, rx: f64, ry: f64) -> Result<&mut Self> {
1266 let path = ClippingPath::ellipse(cx, cy, rx, ry);
1267 self.set_clipping_path(path)
1268 }
1269
1270 pub fn has_clipping(&self) -> bool {
1272 self.clipping_region.has_clip()
1273 }
1274
1275 pub fn clipping_path(&self) -> Option<&ClippingPath> {
1277 self.clipping_region.current()
1278 }
1279
1280 pub fn set_font_manager(&mut self, font_manager: Arc<FontManager>) -> &mut Self {
1282 self.font_manager = Some(font_manager);
1283 self
1284 }
1285
1286 pub fn set_custom_font(&mut self, font_name: &str, size: f64) -> &mut Self {
1288 self.operations.push(ops::Op::SetFont {
1290 name: font_name.to_string(),
1291 size,
1292 });
1293
1294 self.current_font_name = Some(Arc::from(font_name));
1295 self.current_font_size = size;
1296 self.is_custom_font = true;
1297
1298 if let Some(ref font_manager) = self.font_manager {
1300 if let Some(mapping) = font_manager.get_font_glyph_mapping(font_name) {
1301 self.glyph_mapping = Some(mapping);
1302 }
1303 }
1304
1305 self
1306 }
1307
1308 pub fn set_glyph_mapping(&mut self, mapping: HashMap<u32, u16>) -> &mut Self {
1310 self.glyph_mapping = Some(mapping);
1311 self
1312 }
1313
1314 pub fn draw_text(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1316 self.record_used_chars(text);
1319
1320 let needs_unicode = self.is_custom_font || text.chars().any(|c| c as u32 > 255);
1323
1324 if needs_unicode {
1326 self.draw_with_unicode_encoding(text, x, y)
1327 } else {
1328 self.draw_with_simple_encoding(text, x, y)
1329 }
1330 }
1331
1332 fn draw_with_simple_encoding(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1334 let has_unicode = text.chars().any(|c| c as u32 > 255);
1336
1337 if has_unicode {
1338 tracing::debug!("Warning: Text contains Unicode characters but using Latin-1 font. Characters will be replaced with '?'");
1339 }
1340
1341 self.operations.push(ops::Op::BeginText);
1342 self.apply_fill_color();
1343 self.push_active_font();
1344 self.operations.push(ops::Op::SetTextPosition { x, y });
1345
1346 let mut buf = String::new();
1349 for ch in text.chars() {
1350 let code = ch as u32;
1351 if code <= 127 {
1352 match ch {
1353 '(' => buf.push_str("\\("),
1354 ')' => buf.push_str("\\)"),
1355 '\\' => buf.push_str("\\\\"),
1356 '\n' => buf.push_str("\\n"),
1357 '\r' => buf.push_str("\\r"),
1358 '\t' => buf.push_str("\\t"),
1359 _ => buf.push(ch),
1360 }
1361 } else if code <= 255 {
1362 use std::fmt::Write as _;
1363 write!(&mut buf, "\\{code:03o}").expect("write to String never fails");
1364 } else {
1365 buf.push('?');
1366 }
1367 }
1368 self.operations.push(ops::Op::ShowText(buf.into_bytes()));
1369 self.operations.push(ops::Op::EndText);
1370
1371 Ok(self)
1372 }
1373
1374 fn draw_with_unicode_encoding(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1376 self.operations.push(ops::Op::BeginText);
1377 self.apply_fill_color();
1378 self.push_active_font();
1379 self.operations.push(ops::Op::SetTextPosition { x, y });
1380
1381 let mut hex = String::new();
1382 for ch in text.chars() {
1383 encode_char_as_cid(ch, &mut hex);
1384 }
1385 self.operations.push(ops::Op::ShowTextHex(hex.into_bytes()));
1386 self.operations.push(ops::Op::EndText);
1387
1388 Ok(self)
1389 }
1390
1391 fn push_active_font(&mut self) {
1396 let name = self
1397 .current_font_name
1398 .as_deref()
1399 .unwrap_or("Helvetica")
1400 .to_string();
1401 self.operations.push(ops::Op::SetFont {
1402 name,
1403 size: self.current_font_size,
1404 });
1405 }
1406
1407 #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1409 pub fn draw_text_hex(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1410 self.operations.push(ops::Op::BeginText);
1411 self.apply_fill_color();
1412 self.push_active_font();
1413 self.operations.push(ops::Op::SetTextPosition { x, y });
1414
1415 let mut hex = String::new();
1416 for ch in text.chars() {
1417 use std::fmt::Write as _;
1418 if ch as u32 <= 255 {
1419 write!(&mut hex, "{:02X}", ch as u8).expect("write to String never fails");
1420 } else {
1421 hex.push_str("3F");
1422 }
1423 }
1424 self.operations.push(ops::Op::ShowTextHex(hex.into_bytes()));
1425 self.operations.push(ops::Op::EndText);
1426
1427 Ok(self)
1428 }
1429
1430 #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1432 pub fn draw_text_cid(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1433 use crate::fonts::needs_type0_font;
1434
1435 self.operations.push(ops::Op::BeginText);
1436 self.apply_fill_color();
1437 self.push_active_font();
1438 self.operations.push(ops::Op::SetTextPosition { x, y });
1439
1440 let mut hex = String::new();
1441 if needs_type0_font(text) {
1442 for ch in text.chars() {
1443 encode_char_as_cid(ch, &mut hex);
1444 }
1445 } else {
1446 for ch in text.chars() {
1447 use std::fmt::Write as _;
1448 if ch as u32 <= 255 {
1449 write!(&mut hex, "{:02X}", ch as u8).expect("write to String never fails");
1450 } else {
1451 hex.push_str("3F");
1452 }
1453 }
1454 }
1455 self.operations.push(ops::Op::ShowTextHex(hex.into_bytes()));
1456 self.operations.push(ops::Op::EndText);
1457
1458 Ok(self)
1459 }
1460
1461 #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1463 pub fn draw_text_unicode(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1464 self.operations.push(ops::Op::BeginText);
1465 self.apply_fill_color();
1466 self.push_active_font();
1467 self.operations.push(ops::Op::SetTextPosition { x, y });
1468
1469 let mut hex = String::new();
1470 let mut utf16_buffer = [0u16; 2];
1471 for ch in text.chars() {
1472 let encoded = ch.encode_utf16(&mut utf16_buffer);
1473 for unit in encoded {
1474 use std::fmt::Write as _;
1475 write!(&mut hex, "{:04X}", unit).expect("write to String never fails");
1476 }
1477 }
1478 self.operations.push(ops::Op::ShowTextHex(hex.into_bytes()));
1479 self.operations.push(ops::Op::EndText);
1480
1481 Ok(self)
1482 }
1483
1484 fn record_used_chars(&mut self, text: &str) {
1495 let bucket = self.current_font_name.as_deref().unwrap_or("").to_string();
1496 self.used_characters_by_font
1497 .entry(bucket)
1498 .or_default()
1499 .extend(text.chars());
1500 }
1501
1502 #[cfg(test)]
1508 pub(crate) fn get_used_characters(&self) -> Option<HashSet<char>> {
1509 let merged: HashSet<char> = self
1510 .used_characters_by_font
1511 .values()
1512 .flat_map(|s| s.iter().copied())
1513 .collect();
1514 if merged.is_empty() {
1515 None
1516 } else {
1517 Some(merged)
1518 }
1519 }
1520
1521 pub(crate) fn get_used_characters_by_font(&self) -> &HashMap<String, HashSet<char>> {
1529 &self.used_characters_by_font
1530 }
1531
1532 pub(crate) fn merge_font_usage(&mut self, usage: &HashMap<String, HashSet<char>>) {
1538 for (name, chars) in usage {
1539 self.used_characters_by_font
1540 .entry(name.clone())
1541 .or_default()
1542 .extend(chars);
1543 }
1544 }
1545}
1546
1547#[cfg(test)]
1548mod tests {
1549 use super::*;
1550
1551 #[test]
1552 fn test_graphics_context_new() {
1553 let ctx = GraphicsContext::new();
1554 assert_eq!(ctx.fill_color(), Color::black());
1555 assert_eq!(ctx.stroke_color(), Color::black());
1556 assert_eq!(ctx.line_width(), 1.0);
1557 assert_eq!(ctx.fill_opacity(), 1.0);
1558 assert_eq!(ctx.stroke_opacity(), 1.0);
1559 assert!(ctx.operations().is_empty());
1560 }
1561
1562 #[test]
1563 fn test_graphics_context_default() {
1564 let ctx = GraphicsContext::default();
1565 assert_eq!(ctx.fill_color(), Color::black());
1566 assert_eq!(ctx.stroke_color(), Color::black());
1567 assert_eq!(ctx.line_width(), 1.0);
1568 }
1569
1570 #[test]
1571 fn test_move_to() {
1572 let mut ctx = GraphicsContext::new();
1573 ctx.move_to(10.0, 20.0);
1574 assert!(ctx.operations().contains("10.00 20.00 m\n"));
1575 }
1576
1577 #[test]
1578 fn test_line_to() {
1579 let mut ctx = GraphicsContext::new();
1580 ctx.line_to(30.0, 40.0);
1581 assert!(ctx.operations().contains("30.00 40.00 l\n"));
1582 }
1583
1584 #[test]
1585 fn test_curve_to() {
1586 let mut ctx = GraphicsContext::new();
1587 ctx.curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0);
1588 assert!(ctx
1589 .operations()
1590 .contains("10.00 20.00 30.00 40.00 50.00 60.00 c\n"));
1591 }
1592
1593 #[test]
1594 fn test_rect() {
1595 let mut ctx = GraphicsContext::new();
1596 ctx.rect(10.0, 20.0, 100.0, 50.0);
1597 assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
1598 }
1599
1600 #[test]
1601 fn test_rectangle_alias() {
1602 let mut ctx = GraphicsContext::new();
1603 ctx.rectangle(10.0, 20.0, 100.0, 50.0);
1604 assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
1605 }
1606
1607 #[test]
1608 fn test_circle() {
1609 let mut ctx = GraphicsContext::new();
1610 ctx.circle(50.0, 50.0, 25.0);
1611
1612 let ops = ctx.operations();
1613 assert!(ops.contains("75.00 50.00 m\n"));
1615 assert!(ops.contains(" c\n"));
1617 assert!(ops.contains("h\n"));
1619 }
1620
1621 #[test]
1622 fn test_close_path() {
1623 let mut ctx = GraphicsContext::new();
1624 ctx.close_path();
1625 assert!(ctx.operations().contains("h\n"));
1626 }
1627
1628 #[test]
1629 fn test_stroke() {
1630 let mut ctx = GraphicsContext::new();
1631 ctx.set_stroke_color(Color::red());
1632 ctx.rect(0.0, 0.0, 10.0, 10.0);
1633 ctx.stroke();
1634
1635 let ops = ctx.operations();
1636 assert!(ops.contains("1.000 0.000 0.000 RG\n"));
1637 assert!(ops.contains("S\n"));
1638 }
1639
1640 #[test]
1641 fn test_fill() {
1642 let mut ctx = GraphicsContext::new();
1643 ctx.set_fill_color(Color::blue());
1644 ctx.rect(0.0, 0.0, 10.0, 10.0);
1645 ctx.fill();
1646
1647 let ops = ctx.operations();
1648 assert!(ops.contains("0.000 0.000 1.000 rg\n"));
1649 assert!(ops.contains("f\n"));
1650 }
1651
1652 #[test]
1653 fn test_fill_stroke() {
1654 let mut ctx = GraphicsContext::new();
1655 ctx.set_fill_color(Color::green());
1656 ctx.set_stroke_color(Color::red());
1657 ctx.rect(0.0, 0.0, 10.0, 10.0);
1658 ctx.fill_stroke();
1659
1660 let ops = ctx.operations();
1661 assert!(ops.contains("0.000 1.000 0.000 rg\n"));
1662 assert!(ops.contains("1.000 0.000 0.000 RG\n"));
1663 assert!(ops.contains("B\n"));
1664 }
1665
1666 #[test]
1667 fn test_set_stroke_color() {
1668 let mut ctx = GraphicsContext::new();
1669 ctx.set_stroke_color(Color::rgb(0.5, 0.6, 0.7));
1670 assert_eq!(ctx.stroke_color(), Color::Rgb(0.5, 0.6, 0.7));
1671 }
1672
1673 #[test]
1674 fn test_set_fill_color() {
1675 let mut ctx = GraphicsContext::new();
1676 ctx.set_fill_color(Color::gray(0.5));
1677 assert_eq!(ctx.fill_color(), Color::Gray(0.5));
1678 }
1679
1680 #[test]
1681 fn test_set_line_width() {
1682 let mut ctx = GraphicsContext::new();
1683 ctx.set_line_width(2.5);
1684 assert_eq!(ctx.line_width(), 2.5);
1685 assert!(ctx.operations().contains("2.50 w\n"));
1686 }
1687
1688 #[test]
1689 fn test_set_line_cap() {
1690 let mut ctx = GraphicsContext::new();
1691 ctx.set_line_cap(LineCap::Round);
1692 assert!(ctx.operations().contains("1 J\n"));
1693
1694 ctx.set_line_cap(LineCap::Butt);
1695 assert!(ctx.operations().contains("0 J\n"));
1696
1697 ctx.set_line_cap(LineCap::Square);
1698 assert!(ctx.operations().contains("2 J\n"));
1699 }
1700
1701 #[test]
1702 fn test_set_line_join() {
1703 let mut ctx = GraphicsContext::new();
1704 ctx.set_line_join(LineJoin::Round);
1705 assert!(ctx.operations().contains("1 j\n"));
1706
1707 ctx.set_line_join(LineJoin::Miter);
1708 assert!(ctx.operations().contains("0 j\n"));
1709
1710 ctx.set_line_join(LineJoin::Bevel);
1711 assert!(ctx.operations().contains("2 j\n"));
1712 }
1713
1714 #[test]
1715 fn test_save_restore_state() {
1716 let mut ctx = GraphicsContext::new();
1717 ctx.save_state();
1718 assert!(ctx.operations().contains("q\n"));
1719
1720 ctx.restore_state();
1721 assert!(ctx.operations().contains("Q\n"));
1722 }
1723
1724 #[test]
1725 fn test_translate() {
1726 let mut ctx = GraphicsContext::new();
1727 ctx.translate(50.0, 100.0);
1728 assert!(ctx
1732 .operations()
1733 .contains("1.00 0.00 0.00 1.00 50.00 100.00 cm\n"));
1734 }
1735
1736 #[test]
1737 fn test_scale() {
1738 let mut ctx = GraphicsContext::new();
1739 ctx.scale(2.0, 3.0);
1740 assert!(ctx
1742 .operations()
1743 .contains("2.00 0.00 0.00 3.00 0.00 0.00 cm\n"));
1744 }
1745
1746 #[test]
1747 fn test_rotate() {
1748 let mut ctx = GraphicsContext::new();
1749 let angle = std::f64::consts::PI / 4.0; ctx.rotate(angle);
1751
1752 let ops = ctx.operations();
1753 assert!(ops.contains(" cm\n"));
1754 assert!(ops.contains("0.71"));
1757 }
1758
1759 #[test]
1760 fn test_transform() {
1761 let mut ctx = GraphicsContext::new();
1762 ctx.transform(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
1763 assert!(ctx
1764 .operations()
1765 .contains("1.00 2.00 3.00 4.00 5.00 6.00 cm\n"));
1766 }
1767
1768 #[test]
1769 fn test_draw_image() {
1770 let mut ctx = GraphicsContext::new();
1771 ctx.draw_image("Image1", 10.0, 20.0, 100.0, 150.0);
1772
1773 let ops = ctx.operations();
1774 assert!(ops.contains("q\n")); assert!(ops.contains("100.00 0.00 0.00 150.00 10.00 20.00 cm\n"));
1777 assert!(ops.contains("/Image1 Do\n")); assert!(ops.contains("Q\n")); }
1780
1781 #[test]
1782 fn test_gray_color_operations() {
1783 let mut ctx = GraphicsContext::new();
1784 ctx.set_stroke_color(Color::gray(0.5));
1785 ctx.set_fill_color(Color::gray(0.7));
1786 ctx.stroke();
1787 ctx.fill();
1788
1789 let ops = ctx.operations();
1790 assert!(ops.contains("0.500 G\n")); assert!(ops.contains("0.700 g\n")); }
1793
1794 #[test]
1795 fn test_cmyk_color_operations() {
1796 let mut ctx = GraphicsContext::new();
1797 ctx.set_stroke_color(Color::cmyk(0.1, 0.2, 0.3, 0.4));
1798 ctx.set_fill_color(Color::cmyk(0.5, 0.6, 0.7, 0.8));
1799 ctx.stroke();
1800 ctx.fill();
1801
1802 let ops = ctx.operations();
1803 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")); }
1806
1807 #[test]
1808 fn test_method_chaining() {
1809 let mut ctx = GraphicsContext::new();
1810 ctx.move_to(0.0, 0.0)
1811 .line_to(10.0, 0.0)
1812 .line_to(10.0, 10.0)
1813 .line_to(0.0, 10.0)
1814 .close_path()
1815 .set_fill_color(Color::red())
1816 .fill();
1817
1818 let ops = ctx.operations();
1819 assert!(ops.contains("0.00 0.00 m\n"));
1820 assert!(ops.contains("10.00 0.00 l\n"));
1821 assert!(ops.contains("10.00 10.00 l\n"));
1822 assert!(ops.contains("0.00 10.00 l\n"));
1823 assert!(ops.contains("h\n"));
1824 assert!(ops.contains("f\n"));
1825 }
1826
1827 #[test]
1828 fn test_generate_operations() {
1829 let mut ctx = GraphicsContext::new();
1830 ctx.rect(0.0, 0.0, 10.0, 10.0);
1831
1832 let result = ctx.generate_operations();
1833 assert!(result.is_ok());
1834 let bytes = result.expect("Writing to string should never fail");
1835 let ops_string = String::from_utf8(bytes).expect("Writing to string should never fail");
1836 assert!(ops_string.contains("0.00 0.00 10.00 10.00 re"));
1837 }
1838
1839 #[test]
1840 fn test_clear_operations() {
1841 let mut ctx = GraphicsContext::new();
1842 ctx.rect(0.0, 0.0, 10.0, 10.0);
1843 assert!(!ctx.operations().is_empty());
1844
1845 ctx.clear();
1846 assert!(ctx.operations().is_empty());
1847 }
1848
1849 #[test]
1850 fn test_complex_path() {
1851 let mut ctx = GraphicsContext::new();
1852 ctx.save_state()
1853 .translate(100.0, 100.0)
1854 .rotate(std::f64::consts::PI / 6.0)
1855 .scale(2.0, 2.0)
1856 .set_line_width(2.0)
1857 .set_stroke_color(Color::blue())
1858 .move_to(0.0, 0.0)
1859 .line_to(50.0, 0.0)
1860 .curve_to(50.0, 25.0, 25.0, 50.0, 0.0, 50.0)
1861 .close_path()
1862 .stroke()
1863 .restore_state();
1864
1865 let ops = ctx.operations();
1866 assert!(ops.contains("q\n"));
1867 assert!(ops.contains("cm\n"));
1868 assert!(ops.contains("2.00 w\n"));
1869 assert!(ops.contains("0.000 0.000 1.000 RG\n"));
1870 assert!(ops.contains("S\n"));
1871 assert!(ops.contains("Q\n"));
1872 }
1873
1874 #[test]
1875 fn test_graphics_context_clone() {
1876 let mut ctx = GraphicsContext::new();
1877 ctx.set_fill_color(Color::red());
1878 ctx.set_stroke_color(Color::blue());
1879 ctx.set_line_width(3.0);
1880 ctx.set_opacity(0.5);
1881 ctx.rect(0.0, 0.0, 10.0, 10.0);
1882
1883 let ctx_clone = ctx.clone();
1884 assert_eq!(ctx_clone.fill_color(), Color::red());
1885 assert_eq!(ctx_clone.stroke_color(), Color::blue());
1886 assert_eq!(ctx_clone.line_width(), 3.0);
1887 assert_eq!(ctx_clone.fill_opacity(), 0.5);
1888 assert_eq!(ctx_clone.stroke_opacity(), 0.5);
1889 assert_eq!(ctx_clone.operations(), ctx.operations());
1890 }
1891
1892 #[test]
1893 fn test_set_opacity() {
1894 let mut ctx = GraphicsContext::new();
1895
1896 ctx.set_opacity(0.5);
1898 assert_eq!(ctx.fill_opacity(), 0.5);
1899 assert_eq!(ctx.stroke_opacity(), 0.5);
1900
1901 ctx.set_opacity(1.5);
1903 assert_eq!(ctx.fill_opacity(), 1.0);
1904 assert_eq!(ctx.stroke_opacity(), 1.0);
1905
1906 ctx.set_opacity(-0.5);
1907 assert_eq!(ctx.fill_opacity(), 0.0);
1908 assert_eq!(ctx.stroke_opacity(), 0.0);
1909 }
1910
1911 #[test]
1912 fn test_set_fill_opacity() {
1913 let mut ctx = GraphicsContext::new();
1914
1915 ctx.set_fill_opacity(0.3);
1916 assert_eq!(ctx.fill_opacity(), 0.3);
1917 assert_eq!(ctx.stroke_opacity(), 1.0); ctx.set_fill_opacity(2.0);
1921 assert_eq!(ctx.fill_opacity(), 1.0);
1922 }
1923
1924 #[test]
1925 fn test_set_stroke_opacity() {
1926 let mut ctx = GraphicsContext::new();
1927
1928 ctx.set_stroke_opacity(0.7);
1929 assert_eq!(ctx.stroke_opacity(), 0.7);
1930 assert_eq!(ctx.fill_opacity(), 1.0); ctx.set_stroke_opacity(-1.0);
1934 assert_eq!(ctx.stroke_opacity(), 0.0);
1935 }
1936
1937 #[test]
1938 fn test_uses_transparency() {
1939 let mut ctx = GraphicsContext::new();
1940
1941 assert!(!ctx.uses_transparency());
1943
1944 ctx.set_fill_opacity(0.5);
1946 assert!(ctx.uses_transparency());
1947
1948 ctx.set_fill_opacity(1.0);
1950 assert!(!ctx.uses_transparency());
1951 ctx.set_stroke_opacity(0.8);
1952 assert!(ctx.uses_transparency());
1953
1954 ctx.set_fill_opacity(0.5);
1956 assert!(ctx.uses_transparency());
1957 }
1958
1959 #[test]
1960 fn test_generate_graphics_state_dict() {
1961 let mut ctx = GraphicsContext::new();
1962
1963 assert_eq!(ctx.generate_graphics_state_dict(), None);
1965
1966 ctx.set_fill_opacity(0.5);
1968 let dict = ctx
1969 .generate_graphics_state_dict()
1970 .expect("Writing to string should never fail");
1971 assert!(dict.contains("/Type /ExtGState"));
1972 assert!(dict.contains("/ca 0.500"));
1973 assert!(!dict.contains("/CA"));
1974
1975 ctx.set_fill_opacity(1.0);
1977 ctx.set_stroke_opacity(0.75);
1978 let dict = ctx
1979 .generate_graphics_state_dict()
1980 .expect("Writing to string should never fail");
1981 assert!(dict.contains("/Type /ExtGState"));
1982 assert!(dict.contains("/CA 0.750"));
1983 assert!(!dict.contains("/ca"));
1984
1985 ctx.set_fill_opacity(0.25);
1987 let dict = ctx
1988 .generate_graphics_state_dict()
1989 .expect("Writing to string should never fail");
1990 assert!(dict.contains("/Type /ExtGState"));
1991 assert!(dict.contains("/ca 0.250"));
1992 assert!(dict.contains("/CA 0.750"));
1993 }
1994
1995 #[test]
1996 fn test_opacity_with_graphics_operations() {
1997 let mut ctx = GraphicsContext::new();
1998
1999 ctx.set_fill_color(Color::red())
2000 .set_opacity(0.5)
2001 .rect(10.0, 10.0, 100.0, 100.0)
2002 .fill();
2003
2004 assert_eq!(ctx.fill_opacity(), 0.5);
2005 assert_eq!(ctx.stroke_opacity(), 0.5);
2006
2007 let ops = ctx.operations();
2008 assert!(ops.contains("10.00 10.00 100.00 100.00 re"));
2009 assert!(ops.contains("1.000 0.000 0.000 rg")); assert!(ops.contains("f")); }
2012
2013 #[test]
2014 fn test_begin_end_text() {
2015 let mut ctx = GraphicsContext::new();
2016 ctx.begin_text();
2017 assert!(ctx.operations().contains("BT\n"));
2018
2019 ctx.end_text();
2020 assert!(ctx.operations().contains("ET\n"));
2021 }
2022
2023 #[test]
2024 fn test_set_font() {
2025 let mut ctx = GraphicsContext::new();
2026 ctx.set_font(Font::Helvetica, 12.0);
2027 assert!(ctx.operations().contains("/Helvetica 12 Tf\n"));
2028
2029 ctx.set_font(Font::TimesBold, 14.5);
2030 assert!(ctx.operations().contains("/Times-Bold 14.5 Tf\n"));
2031 }
2032
2033 #[test]
2034 fn test_set_text_position() {
2035 let mut ctx = GraphicsContext::new();
2036 ctx.set_text_position(100.0, 200.0);
2037 assert!(ctx.operations().contains("100.00 200.00 Td\n"));
2038 }
2039
2040 #[test]
2041 fn test_show_text() {
2042 let mut ctx = GraphicsContext::new();
2043 ctx.show_text("Hello World")
2044 .expect("Writing to string should never fail");
2045 assert!(ctx.operations().contains("(Hello World) Tj\n"));
2046 }
2047
2048 #[test]
2049 fn test_show_text_with_escaping() {
2050 let mut ctx = GraphicsContext::new();
2051 ctx.show_text("Test (parentheses)")
2052 .expect("Writing to string should never fail");
2053 assert!(ctx.operations().contains("(Test \\(parentheses\\)) Tj\n"));
2054
2055 ctx.clear();
2056 ctx.show_text("Back\\slash")
2057 .expect("Writing to string should never fail");
2058 assert!(ctx.operations().contains("(Back\\\\slash) Tj\n"));
2059
2060 ctx.clear();
2061 ctx.show_text("Line\nBreak")
2062 .expect("Writing to string should never fail");
2063 assert!(ctx.operations().contains("(Line\\nBreak) Tj\n"));
2064 }
2065
2066 #[test]
2067 fn test_text_operations_chaining() {
2068 let mut ctx = GraphicsContext::new();
2069 ctx.begin_text()
2070 .set_font(Font::Courier, 10.0)
2071 .set_text_position(50.0, 100.0)
2072 .show_text("Test")
2073 .unwrap()
2074 .end_text();
2075
2076 let ops = ctx.operations();
2077 assert!(ops.contains("BT\n"));
2078 assert!(ops.contains("/Courier 10 Tf\n"));
2079 assert!(ops.contains("50.00 100.00 Td\n"));
2080 assert!(ops.contains("(Test) Tj\n"));
2081 assert!(ops.contains("ET\n"));
2082 }
2083
2084 #[test]
2085 fn test_clip() {
2086 let mut ctx = GraphicsContext::new();
2087 ctx.clip();
2088 assert!(ctx.operations().contains("W\n"));
2089 }
2090
2091 #[test]
2092 fn test_clip_even_odd() {
2093 let mut ctx = GraphicsContext::new();
2094 ctx.clip_even_odd();
2095 assert!(ctx.operations().contains("W*\n"));
2096 }
2097
2098 #[test]
2099 fn test_clipping_with_path() {
2100 let mut ctx = GraphicsContext::new();
2101
2102 ctx.rect(10.0, 10.0, 100.0, 50.0).clip();
2104
2105 let ops = ctx.operations();
2106 assert!(ops.contains("10.00 10.00 100.00 50.00 re\n"));
2107 assert!(ops.contains("W\n"));
2108 }
2109
2110 #[test]
2111 fn test_clipping_even_odd_with_path() {
2112 let mut ctx = GraphicsContext::new();
2113
2114 ctx.move_to(0.0, 0.0)
2116 .line_to(100.0, 0.0)
2117 .line_to(100.0, 100.0)
2118 .line_to(0.0, 100.0)
2119 .close_path()
2120 .clip_even_odd();
2121
2122 let ops = ctx.operations();
2123 assert!(ops.contains("0.00 0.00 m\n"));
2124 assert!(ops.contains("100.00 0.00 l\n"));
2125 assert!(ops.contains("100.00 100.00 l\n"));
2126 assert!(ops.contains("0.00 100.00 l\n"));
2127 assert!(ops.contains("h\n"));
2128 assert!(ops.contains("W*\n"));
2129 }
2130
2131 #[test]
2132 fn test_clipping_chaining() {
2133 let mut ctx = GraphicsContext::new();
2134
2135 ctx.save_state()
2137 .rect(20.0, 20.0, 60.0, 60.0)
2138 .clip()
2139 .set_fill_color(Color::red())
2140 .rect(0.0, 0.0, 100.0, 100.0)
2141 .fill()
2142 .restore_state();
2143
2144 let ops = ctx.operations();
2145 assert!(ops.contains("q\n"));
2146 assert!(ops.contains("20.00 20.00 60.00 60.00 re\n"));
2147 assert!(ops.contains("W\n"));
2148 assert!(ops.contains("1.000 0.000 0.000 rg\n"));
2149 assert!(ops.contains("0.00 0.00 100.00 100.00 re\n"));
2150 assert!(ops.contains("f\n"));
2151 assert!(ops.contains("Q\n"));
2152 }
2153
2154 #[test]
2155 fn test_multiple_clipping_regions() {
2156 let mut ctx = GraphicsContext::new();
2157
2158 ctx.save_state()
2160 .rect(0.0, 0.0, 200.0, 200.0)
2161 .clip()
2162 .save_state()
2163 .circle(100.0, 100.0, 50.0)
2164 .clip_even_odd()
2165 .set_fill_color(Color::blue())
2166 .rect(50.0, 50.0, 100.0, 100.0)
2167 .fill()
2168 .restore_state()
2169 .restore_state();
2170
2171 let ops = ctx.operations();
2172 let q_count = ops.matches("q\n").count();
2174 let q_restore_count = ops.matches("Q\n").count();
2175 assert_eq!(q_count, 2);
2176 assert_eq!(q_restore_count, 2);
2177
2178 assert!(ops.contains("W\n"));
2180 assert!(ops.contains("W*\n"));
2181 }
2182
2183 #[test]
2186 fn test_move_to_and_line_to() {
2187 let mut ctx = GraphicsContext::new();
2188 ctx.move_to(100.0, 200.0).line_to(300.0, 400.0).stroke();
2189
2190 let ops = ctx
2191 .generate_operations()
2192 .expect("Writing to string should never fail");
2193 let ops_str = String::from_utf8_lossy(&ops);
2194 assert!(ops_str.contains("100.00 200.00 m"));
2195 assert!(ops_str.contains("300.00 400.00 l"));
2196 assert!(ops_str.contains("S"));
2197 }
2198
2199 #[test]
2200 fn test_bezier_curve() {
2201 let mut ctx = GraphicsContext::new();
2202 ctx.move_to(0.0, 0.0)
2203 .curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0)
2204 .stroke();
2205
2206 let ops = ctx
2207 .generate_operations()
2208 .expect("Writing to string should never fail");
2209 let ops_str = String::from_utf8_lossy(&ops);
2210 assert!(ops_str.contains("0.00 0.00 m"));
2211 assert!(ops_str.contains("10.00 20.00 30.00 40.00 50.00 60.00 c"));
2212 assert!(ops_str.contains("S"));
2213 }
2214
2215 #[test]
2216 fn test_circle_path() {
2217 let mut ctx = GraphicsContext::new();
2218 ctx.circle(100.0, 100.0, 50.0).fill();
2219
2220 let ops = ctx
2221 .generate_operations()
2222 .expect("Writing to string should never fail");
2223 let ops_str = String::from_utf8_lossy(&ops);
2224 assert!(ops_str.contains(" c"));
2226 assert!(ops_str.contains("f"));
2227 }
2228
2229 #[test]
2230 fn test_path_closing() {
2231 let mut ctx = GraphicsContext::new();
2232 ctx.move_to(0.0, 0.0)
2233 .line_to(100.0, 0.0)
2234 .line_to(100.0, 100.0)
2235 .close_path()
2236 .stroke();
2237
2238 let ops = ctx
2239 .generate_operations()
2240 .expect("Writing to string should never fail");
2241 let ops_str = String::from_utf8_lossy(&ops);
2242 assert!(ops_str.contains("h")); assert!(ops_str.contains("S"));
2244 }
2245
2246 #[test]
2247 fn test_fill_and_stroke() {
2248 let mut ctx = GraphicsContext::new();
2249 ctx.rect(10.0, 10.0, 50.0, 50.0).fill_stroke();
2250
2251 let ops = ctx
2252 .generate_operations()
2253 .expect("Writing to string should never fail");
2254 let ops_str = String::from_utf8_lossy(&ops);
2255 assert!(ops_str.contains("10.00 10.00 50.00 50.00 re"));
2256 assert!(ops_str.contains("B")); }
2258
2259 #[test]
2260 fn test_color_settings() {
2261 let mut ctx = GraphicsContext::new();
2262 ctx.set_fill_color(Color::rgb(1.0, 0.0, 0.0))
2263 .set_stroke_color(Color::rgb(0.0, 1.0, 0.0))
2264 .rect(10.0, 10.0, 50.0, 50.0)
2265 .fill_stroke(); assert_eq!(ctx.fill_color(), Color::rgb(1.0, 0.0, 0.0));
2268 assert_eq!(ctx.stroke_color(), Color::rgb(0.0, 1.0, 0.0));
2269
2270 let ops = ctx
2271 .generate_operations()
2272 .expect("Writing to string should never fail");
2273 let ops_str = String::from_utf8_lossy(&ops);
2274 assert!(ops_str.contains("1.000 0.000 0.000 rg")); assert!(ops_str.contains("0.000 1.000 0.000 RG")); }
2277
2278 #[test]
2279 fn test_line_styles() {
2280 let mut ctx = GraphicsContext::new();
2281 ctx.set_line_width(2.5)
2282 .set_line_cap(LineCap::Round)
2283 .set_line_join(LineJoin::Bevel);
2284
2285 assert_eq!(ctx.line_width(), 2.5);
2286
2287 let ops = ctx
2288 .generate_operations()
2289 .expect("Writing to string should never fail");
2290 let ops_str = String::from_utf8_lossy(&ops);
2291 assert!(ops_str.contains("2.50 w")); assert!(ops_str.contains("1 J")); assert!(ops_str.contains("2 j")); }
2295
2296 #[test]
2297 fn test_opacity_settings() {
2298 let mut ctx = GraphicsContext::new();
2299 ctx.set_opacity(0.5);
2300
2301 assert_eq!(ctx.fill_opacity(), 0.5);
2302 assert_eq!(ctx.stroke_opacity(), 0.5);
2303 assert!(ctx.uses_transparency());
2304
2305 ctx.set_fill_opacity(0.7).set_stroke_opacity(0.3);
2306
2307 assert_eq!(ctx.fill_opacity(), 0.7);
2308 assert_eq!(ctx.stroke_opacity(), 0.3);
2309 }
2310
2311 #[test]
2312 fn test_state_save_restore() {
2313 let mut ctx = GraphicsContext::new();
2314 ctx.save_state()
2315 .set_fill_color(Color::rgb(1.0, 0.0, 0.0))
2316 .restore_state();
2317
2318 let ops = ctx
2319 .generate_operations()
2320 .expect("Writing to string should never fail");
2321 let ops_str = String::from_utf8_lossy(&ops);
2322 assert!(ops_str.contains("q")); assert!(ops_str.contains("Q")); }
2325
2326 #[test]
2327 fn test_transformations() {
2328 let mut ctx = GraphicsContext::new();
2329 ctx.translate(100.0, 200.0).scale(2.0, 3.0).rotate(45.0);
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("1.00 0.00 0.00 1.00 100.00 200.00 cm")); assert!(ops_str.contains("2.00 0.00 0.00 3.00 0.00 0.00 cm")); assert!(ops_str.contains("cm")); }
2341
2342 #[test]
2343 fn test_custom_transform() {
2344 let mut ctx = GraphicsContext::new();
2345 ctx.transform(1.0, 0.5, 0.5, 1.0, 10.0, 20.0);
2346
2347 let ops = ctx
2348 .generate_operations()
2349 .expect("Writing to string should never fail");
2350 let ops_str = String::from_utf8_lossy(&ops);
2351 assert!(ops_str.contains("1.00 0.50 0.50 1.00 10.00 20.00 cm"));
2352 }
2353
2354 #[test]
2355 fn test_rectangle_path() {
2356 let mut ctx = GraphicsContext::new();
2357 ctx.rectangle(25.0, 25.0, 150.0, 100.0).stroke();
2358
2359 let ops = ctx
2360 .generate_operations()
2361 .expect("Writing to string should never fail");
2362 let ops_str = String::from_utf8_lossy(&ops);
2363 assert!(ops_str.contains("25.00 25.00 150.00 100.00 re"));
2364 assert!(ops_str.contains("S"));
2365 }
2366
2367 #[test]
2368 fn test_empty_operations() {
2369 let ctx = GraphicsContext::new();
2370 let ops = ctx
2371 .generate_operations()
2372 .expect("Writing to string should never fail");
2373 assert!(ops.is_empty());
2374 }
2375
2376 #[test]
2377 fn test_complex_path_operations() {
2378 let mut ctx = GraphicsContext::new();
2379 ctx.move_to(50.0, 50.0)
2380 .line_to(100.0, 50.0)
2381 .curve_to(125.0, 50.0, 150.0, 75.0, 150.0, 100.0)
2382 .line_to(150.0, 150.0)
2383 .close_path()
2384 .fill();
2385
2386 let ops = ctx
2387 .generate_operations()
2388 .expect("Writing to string should never fail");
2389 let ops_str = String::from_utf8_lossy(&ops);
2390 assert!(ops_str.contains("50.00 50.00 m"));
2391 assert!(ops_str.contains("100.00 50.00 l"));
2392 assert!(ops_str.contains("125.00 50.00 150.00 75.00 150.00 100.00 c"));
2393 assert!(ops_str.contains("150.00 150.00 l"));
2394 assert!(ops_str.contains("h"));
2395 assert!(ops_str.contains("f"));
2396 }
2397
2398 #[test]
2399 fn test_graphics_state_dict_generation() {
2400 let mut ctx = GraphicsContext::new();
2401
2402 assert!(ctx.generate_graphics_state_dict().is_none());
2404
2405 ctx.set_opacity(0.5);
2407 let dict = ctx.generate_graphics_state_dict();
2408 assert!(dict.is_some());
2409 let dict_str = dict.expect("Writing to string should never fail");
2410 assert!(dict_str.contains("/ca 0.5"));
2411 assert!(dict_str.contains("/CA 0.5"));
2412 }
2413
2414 #[test]
2415 fn test_line_dash_pattern() {
2416 let mut ctx = GraphicsContext::new();
2417 let pattern = LineDashPattern {
2418 array: vec![3.0, 2.0],
2419 phase: 0.0,
2420 };
2421 ctx.set_line_dash_pattern(pattern);
2422
2423 let ops = ctx
2424 .generate_operations()
2425 .expect("Writing to string should never fail");
2426 let ops_str = String::from_utf8_lossy(&ops);
2427 assert!(ops_str.contains("[3.00 2.00] 0.00 d"));
2428 }
2429
2430 #[test]
2431 fn test_miter_limit_setting() {
2432 let mut ctx = GraphicsContext::new();
2433 ctx.set_miter_limit(4.0);
2434
2435 let ops = ctx
2436 .generate_operations()
2437 .expect("Writing to string should never fail");
2438 let ops_str = String::from_utf8_lossy(&ops);
2439 assert!(ops_str.contains("4.00 M"));
2440 }
2441
2442 #[test]
2443 fn test_line_cap_styles() {
2444 let mut ctx = GraphicsContext::new();
2445
2446 ctx.set_line_cap(LineCap::Butt);
2447 let ops = ctx
2448 .generate_operations()
2449 .expect("Writing to string should never fail");
2450 let ops_str = String::from_utf8_lossy(&ops);
2451 assert!(ops_str.contains("0 J"));
2452
2453 let mut ctx = GraphicsContext::new();
2454 ctx.set_line_cap(LineCap::Round);
2455 let ops = ctx
2456 .generate_operations()
2457 .expect("Writing to string should never fail");
2458 let ops_str = String::from_utf8_lossy(&ops);
2459 assert!(ops_str.contains("1 J"));
2460
2461 let mut ctx = GraphicsContext::new();
2462 ctx.set_line_cap(LineCap::Square);
2463 let ops = ctx
2464 .generate_operations()
2465 .expect("Writing to string should never fail");
2466 let ops_str = String::from_utf8_lossy(&ops);
2467 assert!(ops_str.contains("2 J"));
2468 }
2469
2470 #[test]
2471 fn test_transparency_groups() {
2472 let mut ctx = GraphicsContext::new();
2473
2474 let group = TransparencyGroup::new()
2476 .with_isolated(true)
2477 .with_opacity(0.5);
2478
2479 ctx.begin_transparency_group(group);
2480 assert!(ctx.in_transparency_group());
2481
2482 ctx.rect(10.0, 10.0, 100.0, 100.0);
2484 ctx.fill();
2485
2486 ctx.end_transparency_group();
2487 assert!(!ctx.in_transparency_group());
2488
2489 let ops = ctx.operations();
2491 assert!(ops.contains("% Begin Transparency Group"));
2492 assert!(ops.contains("% End Transparency Group"));
2493 }
2494
2495 #[test]
2496 fn test_nested_transparency_groups() {
2497 let mut ctx = GraphicsContext::new();
2498
2499 let group1 = TransparencyGroup::isolated().with_opacity(0.8);
2501 ctx.begin_transparency_group(group1);
2502 assert!(ctx.in_transparency_group());
2503
2504 let group2 = TransparencyGroup::knockout().with_blend_mode(BlendMode::Multiply);
2506 ctx.begin_transparency_group(group2);
2507
2508 ctx.circle(50.0, 50.0, 25.0);
2510 ctx.fill();
2511
2512 ctx.end_transparency_group();
2514 assert!(ctx.in_transparency_group()); ctx.end_transparency_group();
2518 assert!(!ctx.in_transparency_group());
2519 }
2520
2521 #[test]
2522 fn test_line_join_styles() {
2523 let mut ctx = GraphicsContext::new();
2524
2525 ctx.set_line_join(LineJoin::Miter);
2526 let ops = ctx
2527 .generate_operations()
2528 .expect("Writing to string should never fail");
2529 let ops_str = String::from_utf8_lossy(&ops);
2530 assert!(ops_str.contains("0 j"));
2531
2532 let mut ctx = GraphicsContext::new();
2533 ctx.set_line_join(LineJoin::Round);
2534 let ops = ctx
2535 .generate_operations()
2536 .expect("Writing to string should never fail");
2537 let ops_str = String::from_utf8_lossy(&ops);
2538 assert!(ops_str.contains("1 j"));
2539
2540 let mut ctx = GraphicsContext::new();
2541 ctx.set_line_join(LineJoin::Bevel);
2542 let ops = ctx
2543 .generate_operations()
2544 .expect("Writing to string should never fail");
2545 let ops_str = String::from_utf8_lossy(&ops);
2546 assert!(ops_str.contains("2 j"));
2547 }
2548
2549 #[test]
2550 fn test_rendering_intent() {
2551 let mut ctx = GraphicsContext::new();
2552
2553 ctx.set_rendering_intent(RenderingIntent::AbsoluteColorimetric);
2554 assert_eq!(
2555 ctx.rendering_intent(),
2556 RenderingIntent::AbsoluteColorimetric
2557 );
2558
2559 ctx.set_rendering_intent(RenderingIntent::Perceptual);
2560 assert_eq!(ctx.rendering_intent(), RenderingIntent::Perceptual);
2561
2562 ctx.set_rendering_intent(RenderingIntent::Saturation);
2563 assert_eq!(ctx.rendering_intent(), RenderingIntent::Saturation);
2564 }
2565
2566 #[test]
2567 fn test_flatness_tolerance() {
2568 let mut ctx = GraphicsContext::new();
2569
2570 ctx.set_flatness(0.5);
2571 assert_eq!(ctx.flatness(), 0.5);
2572
2573 let ops = ctx
2574 .generate_operations()
2575 .expect("Writing to string should never fail");
2576 let ops_str = String::from_utf8_lossy(&ops);
2577 assert!(ops_str.contains("0.50 i"));
2578 }
2579
2580 #[test]
2581 fn test_smoothness_tolerance() {
2582 let mut ctx = GraphicsContext::new();
2583
2584 let _ = ctx.set_smoothness(0.1);
2585 assert_eq!(ctx.smoothness(), 0.1);
2586 }
2587
2588 #[test]
2589 fn test_bezier_curves() {
2590 let mut ctx = GraphicsContext::new();
2591
2592 ctx.move_to(10.0, 10.0);
2594 ctx.curve_to(20.0, 10.0, 30.0, 20.0, 30.0, 30.0);
2595
2596 let ops = ctx
2597 .generate_operations()
2598 .expect("Writing to string should never fail");
2599 let ops_str = String::from_utf8_lossy(&ops);
2600 assert!(ops_str.contains("10.00 10.00 m"));
2601 assert!(ops_str.contains("c")); }
2603
2604 #[test]
2605 fn test_clipping_path() {
2606 let mut ctx = GraphicsContext::new();
2607
2608 ctx.rectangle(10.0, 10.0, 100.0, 100.0);
2609 ctx.clip();
2610
2611 let ops = ctx
2612 .generate_operations()
2613 .expect("Writing to string should never fail");
2614 let ops_str = String::from_utf8_lossy(&ops);
2615 assert!(ops_str.contains("W"));
2616 }
2617
2618 #[test]
2619 fn test_even_odd_clipping() {
2620 let mut ctx = GraphicsContext::new();
2621
2622 ctx.rectangle(10.0, 10.0, 100.0, 100.0);
2623 ctx.clip_even_odd();
2624
2625 let ops = ctx
2626 .generate_operations()
2627 .expect("Writing to string should never fail");
2628 let ops_str = String::from_utf8_lossy(&ops);
2629 assert!(ops_str.contains("W*"));
2630 }
2631
2632 #[test]
2633 fn test_color_creation() {
2634 let gray = Color::gray(0.5);
2636 assert_eq!(gray, Color::Gray(0.5));
2637
2638 let rgb = Color::rgb(0.2, 0.4, 0.6);
2639 assert_eq!(rgb, Color::Rgb(0.2, 0.4, 0.6));
2640
2641 let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
2642 assert_eq!(cmyk, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
2643
2644 assert_eq!(Color::black(), Color::Gray(0.0));
2646 assert_eq!(Color::white(), Color::Gray(1.0));
2647 assert_eq!(Color::red(), Color::Rgb(1.0, 0.0, 0.0));
2648 }
2649
2650 #[test]
2651 fn test_extended_graphics_state() {
2652 let ctx = GraphicsContext::new();
2653
2654 let _extgstate = ExtGState::new();
2656
2657 assert!(ctx.generate_operations().is_ok());
2659 }
2660
2661 #[test]
2662 fn test_path_construction_methods() {
2663 let mut ctx = GraphicsContext::new();
2664
2665 ctx.move_to(10.0, 10.0);
2667 ctx.line_to(20.0, 20.0);
2668 ctx.curve_to(30.0, 30.0, 40.0, 40.0, 50.0, 50.0);
2669 ctx.rect(60.0, 60.0, 30.0, 30.0);
2670 ctx.circle(100.0, 100.0, 25.0);
2671 ctx.close_path();
2672
2673 let ops = ctx
2674 .generate_operations()
2675 .expect("Writing to string should never fail");
2676 assert!(!ops.is_empty());
2677 }
2678
2679 #[test]
2680 fn test_graphics_context_clone_advanced() {
2681 let mut ctx = GraphicsContext::new();
2682 ctx.set_fill_color(Color::rgb(1.0, 0.0, 0.0));
2683 ctx.set_line_width(5.0);
2684
2685 let cloned = ctx.clone();
2686 assert_eq!(cloned.fill_color(), Color::rgb(1.0, 0.0, 0.0));
2687 assert_eq!(cloned.line_width(), 5.0);
2688 }
2689
2690 #[test]
2691 fn test_basic_drawing_operations() {
2692 let mut ctx = GraphicsContext::new();
2693
2694 ctx.move_to(50.0, 50.0);
2696 ctx.line_to(100.0, 100.0);
2697 ctx.stroke();
2698
2699 let ops = ctx
2700 .generate_operations()
2701 .expect("Writing to string should never fail");
2702 let ops_str = String::from_utf8_lossy(&ops);
2703 assert!(ops_str.contains("m")); assert!(ops_str.contains("l")); assert!(ops_str.contains("S")); }
2707
2708 #[test]
2709 fn test_graphics_state_stack() {
2710 let mut ctx = GraphicsContext::new();
2711
2712 ctx.set_fill_color(Color::black());
2714
2715 ctx.save_state();
2717 ctx.set_fill_color(Color::red());
2718 assert_eq!(ctx.fill_color(), Color::red());
2719
2720 ctx.save_state();
2722 ctx.set_fill_color(Color::blue());
2723 assert_eq!(ctx.fill_color(), Color::blue());
2724
2725 ctx.restore_state();
2727 assert_eq!(ctx.fill_color(), Color::red());
2728
2729 ctx.restore_state();
2731 assert_eq!(ctx.fill_color(), Color::black());
2732 }
2733
2734 #[test]
2735 fn test_word_spacing() {
2736 let mut ctx = GraphicsContext::new();
2737 ctx.set_word_spacing(2.5);
2738
2739 let ops = ctx.generate_operations().unwrap();
2740 let ops_str = String::from_utf8_lossy(&ops);
2741 assert!(ops_str.contains("2.50 Tw"));
2742 }
2743
2744 #[test]
2745 fn test_character_spacing() {
2746 let mut ctx = GraphicsContext::new();
2747 ctx.set_character_spacing(1.0);
2748
2749 let ops = ctx.generate_operations().unwrap();
2750 let ops_str = String::from_utf8_lossy(&ops);
2751 assert!(ops_str.contains("1.00 Tc"));
2752 }
2753
2754 #[test]
2755 fn test_justified_text() {
2756 let mut ctx = GraphicsContext::new();
2757 ctx.begin_text();
2758 ctx.set_text_position(100.0, 200.0);
2759 ctx.show_justified_text("Hello world from PDF", 200.0)
2760 .unwrap();
2761 ctx.end_text();
2762
2763 let ops = ctx.generate_operations().unwrap();
2764 let ops_str = String::from_utf8_lossy(&ops);
2765
2766 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")); }
2775
2776 #[test]
2777 fn test_justified_text_single_word() {
2778 let mut ctx = GraphicsContext::new();
2779 ctx.begin_text();
2780 ctx.show_justified_text("Hello", 200.0).unwrap();
2781 ctx.end_text();
2782
2783 let ops = ctx.generate_operations().unwrap();
2784 let ops_str = String::from_utf8_lossy(&ops);
2785
2786 assert!(ops_str.contains("(Hello) Tj"));
2788 assert_eq!(ops_str.matches("Tw").count(), 0);
2790 }
2791
2792 #[test]
2793 fn test_text_width_estimation() {
2794 let ctx = GraphicsContext::new();
2795 let width = ctx.estimate_text_width_simple("Hello");
2796
2797 assert!(width > 0.0);
2799 assert_eq!(width, 5.0 * 12.0 * 0.6); }
2801
2802 #[test]
2803 fn test_set_alpha_methods() {
2804 let mut ctx = GraphicsContext::new();
2805
2806 assert!(ctx.set_alpha(0.5).is_ok());
2808 assert!(ctx.set_alpha_fill(0.3).is_ok());
2809 assert!(ctx.set_alpha_stroke(0.7).is_ok());
2810
2811 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
2819 .set_alpha(0.5)
2820 .and_then(|c| c.set_alpha_fill(0.3))
2821 .and_then(|c| c.set_alpha_stroke(0.7));
2822 assert!(result.is_ok());
2823 }
2824
2825 #[test]
2826 fn test_alpha_methods_generate_extgstate() {
2827 let mut ctx = GraphicsContext::new();
2828
2829 ctx.set_alpha(0.5).unwrap();
2831
2832 ctx.rect(10.0, 10.0, 50.0, 50.0).fill();
2834
2835 let ops = ctx.generate_operations().unwrap();
2836 let ops_str = String::from_utf8_lossy(&ops);
2837
2838 assert!(ops_str.contains("/GS")); assert!(ops_str.contains(" gs\n")); ctx.clear();
2844 ctx.set_alpha_fill(0.3).unwrap();
2845 ctx.set_alpha_stroke(0.8).unwrap();
2846 ctx.rect(20.0, 20.0, 60.0, 60.0).fill_stroke();
2847
2848 let ops2 = ctx.generate_operations().unwrap();
2849 let ops_str2 = String::from_utf8_lossy(&ops2);
2850
2851 assert!(ops_str2.contains("/GS")); assert!(ops_str2.contains(" gs\n")); }
2855
2856 #[test]
2857 fn test_add_command() {
2858 let mut ctx = GraphicsContext::new();
2859
2860 ctx.add_command("1 0 0 1 100 200 cm");
2862 let ops = ctx.operations();
2863 assert!(ops.contains("1 0 0 1 100 200 cm\n"));
2864
2865 ctx.clear();
2867 ctx.add_command("q");
2868 assert_eq!(ctx.operations(), "q\n");
2869
2870 ctx.clear();
2872 ctx.add_command("");
2873 assert_eq!(ctx.operations(), "\n");
2874
2875 ctx.clear();
2877 ctx.add_command("Q\n");
2878 assert_eq!(ctx.operations(), "Q\n\n"); ctx.clear();
2882 ctx.add_command("q");
2883 ctx.add_command("1 0 0 1 50 50 cm");
2884 ctx.add_command("Q");
2885 assert_eq!(ctx.operations(), "q\n1 0 0 1 50 50 cm\nQ\n");
2886 }
2887
2888 #[test]
2889 fn test_get_operations() {
2890 let mut ctx = GraphicsContext::new();
2891 ctx.rect(10.0, 10.0, 50.0, 50.0);
2892 let ops1 = ctx.operations();
2893 let ops2 = ctx.get_operations();
2894 assert_eq!(ops1, ops2);
2895 }
2896
2897 #[test]
2898 fn test_set_line_solid() {
2899 let mut ctx = GraphicsContext::new();
2900 ctx.set_line_dash_pattern(LineDashPattern::new(vec![5.0, 3.0], 0.0));
2901 ctx.set_line_solid();
2902 let ops = ctx.operations();
2903 assert!(ops.contains("[] 0 d\n"));
2904 }
2905
2906 #[test]
2907 fn test_set_custom_font() {
2908 let mut ctx = GraphicsContext::new();
2909 ctx.set_custom_font("CustomFont", 14.0);
2910 assert_eq!(ctx.current_font_name.as_deref(), Some("CustomFont"));
2911 assert_eq!(ctx.current_font_size, 14.0);
2912 assert!(ctx.is_custom_font);
2913 }
2914
2915 #[test]
2916 fn test_show_text_standard_font_uses_literal_string() {
2917 let mut ctx = GraphicsContext::new();
2918 ctx.set_font(Font::Helvetica, 12.0);
2919 assert!(!ctx.is_custom_font);
2920
2921 ctx.begin_text();
2922 ctx.set_text_position(10.0, 20.0);
2923 ctx.show_text("Hello World").unwrap();
2924 ctx.end_text();
2925
2926 let ops = ctx.operations();
2927 assert!(ops.contains("(Hello World) Tj"));
2928 assert!(!ops.contains("<"));
2929 }
2930
2931 #[test]
2932 fn test_show_text_custom_font_uses_hex_encoding() {
2933 let mut ctx = GraphicsContext::new();
2934 ctx.set_font(Font::Custom("NotoSansCJK".to_string()), 12.0);
2935 assert!(ctx.is_custom_font);
2936
2937 ctx.begin_text();
2938 ctx.set_text_position(10.0, 20.0);
2939 ctx.show_text("你好").unwrap();
2941 ctx.end_text();
2942
2943 let ops = ctx.operations();
2944 assert!(
2946 ops.contains("<4F60597D> Tj"),
2947 "Expected hex encoding for CJK text, got: {}",
2948 ops
2949 );
2950 assert!(!ops.contains("(你好)"));
2951 }
2952
2953 #[test]
2954 fn test_show_text_custom_font_ascii_still_hex() {
2955 let mut ctx = GraphicsContext::new();
2956 ctx.set_font(Font::Custom("MyFont".to_string()), 10.0);
2957
2958 ctx.begin_text();
2959 ctx.set_text_position(0.0, 0.0);
2960 ctx.show_text("AB").unwrap();
2962 ctx.end_text();
2963
2964 let ops = ctx.operations();
2965 assert!(
2967 ops.contains("<00410042> Tj"),
2968 "Expected hex encoding for ASCII in custom font, got: {}",
2969 ops
2970 );
2971 }
2972
2973 #[test]
2974 fn test_show_text_tracks_used_characters() {
2975 let mut ctx = GraphicsContext::new();
2976 ctx.set_font(Font::Custom("CJKFont".to_string()), 12.0);
2977
2978 ctx.begin_text();
2979 ctx.show_text("你好A").unwrap();
2980 ctx.end_text();
2981
2982 let chars = ctx
2983 .get_used_characters()
2984 .expect("show_text with a custom font must record characters");
2985 assert!(chars.contains(&'你'));
2986 assert!(chars.contains(&'好'));
2987 assert!(chars.contains(&'A'));
2988 }
2989
2990 #[test]
2991 fn test_is_custom_font_toggles_correctly() {
2992 let mut ctx = GraphicsContext::new();
2993 assert!(!ctx.is_custom_font);
2994
2995 ctx.set_font(Font::Custom("CJK".to_string()), 12.0);
2996 assert!(ctx.is_custom_font);
2997
2998 ctx.set_font(Font::Helvetica, 12.0);
2999 assert!(!ctx.is_custom_font);
3000
3001 ctx.set_custom_font("AnotherCJK", 14.0);
3002 assert!(ctx.is_custom_font);
3003
3004 ctx.set_font(Font::CourierBold, 10.0);
3005 assert!(!ctx.is_custom_font);
3006 }
3007
3008 #[test]
3009 fn test_set_glyph_mapping() {
3010 let mut ctx = GraphicsContext::new();
3011
3012 assert!(ctx.glyph_mapping.is_none());
3014
3015 let mut mapping = HashMap::new();
3017 mapping.insert(65u32, 1u16); mapping.insert(66u32, 2u16); ctx.set_glyph_mapping(mapping.clone());
3020 assert!(ctx.glyph_mapping.is_some());
3021 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().len(), 2);
3022 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().get(&65), Some(&1));
3023 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().get(&66), Some(&2));
3024
3025 ctx.set_glyph_mapping(HashMap::new());
3027 assert!(ctx.glyph_mapping.is_some());
3028 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().len(), 0);
3029
3030 let mut new_mapping = HashMap::new();
3032 new_mapping.insert(67u32, 3u16); ctx.set_glyph_mapping(new_mapping);
3034 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().len(), 1);
3035 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().get(&67), Some(&3));
3036 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().get(&65), None); }
3038
3039 #[test]
3040 fn test_draw_text_basic() {
3041 let mut ctx = GraphicsContext::new();
3042 ctx.set_font(Font::Helvetica, 12.0);
3043
3044 let result = ctx.draw_text("Hello", 100.0, 200.0);
3045 assert!(result.is_ok());
3046
3047 let ops = ctx.operations();
3048 assert!(ops.contains("BT\n"));
3050 assert!(ops.contains("ET\n"));
3051
3052 assert!(ops.contains("/Helvetica"));
3054 assert!(ops.contains("12"));
3055 assert!(ops.contains("Tf\n"));
3056
3057 assert!(ops.contains("100"));
3059 assert!(ops.contains("200"));
3060 assert!(ops.contains("Td\n"));
3061
3062 assert!(ops.contains("(Hello)") || ops.contains("<48656c6c6f>")); }
3065
3066 #[test]
3067 fn test_draw_text_with_special_characters() {
3068 let mut ctx = GraphicsContext::new();
3069 ctx.set_font(Font::Helvetica, 12.0);
3070
3071 let result = ctx.draw_text("Test (with) parens", 50.0, 100.0);
3073 assert!(result.is_ok());
3074
3075 let ops = ctx.operations();
3076 assert!(ops.contains("\\(") || ops.contains("\\)") || ops.contains("<"));
3078 }
3080
3081 #[test]
3082 fn test_draw_text_unicode_detection() {
3083 let mut ctx = GraphicsContext::new();
3084 ctx.set_font(Font::Helvetica, 12.0);
3085
3086 ctx.draw_text("ASCII", 0.0, 0.0).unwrap();
3088 let _ops_ascii = ctx.operations();
3089
3090 ctx.clear();
3091
3092 ctx.set_font(Font::Helvetica, 12.0);
3094 ctx.draw_text("中文", 0.0, 0.0).unwrap();
3095 let ops_unicode = ctx.operations();
3096
3097 assert!(ops_unicode.contains("<") && ops_unicode.contains(">"));
3099 }
3100
3101 #[test]
3102 #[allow(deprecated)]
3103 fn test_draw_text_hex_encoding() {
3104 let mut ctx = GraphicsContext::new();
3105 ctx.set_font(Font::Helvetica, 12.0);
3106 let result = ctx.draw_text_hex("Test", 50.0, 100.0);
3107 assert!(result.is_ok());
3108 let ops = ctx.operations();
3109 assert!(ops.contains("<"));
3110 assert!(ops.contains(">"));
3111 }
3112
3113 #[test]
3114 #[allow(deprecated)]
3115 fn test_draw_text_cid() {
3116 let mut ctx = GraphicsContext::new();
3117 ctx.set_custom_font("CustomCIDFont", 12.0);
3118 let result = ctx.draw_text_cid("Test", 50.0, 100.0);
3119 assert!(result.is_ok());
3120 let ops = ctx.operations();
3121 assert!(ops.contains("BT\n"));
3122 assert!(ops.contains("ET\n"));
3123 }
3124
3125 #[test]
3126 #[allow(deprecated)]
3127 fn test_draw_text_unicode() {
3128 let mut ctx = GraphicsContext::new();
3129 ctx.set_custom_font("UnicodeFont", 12.0);
3130 let result = ctx.draw_text_unicode("Test \u{4E2D}\u{6587}", 50.0, 100.0);
3131 assert!(result.is_ok());
3132 let ops = ctx.operations();
3133 assert!(ops.contains("BT\n"));
3134 assert!(ops.contains("ET\n"));
3135 }
3136
3137 #[test]
3138 fn test_begin_end_transparency_group() {
3139 let mut ctx = GraphicsContext::new();
3140
3141 assert!(!ctx.in_transparency_group());
3143 assert!(ctx.current_transparency_group().is_none());
3144
3145 let group = TransparencyGroup::new();
3147 ctx.begin_transparency_group(group);
3148 assert!(ctx.in_transparency_group());
3149 assert!(ctx.current_transparency_group().is_some());
3150
3151 let ops = ctx.operations();
3153 assert!(ops.contains("% Begin Transparency Group"));
3154
3155 ctx.end_transparency_group();
3157 assert!(!ctx.in_transparency_group());
3158 assert!(ctx.current_transparency_group().is_none());
3159
3160 let ops_after = ctx.operations();
3162 assert!(ops_after.contains("% End Transparency Group"));
3163 }
3164
3165 #[test]
3166 fn test_transparency_group_nesting() {
3167 let mut ctx = GraphicsContext::new();
3168
3169 let group1 = TransparencyGroup::new();
3171 let group2 = TransparencyGroup::new();
3172 let group3 = TransparencyGroup::new();
3173
3174 ctx.begin_transparency_group(group1);
3175 assert_eq!(ctx.transparency_stack.len(), 1);
3176
3177 ctx.begin_transparency_group(group2);
3178 assert_eq!(ctx.transparency_stack.len(), 2);
3179
3180 ctx.begin_transparency_group(group3);
3181 assert_eq!(ctx.transparency_stack.len(), 3);
3182
3183 ctx.end_transparency_group();
3185 assert_eq!(ctx.transparency_stack.len(), 2);
3186
3187 ctx.end_transparency_group();
3188 assert_eq!(ctx.transparency_stack.len(), 1);
3189
3190 ctx.end_transparency_group();
3191 assert_eq!(ctx.transparency_stack.len(), 0);
3192 assert!(!ctx.in_transparency_group());
3193 }
3194
3195 #[test]
3196 fn test_transparency_group_without_begin() {
3197 let mut ctx = GraphicsContext::new();
3198
3199 assert!(!ctx.in_transparency_group());
3201 ctx.end_transparency_group();
3202 assert!(!ctx.in_transparency_group());
3203 }
3204
3205 #[test]
3206 fn test_extgstate_manager_access() {
3207 let ctx = GraphicsContext::new();
3208 let manager = ctx.extgstate_manager();
3209 assert_eq!(manager.count(), 0);
3210 }
3211
3212 #[test]
3213 fn test_extgstate_manager_mut_access() {
3214 let mut ctx = GraphicsContext::new();
3215 let manager = ctx.extgstate_manager_mut();
3216 assert_eq!(manager.count(), 0);
3217 }
3218
3219 #[test]
3220 fn test_has_extgstates() {
3221 let mut ctx = GraphicsContext::new();
3222
3223 assert!(!ctx.has_extgstates());
3225 assert_eq!(ctx.extgstate_manager().count(), 0);
3226
3227 ctx.set_alpha(0.5).unwrap();
3229 ctx.rect(10.0, 10.0, 50.0, 50.0).fill();
3230 let result = ctx.generate_operations().unwrap();
3231
3232 assert!(ctx.has_extgstates());
3233 assert!(ctx.extgstate_manager().count() > 0);
3234
3235 let output = String::from_utf8_lossy(&result);
3237 assert!(output.contains("/GS")); assert!(output.contains(" gs\n")); }
3240
3241 #[test]
3242 fn test_generate_extgstate_resources() {
3243 let mut ctx = GraphicsContext::new();
3244 ctx.set_alpha(0.5).unwrap();
3245 ctx.rect(10.0, 10.0, 50.0, 50.0).fill();
3246 ctx.generate_operations().unwrap();
3247
3248 let resources = ctx.generate_extgstate_resources();
3249 assert!(resources.is_ok());
3250 }
3251
3252 #[test]
3253 fn test_apply_extgstate() {
3254 let mut ctx = GraphicsContext::new();
3255
3256 let mut state = ExtGState::new();
3258 state.alpha_fill = Some(0.5);
3259 state.alpha_stroke = Some(0.8);
3260 state.blend_mode = Some(BlendMode::Multiply);
3261
3262 let result = ctx.apply_extgstate(state);
3263 assert!(result.is_ok());
3264
3265 assert!(ctx.has_extgstates());
3267 assert_eq!(ctx.extgstate_manager().count(), 1);
3268
3269 let mut state2 = ExtGState::new();
3271 state2.alpha_fill = Some(0.3);
3272 ctx.apply_extgstate(state2).unwrap();
3273
3274 assert_eq!(ctx.extgstate_manager().count(), 2);
3276 }
3277
3278 #[test]
3279 fn test_with_extgstate() {
3280 let mut ctx = GraphicsContext::new();
3281 let result = ctx.with_extgstate(|mut state| {
3282 state.alpha_fill = Some(0.5);
3283 state.alpha_stroke = Some(0.8);
3284 state
3285 });
3286 assert!(result.is_ok());
3287 }
3288
3289 #[test]
3290 fn test_set_blend_mode() {
3291 let mut ctx = GraphicsContext::new();
3292
3293 let result = ctx.set_blend_mode(BlendMode::Multiply);
3295 assert!(result.is_ok());
3296 assert!(ctx.has_extgstates());
3297
3298 ctx.clear();
3300 ctx.set_blend_mode(BlendMode::Screen).unwrap();
3301 ctx.rect(0.0, 0.0, 10.0, 10.0).fill();
3302 let ops = ctx.generate_operations().unwrap();
3303 let output = String::from_utf8_lossy(&ops);
3304
3305 assert!(output.contains("/GS"));
3307 assert!(output.contains(" gs\n"));
3308 }
3309
3310 #[test]
3311 fn test_render_table() {
3312 let mut ctx = GraphicsContext::new();
3313 let table = Table::with_equal_columns(2, 200.0);
3314 let result = ctx.render_table(&table);
3315 assert!(result.is_ok());
3316 }
3317
3318 #[test]
3319 fn test_render_list() {
3320 let mut ctx = GraphicsContext::new();
3321 use crate::text::{OrderedList, OrderedListStyle};
3322 let ordered = OrderedList::new(OrderedListStyle::Decimal);
3323 let list = ListElement::Ordered(ordered);
3324 let result = ctx.render_list(&list);
3325 assert!(result.is_ok());
3326 }
3327
3328 #[test]
3329 fn test_render_column_layout() {
3330 let mut ctx = GraphicsContext::new();
3331 use crate::text::ColumnContent;
3332 let layout = ColumnLayout::new(2, 100.0, 200.0);
3333 let content = ColumnContent::new("Test content");
3334 let result = ctx.render_column_layout(&layout, &content, 50.0, 50.0, 400.0);
3335 assert!(result.is_ok());
3336 }
3337
3338 #[test]
3339 fn test_clip_ellipse() {
3340 let mut ctx = GraphicsContext::new();
3341
3342 assert!(!ctx.has_clipping());
3344 assert!(ctx.clipping_path().is_none());
3345
3346 let result = ctx.clip_ellipse(100.0, 100.0, 50.0, 30.0);
3348 assert!(result.is_ok());
3349 assert!(ctx.has_clipping());
3350 assert!(ctx.clipping_path().is_some());
3351
3352 let ops = ctx.operations();
3354 assert!(ops.contains("W\n") || ops.contains("W*\n")); ctx.clear_clipping();
3358 assert!(!ctx.has_clipping());
3359 }
3360
3361 #[test]
3362 fn test_clipping_path_access() {
3363 let mut ctx = GraphicsContext::new();
3364
3365 assert!(ctx.clipping_path().is_none());
3367
3368 ctx.clip_rect(10.0, 10.0, 50.0, 50.0).unwrap();
3370 assert!(ctx.clipping_path().is_some());
3371
3372 ctx.clip_circle(100.0, 100.0, 25.0).unwrap();
3374 assert!(ctx.clipping_path().is_some());
3375
3376 ctx.save_state();
3378 ctx.clear_clipping();
3379 assert!(!ctx.has_clipping());
3380
3381 ctx.restore_state();
3382 assert!(ctx.has_clipping());
3384 }
3385
3386 #[test]
3389 fn test_edge_case_move_to_negative() {
3390 let mut ctx = GraphicsContext::new();
3391 ctx.move_to(-100.5, -200.25);
3392 assert!(ctx.operations().contains("-100.50 -200.25 m\n"));
3393 }
3394
3395 #[test]
3396 fn test_edge_case_opacity_out_of_range() {
3397 let mut ctx = GraphicsContext::new();
3398
3399 let _ = ctx.set_opacity(2.5);
3401 assert_eq!(ctx.fill_opacity(), 1.0);
3402
3403 let _ = ctx.set_opacity(-0.5);
3405 assert_eq!(ctx.fill_opacity(), 0.0);
3406 }
3407
3408 #[test]
3409 fn test_edge_case_line_width_extremes() {
3410 let mut ctx = GraphicsContext::new();
3411
3412 ctx.set_line_width(0.0);
3413 assert_eq!(ctx.line_width(), 0.0);
3414
3415 ctx.set_line_width(10000.0);
3416 assert_eq!(ctx.line_width(), 10000.0);
3417 }
3418
3419 #[test]
3422 fn test_interaction_transparency_plus_clipping() {
3423 let mut ctx = GraphicsContext::new();
3424
3425 ctx.set_alpha(0.5).unwrap();
3426 ctx.clip_rect(10.0, 10.0, 100.0, 100.0).unwrap();
3427 ctx.rect(20.0, 20.0, 80.0, 80.0).fill();
3428
3429 let ops = ctx.generate_operations().unwrap();
3430 let output = String::from_utf8_lossy(&ops);
3431
3432 assert!(output.contains("W\n") || output.contains("W*\n"));
3434 assert!(output.contains("/GS"));
3435 }
3436
3437 #[test]
3438 fn test_interaction_extgstate_plus_text() {
3439 let mut ctx = GraphicsContext::new();
3440
3441 let mut state = ExtGState::new();
3442 state.alpha_fill = Some(0.7);
3443 ctx.apply_extgstate(state).unwrap();
3444
3445 ctx.set_font(Font::Helvetica, 14.0);
3446 ctx.draw_text("Test", 100.0, 200.0).unwrap();
3447
3448 let ops = ctx.generate_operations().unwrap();
3449 let output = String::from_utf8_lossy(&ops);
3450
3451 assert!(output.contains("/GS"));
3452 assert!(output.contains("BT\n"));
3453 }
3454
3455 #[test]
3456 fn test_interaction_chained_transformations() {
3457 let mut ctx = GraphicsContext::new();
3458
3459 ctx.translate(50.0, 100.0);
3460 ctx.rotate(45.0);
3461 ctx.scale(2.0, 2.0);
3462
3463 let ops = ctx.operations();
3464 assert_eq!(ops.matches("cm\n").count(), 3);
3465 }
3466
3467 #[test]
3470 fn test_e2e_complete_page_with_header() {
3471 use crate::{Document, Page};
3472
3473 let mut doc = Document::new();
3474 let mut page = Page::a4();
3475 let ctx = page.graphics();
3476
3477 ctx.save_state();
3479 let _ = ctx.set_fill_opacity(0.3);
3480 ctx.set_fill_color(Color::rgb(200.0, 200.0, 255.0));
3481 ctx.rect(0.0, 750.0, 595.0, 42.0).fill();
3482 ctx.restore_state();
3483
3484 ctx.save_state();
3486 ctx.clip_rect(50.0, 50.0, 495.0, 692.0).unwrap();
3487 ctx.rect(60.0, 60.0, 100.0, 100.0).fill();
3488 ctx.restore_state();
3489
3490 let ops = ctx.generate_operations().unwrap();
3491 let output = String::from_utf8_lossy(&ops);
3492
3493 assert!(output.contains("q\n"));
3494 assert!(output.contains("Q\n"));
3495 assert!(output.contains("f\n"));
3496
3497 doc.add_page(page);
3498 assert!(doc.to_bytes().unwrap().len() > 0);
3499 }
3500
3501 #[test]
3502 fn test_e2e_watermark_workflow() {
3503 let mut ctx = GraphicsContext::new();
3504
3505 ctx.save_state();
3506 let _ = ctx.set_fill_opacity(0.2);
3507 ctx.translate(300.0, 400.0);
3508 ctx.rotate(45.0);
3509 ctx.set_font(Font::HelveticaBold, 72.0);
3510 ctx.draw_text("DRAFT", 0.0, 0.0).unwrap();
3511 ctx.restore_state();
3512
3513 let ops = ctx.generate_operations().unwrap();
3514 let output = String::from_utf8_lossy(&ops);
3515
3516 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")); }
3523
3524 #[test]
3527 fn test_set_custom_font_emits_tf_operator() {
3528 let mut ctx = GraphicsContext::new();
3529 ctx.set_custom_font("NotoSansCJK", 14.0);
3530
3531 let ops = ctx.operations();
3532 assert!(
3533 ops.contains("/NotoSansCJK 14 Tf"),
3534 "set_custom_font should emit Tf operator, got: {}",
3535 ops
3536 );
3537 }
3538
3539 #[test]
3542 fn test_draw_text_uses_is_custom_font_flag() {
3543 let mut ctx = GraphicsContext::new();
3544 ctx.set_custom_font("Helvetica", 12.0);
3546 ctx.clear(); ctx.draw_text("A", 10.0, 20.0).unwrap();
3549 let ops = ctx.operations();
3550 assert!(
3552 ops.contains("<0041> Tj"),
3553 "draw_text with is_custom_font=true should use hex, got: {}",
3554 ops
3555 );
3556 }
3557
3558 #[test]
3559 fn test_draw_text_standard_font_uses_literal() {
3560 let mut ctx = GraphicsContext::new();
3561 ctx.set_font(Font::Helvetica, 12.0);
3562 ctx.clear();
3563
3564 ctx.draw_text("Hello", 10.0, 20.0).unwrap();
3565 let ops = ctx.operations();
3566 assert!(
3567 ops.contains("(Hello) Tj"),
3568 "draw_text with standard font should use literal, got: {}",
3569 ops
3570 );
3571 }
3572
3573 #[test]
3576 fn test_show_text_smp_character_uses_surrogate_pairs() {
3577 let mut ctx = GraphicsContext::new();
3578 ctx.set_font(Font::Custom("Emoji".to_string()), 12.0);
3579
3580 ctx.begin_text();
3581 ctx.set_text_position(0.0, 0.0);
3582 ctx.show_text("\u{1F600}").unwrap();
3584 ctx.end_text();
3585
3586 let ops = ctx.operations();
3587 assert!(
3588 ops.contains("<D83DDE00> Tj"),
3589 "SMP character should use UTF-16BE surrogate pair, got: {}",
3590 ops
3591 );
3592 assert!(
3593 !ops.contains("FFFD"),
3594 "SMP character must NOT be replaced with FFFD"
3595 );
3596 }
3597
3598 #[test]
3601 fn test_save_restore_preserves_font_state() {
3602 let mut ctx = GraphicsContext::new();
3603 ctx.set_font(Font::Custom("CJK".to_string()), 12.0);
3604 assert!(ctx.is_custom_font);
3605 assert_eq!(ctx.current_font_name.as_deref(), Some("CJK"));
3606 assert_eq!(ctx.current_font_size, 12.0);
3607
3608 ctx.save_state();
3609 ctx.set_font(Font::Helvetica, 10.0);
3610 assert!(!ctx.is_custom_font);
3611 assert_eq!(ctx.current_font_name.as_deref(), Some("Helvetica"));
3612
3613 ctx.restore_state();
3614 assert!(
3615 ctx.is_custom_font,
3616 "is_custom_font must be restored after restore_state"
3617 );
3618 assert_eq!(ctx.current_font_name.as_deref(), Some("CJK"));
3619 assert_eq!(ctx.current_font_size, 12.0);
3620 }
3621
3622 #[test]
3623 fn test_save_restore_mixed_font_encoding() {
3624 let mut ctx = GraphicsContext::new();
3625 ctx.set_font(Font::Custom("CJK".to_string()), 12.0);
3626
3627 ctx.save_state();
3629 ctx.set_font(Font::Helvetica, 10.0);
3630 ctx.begin_text();
3631 ctx.show_text("Hello").unwrap();
3632 ctx.end_text();
3633 ctx.restore_state();
3634
3635 ctx.begin_text();
3637 ctx.show_text("你好").unwrap();
3638 ctx.end_text();
3639
3640 let ops = ctx.operations();
3641 assert!(
3643 ops.contains("<4F60597D> Tj"),
3644 "After restore_state, CJK text should use hex encoding, got: {}",
3645 ops
3646 );
3647 }
3648
3649 #[test]
3650 fn test_graphics_state_arc_str_save_restore() {
3651 let mut ctx = GraphicsContext::new();
3654
3655 ctx.set_font(Font::Custom("TestFont".to_string()), 14.0);
3657 assert_eq!(ctx.current_font_name.as_deref(), Some("TestFont"));
3658 assert!(ctx.is_custom_font);
3659
3660 ctx.save_state();
3662 ctx.set_font(Font::Custom("Other".to_string()), 10.0);
3663 assert_eq!(ctx.current_font_name.as_deref(), Some("Other"));
3664
3665 ctx.restore_state();
3667 assert_eq!(
3668 ctx.current_font_name.as_deref(),
3669 Some("TestFont"),
3670 "Font name must be restored to TestFont after restore_state"
3671 );
3672 assert_eq!(ctx.current_font_size, 14.0);
3673 assert!(
3674 ctx.is_custom_font,
3675 "is_custom_font must be restored to true"
3676 );
3677
3678 if let Some(ref arc) = ctx.current_font_name {
3680 let cloned = arc.clone();
3681 assert_eq!(arc.as_ref(), cloned.as_ref());
3682 assert!(Arc::ptr_eq(arc, &cloned));
3684 }
3685 }
3686
3687 #[test]
3695 fn nan_line_width_sanitised_at_emission() {
3696 let mut ctx = GraphicsContext::new();
3697 ctx.set_line_width(f64::NAN);
3698 let ops = ctx.operations();
3699 assert!(
3700 ops.contains("0.00 w\n"),
3701 "NaN line width must emit `0.00 w`, got: {ops:?}"
3702 );
3703 assert!(
3704 !ops.contains("NaN"),
3705 "`NaN` must not appear in any content-stream output, got: {ops:?}"
3706 );
3707 }
3708
3709 #[test]
3710 fn pos_inf_line_width_sanitised_at_emission() {
3711 let mut ctx = GraphicsContext::new();
3712 ctx.set_line_width(f64::INFINITY);
3713 let ops = ctx.operations();
3714 assert!(
3715 ops.contains("0.00 w\n"),
3716 "+inf line width must emit `0.00 w`, got: {ops:?}"
3717 );
3718 assert!(
3719 !ops.contains("inf"),
3720 "`inf` must not appear in any content-stream output, got: {ops:?}"
3721 );
3722 }
3723
3724 #[test]
3725 fn nan_path_coords_sanitised_at_emission() {
3726 let mut ctx = GraphicsContext::new();
3727 ctx.move_to(f64::NAN, 20.0);
3728 ctx.line_to(30.0, f64::NEG_INFINITY);
3729 ctx.rect(f64::NAN, f64::INFINITY, 100.0, f64::NEG_INFINITY);
3730 let ops = ctx.operations();
3731 assert!(
3732 !ops.contains("NaN") && !ops.contains("inf"),
3733 "non-finite floats must not appear in path operators, got: {ops:?}"
3734 );
3735 assert!(
3736 ops.contains("0.00 20.00 m\n"),
3737 "NaN x must clamp to 0.00 in `m` op, got: {ops:?}"
3738 );
3739 assert!(
3740 ops.contains("30.00 0.00 l\n"),
3741 "-inf y must clamp to 0.00 in `l` op, got: {ops:?}"
3742 );
3743 assert!(
3744 ops.contains("0.00 0.00 100.00 0.00 re\n"),
3745 "non-finite components must clamp to 0.00 in `re` op, got: {ops:?}"
3746 );
3747 }
3748
3749 fn expected_components(values: &[f64], op: &str) -> String {
3762 let mut s = String::new();
3763 for v in values {
3764 s.push_str(&format!("{v:.4} "));
3765 }
3766 s.push_str(op);
3767 s.push('\n');
3768 s
3769 }
3770
3771 #[test]
3772 fn set_fill_color_icc_emits_named_cs_then_components() {
3773 let mut ctx = GraphicsContext::new();
3774 ctx.set_fill_color_icc("ICCRGB1", vec![0.25, 0.5, 0.75]);
3775 let expected = format!(
3776 "/ICCRGB1 cs\n{}",
3777 expected_components(&[0.25, 0.5, 0.75], "sc")
3778 );
3779 assert_eq!(
3780 ctx.operations(),
3781 expected,
3782 "ICC fill must emit `/ICCRGB1 cs` then `0.2500 0.5000 0.7500 sc`"
3783 );
3784 }
3785
3786 #[test]
3787 fn set_stroke_color_icc_emits_named_cs_then_components() {
3788 let mut ctx = GraphicsContext::new();
3789 ctx.set_stroke_color_icc("ICCRGB1", vec![0.25, 0.5, 0.75]);
3790 let expected = format!(
3791 "/ICCRGB1 CS\n{}",
3792 expected_components(&[0.25, 0.5, 0.75], "SC")
3793 );
3794 assert_eq!(
3795 ctx.operations(),
3796 expected,
3797 "ICC stroke must emit `/ICCRGB1 CS` then `0.2500 0.5000 0.7500 SC`"
3798 );
3799 }
3800
3801 #[test]
3802 fn set_fill_color_icc_single_channel_gray() {
3803 let mut ctx = GraphicsContext::new();
3804 ctx.set_fill_color_icc("ICCGray1", vec![0.42]);
3805 let expected = format!("/ICCGray1 cs\n{}", expected_components(&[0.42], "sc"));
3806 assert_eq!(ctx.operations(), expected);
3807 }
3808
3809 #[test]
3810 fn set_fill_color_icc_cmyk_four_channels() {
3811 let mut ctx = GraphicsContext::new();
3812 ctx.set_fill_color_icc("ICCCmyk1", vec![0.1, 0.2, 0.3, 0.4]);
3813 let expected = format!(
3814 "/ICCCmyk1 cs\n{}",
3815 expected_components(&[0.1, 0.2, 0.3, 0.4], "sc")
3816 );
3817 assert_eq!(ctx.operations(), expected);
3818 }
3819
3820 #[test]
3821 fn set_fill_color_calibrated_named_uses_caller_name() {
3822 let color = CalibratedColor::cal_rgb([0.1, 0.2, 0.3], CalRgbColorSpace::new());
3823 let mut ctx = GraphicsContext::new();
3824 ctx.set_fill_color_calibrated_named("MyCalRgb", color.clone());
3825 let expected = format!(
3826 "/MyCalRgb cs\n{}",
3827 expected_components(&color.values(), "sc")
3828 );
3829 assert_eq!(ctx.operations(), expected);
3830 }
3831
3832 #[test]
3833 fn set_stroke_color_calibrated_named_uses_caller_name() {
3834 let color = CalibratedColor::cal_gray(0.6, CalGrayColorSpace::new());
3835 let mut ctx = GraphicsContext::new();
3836 ctx.set_stroke_color_calibrated_named("MyCalGray", color.clone());
3837 let expected = format!(
3838 "/MyCalGray CS\n{}",
3839 expected_components(&color.values(), "SC")
3840 );
3841 assert_eq!(ctx.operations(), expected);
3842 }
3843
3844 #[test]
3845 fn set_fill_color_lab_named_uses_caller_name() {
3846 let color = LabColor::with_default(50.0, 12.0, -8.0);
3847 let mut ctx = GraphicsContext::new();
3848 ctx.set_fill_color_lab_named("MyLab", color.clone());
3849 let expected = format!("/MyLab cs\n{}", expected_components(&color.values(), "sc"));
3850 assert_eq!(ctx.operations(), expected);
3851 }
3852
3853 #[test]
3854 fn set_stroke_color_lab_named_uses_caller_name() {
3855 let color = LabColor::with_default(50.0, 12.0, -8.0);
3856 let mut ctx = GraphicsContext::new();
3857 ctx.set_stroke_color_lab_named("MyLab", color.clone());
3858 let expected = format!("/MyLab CS\n{}", expected_components(&color.values(), "SC"));
3859 assert_eq!(ctx.operations(), expected);
3860 }
3861
3862 #[test]
3866 fn legacy_calibrated_rgb_still_emits_calrgb1() {
3867 let color = CalibratedColor::cal_rgb([0.1, 0.2, 0.3], CalRgbColorSpace::new());
3868 let mut ctx = GraphicsContext::new();
3869 ctx.set_fill_color_calibrated(color.clone());
3870 let expected = format!(
3871 "/CalRGB1 cs\n{}",
3872 expected_components(&color.values(), "sc")
3873 );
3874 assert_eq!(ctx.operations(), expected);
3875 }
3876
3877 #[test]
3878 fn legacy_calibrated_gray_still_emits_calgray1() {
3879 let color = CalibratedColor::cal_gray(0.6, CalGrayColorSpace::new());
3880 let mut ctx = GraphicsContext::new();
3881 ctx.set_stroke_color_calibrated(color.clone());
3882 let expected = format!(
3883 "/CalGray1 CS\n{}",
3884 expected_components(&color.values(), "SC")
3885 );
3886 assert_eq!(ctx.operations(), expected);
3887 }
3888
3889 #[test]
3890 fn legacy_lab_still_emits_lab1() {
3891 let color = LabColor::with_default(50.0, 12.0, -8.0);
3892 let mut ctx = GraphicsContext::new();
3893 ctx.set_fill_color_lab(color.clone());
3894 let expected = format!("/Lab1 cs\n{}", expected_components(&color.values(), "sc"));
3895 assert_eq!(ctx.operations(), expected);
3896 }
3897
3898 #[test]
3899 fn legacy_lab_stroke_still_emits_lab1() {
3900 let color = LabColor::with_default(50.0, 12.0, -8.0);
3901 let mut ctx = GraphicsContext::new();
3902 ctx.set_stroke_color_lab(color.clone());
3903 let expected = format!("/Lab1 CS\n{}", expected_components(&color.values(), "SC"));
3904 assert_eq!(ctx.operations(), expected);
3905 }
3906
3907 #[test]
3912 #[cfg(debug_assertions)]
3913 #[should_panic(expected = "ICC fill colour components must not be empty")]
3914 fn set_fill_color_icc_empty_components_panics_in_debug() {
3915 let mut ctx = GraphicsContext::new();
3919 ctx.set_fill_color_icc("ICCRGB1", vec![]);
3920 }
3921
3922 #[test]
3923 #[cfg(debug_assertions)]
3924 #[should_panic(expected = "ICC stroke colour components must not be empty")]
3925 fn set_stroke_color_icc_empty_components_panics_in_debug() {
3926 let mut ctx = GraphicsContext::new();
3927 ctx.set_stroke_color_icc("ICCRGB1", vec![]);
3928 }
3929}