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(Debug, Clone, Copy, PartialEq)]
76#[non_exhaustive]
77pub struct CidShowElement {
78 pub cid: u16,
80 pub adjust: f32,
84 pub x_offset: f32,
91}
92
93impl CidShowElement {
94 pub fn new(cid: u16, adjust: f32) -> Self {
104 Self {
105 cid,
106 adjust,
107 x_offset: 0.0,
108 }
109 }
110
111 pub fn with_x_offset(mut self, x_offset: f32) -> Self {
113 self.x_offset = x_offset;
114 self
115 }
116}
117
118#[derive(Clone)]
121struct GraphicsState {
122 fill_color: Color,
123 stroke_color: Color,
124 font_name: Option<Arc<str>>,
125 font_size: f64,
126 is_custom_font: bool,
127}
128
129#[derive(Clone)]
130pub struct GraphicsContext {
131 operations: Vec<ops::Op>,
132 current_color: Color,
133 stroke_color: Color,
134 line_width: f64,
135 fill_opacity: f64,
136 stroke_opacity: f64,
137 extgstate_manager: ExtGStateManager,
139 pending_extgstate: Option<ExtGState>,
140 current_dash_pattern: Option<LineDashPattern>,
141 current_miter_limit: f64,
142 current_line_cap: LineCap,
143 current_line_join: LineJoin,
144 current_rendering_intent: RenderingIntent,
145 current_flatness: f64,
146 current_smoothness: f64,
147 clipping_region: ClippingRegion,
149 font_manager: Option<Arc<FontManager>>,
151 state_stack: Vec<GraphicsState>,
153 current_font_name: Option<Arc<str>>,
154 current_font_size: f64,
155 is_custom_font: bool,
157 used_characters_by_font: HashMap<String, HashSet<char>>,
163 glyph_mapping: Option<HashMap<u32, u16>>,
165 transparency_stack: Vec<TransparencyGroupState>,
167}
168
169fn encode_char_as_cid(ch: char, buf: &mut String) {
173 let code = ch as u32;
174 if code <= 0xFFFF {
175 write!(buf, "{:04X}", code).expect("Writing to string should never fail");
176 } else {
177 let adjusted = code - 0x10000;
179 let high = ((adjusted >> 10) & 0x3FF) + 0xD800;
180 let low = (adjusted & 0x3FF) + 0xDC00;
181 write!(buf, "{:04X}{:04X}", high, low).expect("Writing to string should never fail");
182 }
183}
184
185impl Default for GraphicsContext {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191impl GraphicsContext {
192 pub fn new() -> Self {
193 Self {
194 operations: Vec::new(),
195 current_color: Color::black(),
196 stroke_color: Color::black(),
197 line_width: 1.0,
198 fill_opacity: 1.0,
199 stroke_opacity: 1.0,
200 extgstate_manager: ExtGStateManager::new(),
202 pending_extgstate: None,
203 current_dash_pattern: None,
204 current_miter_limit: 10.0,
205 current_line_cap: LineCap::Butt,
206 current_line_join: LineJoin::Miter,
207 current_rendering_intent: RenderingIntent::RelativeColorimetric,
208 current_flatness: 1.0,
209 current_smoothness: 0.0,
210 clipping_region: ClippingRegion::new(),
212 font_manager: None,
214 state_stack: Vec::new(),
215 current_font_name: None,
216 current_font_size: 12.0,
217 is_custom_font: false,
218 used_characters_by_font: HashMap::new(),
219 glyph_mapping: None,
220 transparency_stack: Vec::new(),
221 }
222 }
223
224 pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
225 self.operations.push(ops::Op::MoveTo { x, y });
226 self
227 }
228
229 pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
230 self.operations.push(ops::Op::LineTo { x, y });
231 self
232 }
233
234 pub fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
235 self.operations.push(ops::Op::CurveTo {
236 x1,
237 y1,
238 x2,
239 y2,
240 x3,
241 y3,
242 });
243 self
244 }
245
246 pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
247 self.operations.push(ops::Op::Rect {
248 x,
249 y,
250 w: width,
251 h: height,
252 });
253 self
254 }
255
256 pub fn circle(&mut self, cx: f64, cy: f64, radius: f64) -> &mut Self {
257 let k = 0.552284749831;
258 let r = radius;
259
260 self.move_to(cx + r, cy);
261 self.curve_to(cx + r, cy + k * r, cx + k * r, cy + r, cx, cy + r);
262 self.curve_to(cx - k * r, cy + r, cx - r, cy + k * r, cx - r, cy);
263 self.curve_to(cx - r, cy - k * r, cx - k * r, cy - r, cx, cy - r);
264 self.curve_to(cx + k * r, cy - r, cx + r, cy - k * r, cx + r, cy);
265 self.close_path()
266 }
267
268 pub fn close_path(&mut self) -> &mut Self {
269 self.operations.push(ops::Op::ClosePath);
270 self
271 }
272
273 pub fn stroke(&mut self) -> &mut Self {
274 self.apply_pending_extgstate().unwrap_or_default();
275 self.apply_stroke_color();
276 self.operations.push(ops::Op::Stroke);
277 self
278 }
279
280 pub fn fill(&mut self) -> &mut Self {
281 self.apply_pending_extgstate().unwrap_or_default();
282 self.apply_fill_color();
283 self.operations.push(ops::Op::FillNonZero);
284 self
285 }
286
287 pub fn fill_stroke(&mut self) -> &mut Self {
288 self.apply_pending_extgstate().unwrap_or_default();
289 self.apply_fill_color();
290 self.apply_stroke_color();
291 self.operations.push(ops::Op::FillStroke);
292 self
293 }
294
295 pub fn paint_shading(&mut self, name: impl Into<String>) -> &mut Self {
304 self.apply_pending_extgstate().unwrap_or_default();
305 self.operations.push(ops::Op::PaintShading(name.into()));
306 self
307 }
308
309 pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
310 self.stroke_color = color;
311 self
312 }
313
314 pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
315 self.current_color = color;
316 self
317 }
318
319 fn push_color_space_and_components(
325 &mut self,
326 space: ops::Op,
327 components: ops::Op,
328 ) -> &mut Self {
329 self.operations.push(space);
330 self.operations.push(components);
331 self
332 }
333
334 pub fn set_fill_color_icc(
345 &mut self,
346 name: impl Into<String>,
347 components: Vec<f64>,
348 ) -> &mut Self {
349 debug_assert!(
350 !components.is_empty(),
351 "ICC fill colour components must not be empty"
352 );
353 self.push_color_space_and_components(
354 ops::Op::SetFillColorSpace(name.into()),
355 ops::Op::SetFillColorComponents(components),
356 )
357 }
358
359 pub fn set_stroke_color_icc(
365 &mut self,
366 name: impl Into<String>,
367 components: Vec<f64>,
368 ) -> &mut Self {
369 debug_assert!(
370 !components.is_empty(),
371 "ICC stroke colour components must not be empty"
372 );
373 self.push_color_space_and_components(
374 ops::Op::SetStrokeColorSpace(name.into()),
375 ops::Op::SetStrokeColorComponents(components),
376 )
377 }
378
379 pub fn set_fill_color_calibrated_named(
387 &mut self,
388 name: impl Into<String>,
389 color: CalibratedColor,
390 ) -> &mut Self {
391 self.push_color_space_and_components(
392 ops::Op::SetFillColorSpace(name.into()),
393 ops::Op::SetFillColorComponents(color.values()),
394 )
395 }
396
397 pub fn set_stroke_color_calibrated_named(
400 &mut self,
401 name: impl Into<String>,
402 color: CalibratedColor,
403 ) -> &mut Self {
404 self.push_color_space_and_components(
405 ops::Op::SetStrokeColorSpace(name.into()),
406 ops::Op::SetStrokeColorComponents(color.values()),
407 )
408 }
409
410 pub fn set_fill_color_lab_named(
417 &mut self,
418 name: impl Into<String>,
419 color: LabColor,
420 ) -> &mut Self {
421 self.push_color_space_and_components(
422 ops::Op::SetFillColorSpace(name.into()),
423 ops::Op::SetFillColorComponents(color.values()),
424 )
425 }
426
427 pub fn set_stroke_color_lab_named(
430 &mut self,
431 name: impl Into<String>,
432 color: LabColor,
433 ) -> &mut Self {
434 self.push_color_space_and_components(
435 ops::Op::SetStrokeColorSpace(name.into()),
436 ops::Op::SetStrokeColorComponents(color.values()),
437 )
438 }
439
440 pub fn set_fill_color_calibrated(&mut self, color: CalibratedColor) -> &mut Self {
446 let cs_name = match &color {
447 CalibratedColor::Gray(_, _) => "CalGray1",
448 CalibratedColor::Rgb(_, _) => "CalRGB1",
449 };
450 self.set_fill_color_calibrated_named(cs_name, color)
451 }
452
453 pub fn set_stroke_color_calibrated(&mut self, color: CalibratedColor) -> &mut Self {
459 let cs_name = match &color {
460 CalibratedColor::Gray(_, _) => "CalGray1",
461 CalibratedColor::Rgb(_, _) => "CalRGB1",
462 };
463 self.set_stroke_color_calibrated_named(cs_name, color)
464 }
465
466 pub fn set_fill_color_lab(&mut self, color: LabColor) -> &mut Self {
472 self.set_fill_color_lab_named("Lab1", color)
473 }
474
475 pub fn set_stroke_color_lab(&mut self, color: LabColor) -> &mut Self {
481 self.set_stroke_color_lab_named("Lab1", color)
482 }
483
484 pub fn set_line_width(&mut self, width: f64) -> &mut Self {
485 self.line_width = width;
486 self.operations.push(ops::Op::SetLineWidth(width));
487 self
488 }
489
490 pub fn set_line_cap(&mut self, cap: LineCap) -> &mut Self {
491 self.current_line_cap = cap;
492 self.operations.push(ops::Op::SetLineCap(cap as u8));
493 self
494 }
495
496 pub fn set_line_join(&mut self, join: LineJoin) -> &mut Self {
497 self.current_line_join = join;
498 self.operations.push(ops::Op::SetLineJoin(join as u8));
499 self
500 }
501
502 pub fn set_opacity(&mut self, opacity: f64) -> &mut Self {
504 let opacity = opacity.clamp(0.0, 1.0);
505 self.fill_opacity = opacity;
506 self.stroke_opacity = opacity;
507
508 if opacity < 1.0 {
510 let mut state = ExtGState::new();
511 state.alpha_fill = Some(opacity);
512 state.alpha_stroke = Some(opacity);
513 self.pending_extgstate = Some(state);
514 }
515
516 self
517 }
518
519 pub fn set_fill_opacity(&mut self, opacity: f64) -> &mut Self {
521 self.fill_opacity = opacity.clamp(0.0, 1.0);
522
523 if opacity < 1.0 {
525 if let Some(ref mut state) = self.pending_extgstate {
526 state.alpha_fill = Some(opacity);
527 } else {
528 let mut state = ExtGState::new();
529 state.alpha_fill = Some(opacity);
530 self.pending_extgstate = Some(state);
531 }
532 }
533
534 self
535 }
536
537 pub fn set_stroke_opacity(&mut self, opacity: f64) -> &mut Self {
539 self.stroke_opacity = opacity.clamp(0.0, 1.0);
540
541 if opacity < 1.0 {
543 if let Some(ref mut state) = self.pending_extgstate {
544 state.alpha_stroke = Some(opacity);
545 } else {
546 let mut state = ExtGState::new();
547 state.alpha_stroke = Some(opacity);
548 self.pending_extgstate = Some(state);
549 }
550 }
551
552 self
553 }
554
555 pub fn save_state(&mut self) -> &mut Self {
556 self.operations.push(ops::Op::SaveState);
557 self.save_clipping_state();
558 self.state_stack.push(GraphicsState {
560 fill_color: self.current_color,
561 stroke_color: self.stroke_color,
562 font_name: self.current_font_name.clone(),
563 font_size: self.current_font_size,
564 is_custom_font: self.is_custom_font,
565 });
566 self
567 }
568
569 pub fn restore_state(&mut self) -> &mut Self {
570 self.operations.push(ops::Op::RestoreState);
571 self.restore_clipping_state();
572 if let Some(state) = self.state_stack.pop() {
574 self.current_color = state.fill_color;
575 self.stroke_color = state.stroke_color;
576 self.current_font_name = state.font_name;
577 self.current_font_size = state.font_size;
578 self.is_custom_font = state.is_custom_font;
579 }
580 self
581 }
582
583 pub fn begin_transparency_group(&mut self, group: TransparencyGroup) -> &mut Self {
586 self.save_state();
588
589 self.operations
591 .push(ops::Op::Comment("Begin Transparency Group".to_string()));
592
593 let mut extgstate = ExtGState::new();
595 extgstate = extgstate.with_blend_mode(group.blend_mode.clone());
596 extgstate.alpha_fill = Some(group.opacity as f64);
597 extgstate.alpha_stroke = Some(group.opacity as f64);
598
599 self.pending_extgstate = Some(extgstate);
601 let _ = self.apply_pending_extgstate();
602
603 self.transparency_stack
608 .push(TransparencyGroupState::new(group));
609
610 self
611 }
612
613 pub fn end_transparency_group(&mut self) -> &mut Self {
615 if let Some(_group_state) = self.transparency_stack.pop() {
616 self.operations
618 .push(ops::Op::Comment("End Transparency Group".to_string()));
619
620 self.restore_state();
622 }
623 self
624 }
625
626 pub fn in_transparency_group(&self) -> bool {
628 !self.transparency_stack.is_empty()
629 }
630
631 pub fn current_transparency_group(&self) -> Option<&TransparencyGroup> {
633 self.transparency_stack.last().map(|state| &state.group)
634 }
635
636 pub fn translate(&mut self, tx: f64, ty: f64) -> &mut Self {
637 self.operations.push(ops::Op::Cm {
638 a: 1.0,
639 b: 0.0,
640 c: 0.0,
641 d: 1.0,
642 e: tx,
643 f: ty,
644 });
645 self
646 }
647
648 pub fn scale(&mut self, sx: f64, sy: f64) -> &mut Self {
649 self.operations.push(ops::Op::Cm {
650 a: sx,
651 b: 0.0,
652 c: 0.0,
653 d: sy,
654 e: 0.0,
655 f: 0.0,
656 });
657 self
658 }
659
660 pub fn rotate(&mut self, angle: f64) -> &mut Self {
661 let cos = angle.cos();
662 let sin = angle.sin();
663 self.operations.push(ops::Op::Cm {
667 a: cos,
668 b: sin,
669 c: -sin,
670 d: cos,
671 e: 0.0,
672 f: 0.0,
673 });
674 self
675 }
676
677 pub fn transform(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> &mut Self {
678 self.operations.push(ops::Op::Cm { a, b, c, d, e, f });
679 self
680 }
681
682 pub fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) -> &mut Self {
683 self.rect(x, y, width, height)
684 }
685
686 pub fn draw_image(
687 &mut self,
688 image_name: impl Into<String>,
689 x: f64,
690 y: f64,
691 width: f64,
692 height: f64,
693 ) -> &mut Self {
694 self.save_state();
696
697 self.operations.push(ops::Op::Cm {
700 a: width,
701 b: 0.0,
702 c: 0.0,
703 d: height,
704 e: x,
705 f: y,
706 });
707
708 self.operations
710 .push(ops::Op::InvokeXObject(image_name.into()));
711
712 self.restore_state();
714
715 self
716 }
717
718 pub fn draw_image_with_transparency(
721 &mut self,
722 image_name: impl Into<String>,
723 x: f64,
724 y: f64,
725 width: f64,
726 height: f64,
727 mask_name: Option<&str>,
728 ) -> &mut Self {
729 self.save_state();
731
732 if let Some(mask) = mask_name {
734 let mut extgstate = ExtGState::new();
736 extgstate.set_soft_mask_name(mask.to_string());
737
738 let gs_name = self
740 .extgstate_manager
741 .add_state(extgstate)
742 .unwrap_or_else(|_| "GS1".to_string());
743 self.operations.push(ops::Op::SetExtGState(gs_name));
744 }
745
746 self.operations.push(ops::Op::Cm {
748 a: width,
749 b: 0.0,
750 c: 0.0,
751 d: height,
752 e: x,
753 f: y,
754 });
755
756 self.operations
758 .push(ops::Op::InvokeXObject(image_name.into()));
759
760 if mask_name.is_some() {
762 let mut reset_extgstate = ExtGState::new();
764 reset_extgstate.set_soft_mask_none();
765
766 let gs_name = self
767 .extgstate_manager
768 .add_state(reset_extgstate)
769 .unwrap_or_else(|_| "GS2".to_string());
770 self.operations.push(ops::Op::SetExtGState(gs_name));
771 }
772
773 self.restore_state();
775
776 self
777 }
778
779 fn apply_stroke_color(&mut self) {
780 self.operations
786 .push(ops::Op::SetStrokeColor(self.stroke_color));
787 }
788
789 fn apply_fill_color(&mut self) {
790 self.operations
795 .push(ops::Op::SetFillColor(self.current_color));
796 }
797
798 pub(crate) fn generate_operations(&self) -> Result<Vec<u8>> {
799 let mut buf = Vec::new();
800 ops::serialize_ops(&mut buf, &self.operations);
801 Ok(buf)
802 }
803
804 pub(crate) fn drain_ops(&mut self) -> Vec<ops::Op> {
814 std::mem::take(&mut self.operations)
815 }
816
817 pub(crate) fn ops_slice(&self) -> &[ops::Op] {
820 &self.operations
821 }
822
823 pub fn uses_transparency(&self) -> bool {
825 self.fill_opacity < 1.0 || self.stroke_opacity < 1.0
826 }
827
828 pub fn generate_graphics_state_dict(&self) -> Option<String> {
830 if !self.uses_transparency() {
831 return None;
832 }
833
834 let mut dict = String::from("<< /Type /ExtGState");
835
836 if self.fill_opacity < 1.0 {
837 write!(&mut dict, " /ca {:.3}", self.fill_opacity)
838 .expect("Writing to string should never fail");
839 }
840
841 if self.stroke_opacity < 1.0 {
842 write!(&mut dict, " /CA {:.3}", self.stroke_opacity)
843 .expect("Writing to string should never fail");
844 }
845
846 dict.push_str(" >>");
847 Some(dict)
848 }
849
850 pub fn fill_color(&self) -> Color {
852 self.current_color
853 }
854
855 pub fn stroke_color(&self) -> Color {
857 self.stroke_color
858 }
859
860 pub fn line_width(&self) -> f64 {
862 self.line_width
863 }
864
865 pub fn fill_opacity(&self) -> f64 {
867 self.fill_opacity
868 }
869
870 pub fn stroke_opacity(&self) -> f64 {
872 self.stroke_opacity
873 }
874
875 pub fn operations(&self) -> String {
882 ops::ops_to_string(&self.operations)
883 }
884
885 pub fn get_operations(&self) -> String {
888 ops::ops_to_string(&self.operations)
889 }
890
891 pub fn clear(&mut self) {
893 self.operations.clear();
894 }
895
896 pub fn begin_text(&mut self) -> &mut Self {
898 self.operations.push(ops::Op::BeginText);
899 self
900 }
901
902 pub fn end_text(&mut self) -> &mut Self {
904 self.operations.push(ops::Op::EndText);
905 self
906 }
907
908 pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
910 self.operations.push(ops::Op::SetFont {
911 name: font.pdf_name(),
912 size,
913 });
914
915 match &font {
917 Font::Custom(name) => {
918 self.current_font_name = Some(Arc::from(name.as_str()));
919 self.current_font_size = size;
920 self.is_custom_font = true;
921 }
922 _ => {
923 self.current_font_name = Some(Arc::from(font.pdf_name().as_str()));
924 self.current_font_size = size;
925 self.is_custom_font = false;
926 }
927 }
928
929 self
930 }
931
932 pub fn set_text_position(&mut self, x: f64, y: f64) -> &mut Self {
934 self.operations.push(ops::Op::SetTextPosition { x, y });
935 self
936 }
937
938 pub fn show_text(&mut self, text: &str) -> Result<&mut Self> {
945 self.record_used_chars(text);
949
950 if self.is_custom_font {
951 let mut hex = String::new();
953 for ch in text.chars() {
954 encode_char_as_cid(ch, &mut hex);
955 }
956 self.operations.push(ops::Op::ShowTextHex(hex.into_bytes()));
957 } else {
958 let mut escaped = String::new();
960 for ch in text.chars() {
961 match ch {
962 '(' => escaped.push_str("\\("),
963 ')' => escaped.push_str("\\)"),
964 '\\' => escaped.push_str("\\\\"),
965 '\n' => escaped.push_str("\\n"),
966 '\r' => escaped.push_str("\\r"),
967 '\t' => escaped.push_str("\\t"),
968 _ => escaped.push(ch),
969 }
970 }
971 self.operations
972 .push(ops::Op::ShowText(escaped.into_bytes()));
973 }
974 Ok(self)
975 }
976
977 pub fn set_word_spacing(&mut self, spacing: f64) -> &mut Self {
979 self.operations.push(ops::Op::SetWordSpacing(spacing));
980 self
981 }
982
983 pub fn set_character_spacing(&mut self, spacing: f64) -> &mut Self {
985 self.operations.push(ops::Op::SetCharSpacing(spacing));
986 self
987 }
988
989 pub fn show_justified_text(&mut self, text: &str, target_width: f64) -> Result<&mut Self> {
991 let words: Vec<&str> = text.split_whitespace().collect();
993 if words.len() <= 1 {
994 return self.show_text(text);
996 }
997
998 let text_without_spaces = words.join("");
1000 let natural_text_width = self.estimate_text_width_simple(&text_without_spaces);
1001 let space_width = self.estimate_text_width_simple(" ");
1002 let natural_width = natural_text_width + (space_width * (words.len() - 1) as f64);
1003
1004 let extra_space_needed = target_width - natural_width;
1006 let word_gaps = (words.len() - 1) as f64;
1007
1008 if word_gaps > 0.0 && extra_space_needed > 0.0 {
1009 let extra_word_spacing = extra_space_needed / word_gaps;
1010
1011 self.set_word_spacing(extra_word_spacing);
1013
1014 self.show_text(text)?;
1016
1017 self.set_word_spacing(0.0);
1019 } else {
1020 self.show_text(text)?;
1022 }
1023
1024 Ok(self)
1025 }
1026
1027 fn estimate_text_width_simple(&self, text: &str) -> f64 {
1029 let font_size = self.current_font_size;
1032 text.len() as f64 * font_size * 0.6 }
1034
1035 pub fn render_table(&mut self, table: &Table) -> Result<()> {
1037 table.render(self)
1038 }
1039
1040 pub fn render_list(&mut self, list: &ListElement) -> Result<()> {
1042 match list {
1043 ListElement::Ordered(ordered) => ordered.render(self),
1044 ListElement::Unordered(unordered) => unordered.render(self),
1045 }
1046 }
1047
1048 pub fn render_column_layout(
1050 &mut self,
1051 layout: &ColumnLayout,
1052 content: &ColumnContent,
1053 x: f64,
1054 y: f64,
1055 height: f64,
1056 ) -> Result<()> {
1057 layout.render(self, content, x, y, height)
1058 }
1059
1060 pub fn set_line_dash_pattern(&mut self, pattern: LineDashPattern) -> &mut Self {
1064 self.current_dash_pattern = Some(pattern.clone());
1065 self.operations
1066 .push(ops::Op::SetDashPatternRaw(pattern.to_pdf_string()));
1067 self
1068 }
1069
1070 pub fn set_line_solid(&mut self) -> &mut Self {
1072 self.current_dash_pattern = None;
1073 self.operations
1074 .push(ops::Op::SetDashPatternRaw("[] 0".to_string()));
1075 self
1076 }
1077
1078 pub fn set_miter_limit(&mut self, limit: f64) -> &mut Self {
1080 self.current_miter_limit = limit.max(1.0);
1081 self.operations
1082 .push(ops::Op::SetMiterLimit(self.current_miter_limit));
1083 self
1084 }
1085
1086 pub fn set_rendering_intent(&mut self, intent: RenderingIntent) -> &mut Self {
1088 self.current_rendering_intent = intent;
1089 self.operations
1090 .push(ops::Op::SetRenderingIntent(intent.pdf_name().to_string()));
1091 self
1092 }
1093
1094 pub fn set_flatness(&mut self, flatness: f64) -> &mut Self {
1096 self.current_flatness = flatness.clamp(0.0, 100.0);
1097 self.operations
1098 .push(ops::Op::SetFlatness(self.current_flatness));
1099 self
1100 }
1101
1102 pub fn apply_extgstate(&mut self, state: ExtGState) -> Result<&mut Self> {
1104 let state_name = self.extgstate_manager.add_state(state)?;
1105 self.operations.push(ops::Op::SetExtGState(state_name));
1106 Ok(self)
1107 }
1108
1109 #[allow(dead_code)]
1111 fn set_pending_extgstate(&mut self, state: ExtGState) {
1112 self.pending_extgstate = Some(state);
1113 }
1114
1115 fn apply_pending_extgstate(&mut self) -> Result<()> {
1117 if let Some(state) = self.pending_extgstate.take() {
1118 let state_name = self.extgstate_manager.add_state(state)?;
1119 self.operations.push(ops::Op::SetExtGState(state_name));
1120 }
1121 Ok(())
1122 }
1123
1124 pub fn with_extgstate<F>(&mut self, builder: F) -> Result<&mut Self>
1126 where
1127 F: FnOnce(ExtGState) -> ExtGState,
1128 {
1129 let state = builder(ExtGState::new());
1130 self.apply_extgstate(state)
1131 }
1132
1133 pub fn set_blend_mode(&mut self, mode: BlendMode) -> Result<&mut Self> {
1135 let state = ExtGState::new().with_blend_mode(mode);
1136 self.apply_extgstate(state)
1137 }
1138
1139 pub fn set_alpha(&mut self, alpha: f64) -> Result<&mut Self> {
1141 let state = ExtGState::new().with_alpha(alpha);
1142 self.apply_extgstate(state)
1143 }
1144
1145 pub fn set_alpha_stroke(&mut self, alpha: f64) -> Result<&mut Self> {
1147 let state = ExtGState::new().with_alpha_stroke(alpha);
1148 self.apply_extgstate(state)
1149 }
1150
1151 pub fn set_alpha_fill(&mut self, alpha: f64) -> Result<&mut Self> {
1153 let state = ExtGState::new().with_alpha_fill(alpha);
1154 self.apply_extgstate(state)
1155 }
1156
1157 pub fn set_overprint_stroke(&mut self, overprint: bool) -> Result<&mut Self> {
1159 let state = ExtGState::new().with_overprint_stroke(overprint);
1160 self.apply_extgstate(state)
1161 }
1162
1163 pub fn set_overprint_fill(&mut self, overprint: bool) -> Result<&mut Self> {
1165 let state = ExtGState::new().with_overprint_fill(overprint);
1166 self.apply_extgstate(state)
1167 }
1168
1169 pub fn set_stroke_adjustment(&mut self, adjustment: bool) -> Result<&mut Self> {
1171 let state = ExtGState::new().with_stroke_adjustment(adjustment);
1172 self.apply_extgstate(state)
1173 }
1174
1175 pub fn set_smoothness(&mut self, smoothness: f64) -> Result<&mut Self> {
1177 self.current_smoothness = smoothness.clamp(0.0, 1.0);
1178 let state = ExtGState::new().with_smoothness(self.current_smoothness);
1179 self.apply_extgstate(state)
1180 }
1181
1182 pub fn line_dash_pattern(&self) -> Option<&LineDashPattern> {
1186 self.current_dash_pattern.as_ref()
1187 }
1188
1189 pub fn miter_limit(&self) -> f64 {
1191 self.current_miter_limit
1192 }
1193
1194 pub fn line_cap(&self) -> LineCap {
1196 self.current_line_cap
1197 }
1198
1199 pub fn line_join(&self) -> LineJoin {
1201 self.current_line_join
1202 }
1203
1204 pub fn rendering_intent(&self) -> RenderingIntent {
1206 self.current_rendering_intent
1207 }
1208
1209 pub fn flatness(&self) -> f64 {
1211 self.current_flatness
1212 }
1213
1214 pub fn smoothness(&self) -> f64 {
1216 self.current_smoothness
1217 }
1218
1219 pub fn extgstate_manager(&self) -> &ExtGStateManager {
1221 &self.extgstate_manager
1222 }
1223
1224 pub fn extgstate_manager_mut(&mut self) -> &mut ExtGStateManager {
1226 &mut self.extgstate_manager
1227 }
1228
1229 pub fn generate_extgstate_resources(&self) -> Result<String> {
1231 self.extgstate_manager.to_resource_dictionary()
1232 }
1233
1234 pub fn has_extgstates(&self) -> bool {
1236 self.extgstate_manager.count() > 0
1237 }
1238
1239 pub fn add_command(&mut self, command: &str) {
1246 let mut bytes = command.as_bytes().to_vec();
1247 bytes.push(b'\n');
1248 self.operations.push(ops::Op::Raw(bytes));
1249 }
1250
1251 pub fn clip(&mut self) -> &mut Self {
1253 self.operations.push(ops::Op::ClipNonZero);
1254 self
1255 }
1256
1257 pub fn end_path(&mut self) -> &mut Self {
1263 self.operations.push(ops::Op::EndPath);
1266 self
1267 }
1268
1269 pub fn clip_even_odd(&mut self) -> &mut Self {
1271 self.operations.push(ops::Op::ClipEvenOdd);
1272 self
1273 }
1274
1275 pub fn clip_stroke(&mut self) -> &mut Self {
1277 self.apply_stroke_color();
1278 self.operations.push(ops::Op::ClipStroke);
1279 self
1280 }
1281
1282 pub fn set_clipping_path(&mut self, path: ClippingPath) -> Result<&mut Self> {
1284 let ops_str = path.to_pdf_operations()?;
1285 self.operations.push(ops::Op::Raw(ops_str.into_bytes()));
1286 self.clipping_region.set_clip(path);
1287 Ok(self)
1288 }
1289
1290 pub fn clear_clipping(&mut self) -> &mut Self {
1292 self.clipping_region.clear_clip();
1293 self
1294 }
1295
1296 fn save_clipping_state(&mut self) {
1298 self.clipping_region.save();
1299 }
1300
1301 fn restore_clipping_state(&mut self) {
1303 self.clipping_region.restore();
1304 }
1305
1306 pub fn clip_rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> Result<&mut Self> {
1308 let path = ClippingPath::rect(x, y, width, height);
1309 self.set_clipping_path(path)
1310 }
1311
1312 pub fn clip_circle(&mut self, cx: f64, cy: f64, radius: f64) -> Result<&mut Self> {
1314 let path = ClippingPath::circle(cx, cy, radius);
1315 self.set_clipping_path(path)
1316 }
1317
1318 pub fn clip_ellipse(&mut self, cx: f64, cy: f64, rx: f64, ry: f64) -> Result<&mut Self> {
1320 let path = ClippingPath::ellipse(cx, cy, rx, ry);
1321 self.set_clipping_path(path)
1322 }
1323
1324 pub fn has_clipping(&self) -> bool {
1326 self.clipping_region.has_clip()
1327 }
1328
1329 pub fn clipping_path(&self) -> Option<&ClippingPath> {
1331 self.clipping_region.current()
1332 }
1333
1334 pub fn set_font_manager(&mut self, font_manager: Arc<FontManager>) -> &mut Self {
1336 self.font_manager = Some(font_manager);
1337 self
1338 }
1339
1340 pub fn set_custom_font(&mut self, font_name: &str, size: f64) -> &mut Self {
1342 self.operations.push(ops::Op::SetFont {
1344 name: font_name.to_string(),
1345 size,
1346 });
1347
1348 self.current_font_name = Some(Arc::from(font_name));
1349 self.current_font_size = size;
1350 self.is_custom_font = true;
1351
1352 if let Some(ref font_manager) = self.font_manager {
1354 if let Some(mapping) = font_manager.get_font_glyph_mapping(font_name) {
1355 self.glyph_mapping = Some(mapping);
1356 }
1357 }
1358
1359 self
1360 }
1361
1362 pub fn set_glyph_mapping(&mut self, mapping: HashMap<u32, u16>) -> &mut Self {
1364 self.glyph_mapping = Some(mapping);
1365 self
1366 }
1367
1368 pub fn draw_text(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1370 self.record_used_chars(text);
1373
1374 let needs_unicode = self.is_custom_font || text.chars().any(|c| c as u32 > 255);
1377
1378 if needs_unicode {
1380 self.draw_with_unicode_encoding(text, x, y)
1381 } else {
1382 self.draw_with_simple_encoding(text, x, y)
1383 }
1384 }
1385
1386 fn draw_with_simple_encoding(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1388 let has_unicode = text.chars().any(|c| c as u32 > 255);
1390
1391 if has_unicode {
1392 tracing::debug!("Warning: Text contains Unicode characters but using Latin-1 font. Characters will be replaced with '?'");
1393 }
1394
1395 self.operations.push(ops::Op::BeginText);
1396 self.apply_fill_color();
1397 self.push_active_font();
1398 self.operations.push(ops::Op::SetTextPosition { x, y });
1399
1400 let mut buf = String::new();
1403 for ch in text.chars() {
1404 let code = ch as u32;
1405 if code <= 127 {
1406 match ch {
1407 '(' => buf.push_str("\\("),
1408 ')' => buf.push_str("\\)"),
1409 '\\' => buf.push_str("\\\\"),
1410 '\n' => buf.push_str("\\n"),
1411 '\r' => buf.push_str("\\r"),
1412 '\t' => buf.push_str("\\t"),
1413 _ => buf.push(ch),
1414 }
1415 } else if code <= 255 {
1416 use std::fmt::Write as _;
1417 write!(&mut buf, "\\{code:03o}").expect("write to String never fails");
1418 } else {
1419 buf.push('?');
1420 }
1421 }
1422 self.operations.push(ops::Op::ShowText(buf.into_bytes()));
1423 self.operations.push(ops::Op::EndText);
1424
1425 Ok(self)
1426 }
1427
1428 fn draw_with_unicode_encoding(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1430 self.operations.push(ops::Op::BeginText);
1431 self.apply_fill_color();
1432 self.push_active_font();
1433 self.operations.push(ops::Op::SetTextPosition { x, y });
1434
1435 let mut hex = String::new();
1436 for ch in text.chars() {
1437 encode_char_as_cid(ch, &mut hex);
1438 }
1439 self.operations.push(ops::Op::ShowTextHex(hex.into_bytes()));
1440 self.operations.push(ops::Op::EndText);
1441
1442 Ok(self)
1443 }
1444
1445 fn push_active_font(&mut self) {
1450 let name = self
1451 .current_font_name
1452 .as_deref()
1453 .unwrap_or("Helvetica")
1454 .to_string();
1455 self.operations.push(ops::Op::SetFont {
1456 name,
1457 size: self.current_font_size,
1458 });
1459 }
1460
1461 pub fn show_cid_array(&mut self, elements: &[CidShowElement], x: f64, y: f64) -> &mut Self {
1473 self.operations.push(ops::Op::BeginText);
1474 self.apply_fill_color();
1475 self.push_active_font();
1476 self.operations.push(ops::Op::SetTextPosition { x, y });
1477
1478 let mut tj: Vec<ops::TextArrayElement> = Vec::new();
1479 let mut run = String::new();
1480 for el in elements {
1481 if el.x_offset != 0.0 {
1482 if !run.is_empty() {
1486 tj.push(ops::TextArrayElement::Glyphs(
1487 std::mem::take(&mut run).into_bytes(),
1488 ));
1489 }
1490 tj.push(ops::TextArrayElement::Adjust(-el.x_offset));
1491 tj.push(ops::TextArrayElement::Glyphs(
1492 format!("{:04X}", el.cid).into_bytes(),
1493 ));
1494 tj.push(ops::TextArrayElement::Adjust(el.x_offset + el.adjust));
1495 continue;
1496 }
1497 write!(&mut run, "{:04X}", el.cid).expect("write to String never fails");
1498 if el.adjust != 0.0 {
1499 tj.push(ops::TextArrayElement::Glyphs(
1502 std::mem::take(&mut run).into_bytes(),
1503 ));
1504 tj.push(ops::TextArrayElement::Adjust(el.adjust));
1505 }
1506 }
1507 if !run.is_empty() {
1508 tj.push(ops::TextArrayElement::Glyphs(run.into_bytes()));
1509 }
1510
1511 self.operations.push(ops::Op::ShowTextArray(tj));
1512 self.operations.push(ops::Op::EndText);
1513 self
1514 }
1515
1516 #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1518 pub fn draw_text_hex(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1519 self.operations.push(ops::Op::BeginText);
1520 self.apply_fill_color();
1521 self.push_active_font();
1522 self.operations.push(ops::Op::SetTextPosition { x, y });
1523
1524 let mut hex = String::new();
1525 for ch in text.chars() {
1526 use std::fmt::Write as _;
1527 if ch as u32 <= 255 {
1528 write!(&mut hex, "{:02X}", ch as u8).expect("write to String never fails");
1529 } else {
1530 hex.push_str("3F");
1531 }
1532 }
1533 self.operations.push(ops::Op::ShowTextHex(hex.into_bytes()));
1534 self.operations.push(ops::Op::EndText);
1535
1536 Ok(self)
1537 }
1538
1539 #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1541 pub fn draw_text_cid(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1542 use crate::fonts::needs_type0_font;
1543
1544 self.operations.push(ops::Op::BeginText);
1545 self.apply_fill_color();
1546 self.push_active_font();
1547 self.operations.push(ops::Op::SetTextPosition { x, y });
1548
1549 let mut hex = String::new();
1550 if needs_type0_font(text) {
1551 for ch in text.chars() {
1552 encode_char_as_cid(ch, &mut hex);
1553 }
1554 } else {
1555 for ch in text.chars() {
1556 use std::fmt::Write as _;
1557 if ch as u32 <= 255 {
1558 write!(&mut hex, "{:02X}", ch as u8).expect("write to String never fails");
1559 } else {
1560 hex.push_str("3F");
1561 }
1562 }
1563 }
1564 self.operations.push(ops::Op::ShowTextHex(hex.into_bytes()));
1565 self.operations.push(ops::Op::EndText);
1566
1567 Ok(self)
1568 }
1569
1570 #[deprecated(note = "Use draw_text() which automatically detects encoding")]
1572 pub fn draw_text_unicode(&mut self, text: &str, x: f64, y: f64) -> Result<&mut Self> {
1573 self.operations.push(ops::Op::BeginText);
1574 self.apply_fill_color();
1575 self.push_active_font();
1576 self.operations.push(ops::Op::SetTextPosition { x, y });
1577
1578 let mut hex = String::new();
1579 let mut utf16_buffer = [0u16; 2];
1580 for ch in text.chars() {
1581 let encoded = ch.encode_utf16(&mut utf16_buffer);
1582 for unit in encoded {
1583 use std::fmt::Write as _;
1584 write!(&mut hex, "{:04X}", unit).expect("write to String never fails");
1585 }
1586 }
1587 self.operations.push(ops::Op::ShowTextHex(hex.into_bytes()));
1588 self.operations.push(ops::Op::EndText);
1589
1590 Ok(self)
1591 }
1592
1593 fn record_used_chars(&mut self, text: &str) {
1604 let bucket = self.current_font_name.as_deref().unwrap_or("").to_string();
1605 self.used_characters_by_font
1606 .entry(bucket)
1607 .or_default()
1608 .extend(text.chars());
1609 }
1610
1611 #[cfg(test)]
1617 pub(crate) fn get_used_characters(&self) -> Option<HashSet<char>> {
1618 let merged: HashSet<char> = self
1619 .used_characters_by_font
1620 .values()
1621 .flat_map(|s| s.iter().copied())
1622 .collect();
1623 if merged.is_empty() {
1624 None
1625 } else {
1626 Some(merged)
1627 }
1628 }
1629
1630 pub(crate) fn get_used_characters_by_font(&self) -> &HashMap<String, HashSet<char>> {
1638 &self.used_characters_by_font
1639 }
1640
1641 pub(crate) fn merge_font_usage(&mut self, usage: &HashMap<String, HashSet<char>>) {
1647 for (name, chars) in usage {
1648 self.used_characters_by_font
1649 .entry(name.clone())
1650 .or_default()
1651 .extend(chars);
1652 }
1653 }
1654}
1655
1656#[cfg(test)]
1657mod tests {
1658 use super::*;
1659
1660 #[test]
1661 fn cid_show_element_new_sets_fields() {
1662 let el = CidShowElement::new(7, -25.0);
1666 assert_eq!(el.cid, 7);
1667 assert_eq!(el.adjust, -25.0);
1668 }
1669
1670 #[test]
1671 fn cid_show_element_x_offset_defaults_zero_and_builder_sets_it() {
1672 let e = CidShowElement::new(7, -25.0);
1675 assert_eq!(e.x_offset, 0.0);
1676 let e2 = CidShowElement::new(7, -25.0).with_x_offset(40.0);
1677 assert_eq!(e2.x_offset, 40.0);
1678 assert_eq!(e2.cid, 7);
1679 assert_eq!(e2.adjust, -25.0);
1680 }
1681
1682 #[test]
1683 fn show_cid_array_x_offset_displaces_without_consuming_advance() {
1684 let mut gc = GraphicsContext::new();
1687 gc.set_custom_font("ShapedRoboto", 12.0);
1688 gc.show_cid_array(
1689 &[CidShowElement::new(5, 0.0).with_x_offset(40.0)],
1690 100.0,
1691 700.0,
1692 );
1693 let out = String::from_utf8(gc.generate_operations().unwrap()).unwrap();
1694 assert!(
1695 out.contains("[ -40.00 <0005> 40.00 ] TJ"),
1696 "x_offset must wrap the glyph with paired TJ adjustments; got:\n{out}"
1697 );
1698 }
1699
1700 #[test]
1701 fn show_cid_array_x_offset_combines_with_adjust() {
1702 let mut gc = GraphicsContext::new();
1705 gc.set_custom_font("ShapedRoboto", 12.0);
1706 gc.show_cid_array(
1707 &[CidShowElement::new(5, -30.0).with_x_offset(40.0)],
1708 0.0,
1709 0.0,
1710 );
1711 let out = String::from_utf8(gc.generate_operations().unwrap()).unwrap();
1712 assert!(
1713 out.contains("[ -40.00 <0005> 10.00 ] TJ"),
1714 "x_offset + adjust must fold into the trailing TJ number; got:\n{out}"
1715 );
1716 }
1717
1718 #[test]
1719 fn show_cid_array_emits_tj_with_adjustment_between_glyph_runs() {
1720 let mut gc = GraphicsContext::new();
1723 gc.set_custom_font("ShapedRoboto", 12.0);
1724 gc.show_cid_array(
1725 &[CidShowElement::new(5, -30.0), CidShowElement::new(6, 0.0)],
1726 100.0,
1727 700.0,
1728 );
1729 let out = String::from_utf8(gc.generate_operations().unwrap()).unwrap();
1730 assert!(
1731 out.contains("[ <0005> -30.00 <0006> ] TJ"),
1732 "expected TJ array with adjustment; got:\n{out}"
1733 );
1734 }
1735
1736 #[test]
1737 fn show_cid_array_with_empty_run_does_not_panic_and_emits_empty_tj() {
1738 let mut gc = GraphicsContext::new();
1740 gc.set_custom_font("ShapedRoboto", 12.0);
1741 gc.show_cid_array(&[], 0.0, 0.0);
1742 let out = String::from_utf8(gc.generate_operations().unwrap()).unwrap();
1743 assert!(
1744 out.contains("[ ] TJ"),
1745 "expected empty TJ array; got:\n{out}"
1746 );
1747 }
1748
1749 #[test]
1750 fn show_cid_array_coalesces_consecutive_unadjusted_glyphs() {
1751 let mut gc = GraphicsContext::new();
1753 gc.set_custom_font("ShapedRoboto", 12.0);
1754 gc.show_cid_array(
1755 &[CidShowElement::new(5, 0.0), CidShowElement::new(6, 0.0)],
1756 0.0,
1757 0.0,
1758 );
1759 let out = String::from_utf8(gc.generate_operations().unwrap()).unwrap();
1760 assert!(
1761 out.contains("[ <00050006> ] TJ"),
1762 "expected coalesced glyph run; got:\n{out}"
1763 );
1764 }
1765
1766 #[test]
1767 fn test_graphics_context_new() {
1768 let ctx = GraphicsContext::new();
1769 assert_eq!(ctx.fill_color(), Color::black());
1770 assert_eq!(ctx.stroke_color(), Color::black());
1771 assert_eq!(ctx.line_width(), 1.0);
1772 assert_eq!(ctx.fill_opacity(), 1.0);
1773 assert_eq!(ctx.stroke_opacity(), 1.0);
1774 assert!(ctx.operations().is_empty());
1775 }
1776
1777 #[test]
1778 fn test_graphics_context_default() {
1779 let ctx = GraphicsContext::default();
1780 assert_eq!(ctx.fill_color(), Color::black());
1781 assert_eq!(ctx.stroke_color(), Color::black());
1782 assert_eq!(ctx.line_width(), 1.0);
1783 }
1784
1785 #[test]
1786 fn test_move_to() {
1787 let mut ctx = GraphicsContext::new();
1788 ctx.move_to(10.0, 20.0);
1789 assert!(ctx.operations().contains("10.00 20.00 m\n"));
1790 }
1791
1792 #[test]
1793 fn test_line_to() {
1794 let mut ctx = GraphicsContext::new();
1795 ctx.line_to(30.0, 40.0);
1796 assert!(ctx.operations().contains("30.00 40.00 l\n"));
1797 }
1798
1799 #[test]
1800 fn test_curve_to() {
1801 let mut ctx = GraphicsContext::new();
1802 ctx.curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0);
1803 assert!(ctx
1804 .operations()
1805 .contains("10.00 20.00 30.00 40.00 50.00 60.00 c\n"));
1806 }
1807
1808 #[test]
1809 fn test_rect() {
1810 let mut ctx = GraphicsContext::new();
1811 ctx.rect(10.0, 20.0, 100.0, 50.0);
1812 assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
1813 }
1814
1815 #[test]
1816 fn test_rectangle_alias() {
1817 let mut ctx = GraphicsContext::new();
1818 ctx.rectangle(10.0, 20.0, 100.0, 50.0);
1819 assert!(ctx.operations().contains("10.00 20.00 100.00 50.00 re\n"));
1820 }
1821
1822 #[test]
1823 fn test_circle() {
1824 let mut ctx = GraphicsContext::new();
1825 ctx.circle(50.0, 50.0, 25.0);
1826
1827 let ops = ctx.operations();
1828 assert!(ops.contains("75.00 50.00 m\n"));
1830 assert!(ops.contains(" c\n"));
1832 assert!(ops.contains("h\n"));
1834 }
1835
1836 #[test]
1837 fn test_close_path() {
1838 let mut ctx = GraphicsContext::new();
1839 ctx.close_path();
1840 assert!(ctx.operations().contains("h\n"));
1841 }
1842
1843 #[test]
1844 fn test_stroke() {
1845 let mut ctx = GraphicsContext::new();
1846 ctx.set_stroke_color(Color::red());
1847 ctx.rect(0.0, 0.0, 10.0, 10.0);
1848 ctx.stroke();
1849
1850 let ops = ctx.operations();
1851 assert!(ops.contains("1.000 0.000 0.000 RG\n"));
1852 assert!(ops.contains("S\n"));
1853 }
1854
1855 #[test]
1856 fn test_fill() {
1857 let mut ctx = GraphicsContext::new();
1858 ctx.set_fill_color(Color::blue());
1859 ctx.rect(0.0, 0.0, 10.0, 10.0);
1860 ctx.fill();
1861
1862 let ops = ctx.operations();
1863 assert!(ops.contains("0.000 0.000 1.000 rg\n"));
1864 assert!(ops.contains("f\n"));
1865 }
1866
1867 #[test]
1868 fn test_fill_stroke() {
1869 let mut ctx = GraphicsContext::new();
1870 ctx.set_fill_color(Color::green());
1871 ctx.set_stroke_color(Color::red());
1872 ctx.rect(0.0, 0.0, 10.0, 10.0);
1873 ctx.fill_stroke();
1874
1875 let ops = ctx.operations();
1876 assert!(ops.contains("0.000 1.000 0.000 rg\n"));
1877 assert!(ops.contains("1.000 0.000 0.000 RG\n"));
1878 assert!(ops.contains("B\n"));
1879 }
1880
1881 #[test]
1882 fn test_set_stroke_color() {
1883 let mut ctx = GraphicsContext::new();
1884 ctx.set_stroke_color(Color::rgb(0.5, 0.6, 0.7));
1885 assert_eq!(ctx.stroke_color(), Color::Rgb(0.5, 0.6, 0.7));
1886 }
1887
1888 #[test]
1889 fn test_set_fill_color() {
1890 let mut ctx = GraphicsContext::new();
1891 ctx.set_fill_color(Color::gray(0.5));
1892 assert_eq!(ctx.fill_color(), Color::Gray(0.5));
1893 }
1894
1895 #[test]
1896 fn test_set_line_width() {
1897 let mut ctx = GraphicsContext::new();
1898 ctx.set_line_width(2.5);
1899 assert_eq!(ctx.line_width(), 2.5);
1900 assert!(ctx.operations().contains("2.50 w\n"));
1901 }
1902
1903 #[test]
1904 fn test_set_line_cap() {
1905 let mut ctx = GraphicsContext::new();
1906 ctx.set_line_cap(LineCap::Round);
1907 assert!(ctx.operations().contains("1 J\n"));
1908
1909 ctx.set_line_cap(LineCap::Butt);
1910 assert!(ctx.operations().contains("0 J\n"));
1911
1912 ctx.set_line_cap(LineCap::Square);
1913 assert!(ctx.operations().contains("2 J\n"));
1914 }
1915
1916 #[test]
1917 fn test_set_line_join() {
1918 let mut ctx = GraphicsContext::new();
1919 ctx.set_line_join(LineJoin::Round);
1920 assert!(ctx.operations().contains("1 j\n"));
1921
1922 ctx.set_line_join(LineJoin::Miter);
1923 assert!(ctx.operations().contains("0 j\n"));
1924
1925 ctx.set_line_join(LineJoin::Bevel);
1926 assert!(ctx.operations().contains("2 j\n"));
1927 }
1928
1929 #[test]
1930 fn test_save_restore_state() {
1931 let mut ctx = GraphicsContext::new();
1932 ctx.save_state();
1933 assert!(ctx.operations().contains("q\n"));
1934
1935 ctx.restore_state();
1936 assert!(ctx.operations().contains("Q\n"));
1937 }
1938
1939 #[test]
1940 fn test_translate() {
1941 let mut ctx = GraphicsContext::new();
1942 ctx.translate(50.0, 100.0);
1943 assert!(ctx
1947 .operations()
1948 .contains("1.00 0.00 0.00 1.00 50.00 100.00 cm\n"));
1949 }
1950
1951 #[test]
1952 fn test_scale() {
1953 let mut ctx = GraphicsContext::new();
1954 ctx.scale(2.0, 3.0);
1955 assert!(ctx
1957 .operations()
1958 .contains("2.00 0.00 0.00 3.00 0.00 0.00 cm\n"));
1959 }
1960
1961 #[test]
1962 fn test_rotate() {
1963 let mut ctx = GraphicsContext::new();
1964 let angle = std::f64::consts::PI / 4.0; ctx.rotate(angle);
1966
1967 let ops = ctx.operations();
1968 assert!(ops.contains(" cm\n"));
1969 assert!(ops.contains("0.71"));
1972 }
1973
1974 #[test]
1975 fn test_transform() {
1976 let mut ctx = GraphicsContext::new();
1977 ctx.transform(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
1978 assert!(ctx
1979 .operations()
1980 .contains("1.00 2.00 3.00 4.00 5.00 6.00 cm\n"));
1981 }
1982
1983 #[test]
1984 fn test_draw_image() {
1985 let mut ctx = GraphicsContext::new();
1986 ctx.draw_image("Image1", 10.0, 20.0, 100.0, 150.0);
1987
1988 let ops = ctx.operations();
1989 assert!(ops.contains("q\n")); assert!(ops.contains("100.00 0.00 0.00 150.00 10.00 20.00 cm\n"));
1992 assert!(ops.contains("/Image1 Do\n")); assert!(ops.contains("Q\n")); }
1995
1996 #[test]
1997 fn test_gray_color_operations() {
1998 let mut ctx = GraphicsContext::new();
1999 ctx.set_stroke_color(Color::gray(0.5));
2000 ctx.set_fill_color(Color::gray(0.7));
2001 ctx.stroke();
2002 ctx.fill();
2003
2004 let ops = ctx.operations();
2005 assert!(ops.contains("0.500 G\n")); assert!(ops.contains("0.700 g\n")); }
2008
2009 #[test]
2010 fn test_cmyk_color_operations() {
2011 let mut ctx = GraphicsContext::new();
2012 ctx.set_stroke_color(Color::cmyk(0.1, 0.2, 0.3, 0.4));
2013 ctx.set_fill_color(Color::cmyk(0.5, 0.6, 0.7, 0.8));
2014 ctx.stroke();
2015 ctx.fill();
2016
2017 let ops = ctx.operations();
2018 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")); }
2021
2022 #[test]
2023 fn test_method_chaining() {
2024 let mut ctx = GraphicsContext::new();
2025 ctx.move_to(0.0, 0.0)
2026 .line_to(10.0, 0.0)
2027 .line_to(10.0, 10.0)
2028 .line_to(0.0, 10.0)
2029 .close_path()
2030 .set_fill_color(Color::red())
2031 .fill();
2032
2033 let ops = ctx.operations();
2034 assert!(ops.contains("0.00 0.00 m\n"));
2035 assert!(ops.contains("10.00 0.00 l\n"));
2036 assert!(ops.contains("10.00 10.00 l\n"));
2037 assert!(ops.contains("0.00 10.00 l\n"));
2038 assert!(ops.contains("h\n"));
2039 assert!(ops.contains("f\n"));
2040 }
2041
2042 #[test]
2043 fn test_generate_operations() {
2044 let mut ctx = GraphicsContext::new();
2045 ctx.rect(0.0, 0.0, 10.0, 10.0);
2046
2047 let result = ctx.generate_operations();
2048 assert!(result.is_ok());
2049 let bytes = result.expect("Writing to string should never fail");
2050 let ops_string = String::from_utf8(bytes).expect("Writing to string should never fail");
2051 assert!(ops_string.contains("0.00 0.00 10.00 10.00 re"));
2052 }
2053
2054 #[test]
2055 fn test_clear_operations() {
2056 let mut ctx = GraphicsContext::new();
2057 ctx.rect(0.0, 0.0, 10.0, 10.0);
2058 assert!(!ctx.operations().is_empty());
2059
2060 ctx.clear();
2061 assert!(ctx.operations().is_empty());
2062 }
2063
2064 #[test]
2065 fn test_complex_path() {
2066 let mut ctx = GraphicsContext::new();
2067 ctx.save_state()
2068 .translate(100.0, 100.0)
2069 .rotate(std::f64::consts::PI / 6.0)
2070 .scale(2.0, 2.0)
2071 .set_line_width(2.0)
2072 .set_stroke_color(Color::blue())
2073 .move_to(0.0, 0.0)
2074 .line_to(50.0, 0.0)
2075 .curve_to(50.0, 25.0, 25.0, 50.0, 0.0, 50.0)
2076 .close_path()
2077 .stroke()
2078 .restore_state();
2079
2080 let ops = ctx.operations();
2081 assert!(ops.contains("q\n"));
2082 assert!(ops.contains("cm\n"));
2083 assert!(ops.contains("2.00 w\n"));
2084 assert!(ops.contains("0.000 0.000 1.000 RG\n"));
2085 assert!(ops.contains("S\n"));
2086 assert!(ops.contains("Q\n"));
2087 }
2088
2089 #[test]
2090 fn test_graphics_context_clone() {
2091 let mut ctx = GraphicsContext::new();
2092 ctx.set_fill_color(Color::red());
2093 ctx.set_stroke_color(Color::blue());
2094 ctx.set_line_width(3.0);
2095 ctx.set_opacity(0.5);
2096 ctx.rect(0.0, 0.0, 10.0, 10.0);
2097
2098 let ctx_clone = ctx.clone();
2099 assert_eq!(ctx_clone.fill_color(), Color::red());
2100 assert_eq!(ctx_clone.stroke_color(), Color::blue());
2101 assert_eq!(ctx_clone.line_width(), 3.0);
2102 assert_eq!(ctx_clone.fill_opacity(), 0.5);
2103 assert_eq!(ctx_clone.stroke_opacity(), 0.5);
2104 assert_eq!(ctx_clone.operations(), ctx.operations());
2105 }
2106
2107 #[test]
2108 fn test_set_opacity() {
2109 let mut ctx = GraphicsContext::new();
2110
2111 ctx.set_opacity(0.5);
2113 assert_eq!(ctx.fill_opacity(), 0.5);
2114 assert_eq!(ctx.stroke_opacity(), 0.5);
2115
2116 ctx.set_opacity(1.5);
2118 assert_eq!(ctx.fill_opacity(), 1.0);
2119 assert_eq!(ctx.stroke_opacity(), 1.0);
2120
2121 ctx.set_opacity(-0.5);
2122 assert_eq!(ctx.fill_opacity(), 0.0);
2123 assert_eq!(ctx.stroke_opacity(), 0.0);
2124 }
2125
2126 #[test]
2127 fn test_set_fill_opacity() {
2128 let mut ctx = GraphicsContext::new();
2129
2130 ctx.set_fill_opacity(0.3);
2131 assert_eq!(ctx.fill_opacity(), 0.3);
2132 assert_eq!(ctx.stroke_opacity(), 1.0); ctx.set_fill_opacity(2.0);
2136 assert_eq!(ctx.fill_opacity(), 1.0);
2137 }
2138
2139 #[test]
2140 fn test_set_stroke_opacity() {
2141 let mut ctx = GraphicsContext::new();
2142
2143 ctx.set_stroke_opacity(0.7);
2144 assert_eq!(ctx.stroke_opacity(), 0.7);
2145 assert_eq!(ctx.fill_opacity(), 1.0); ctx.set_stroke_opacity(-1.0);
2149 assert_eq!(ctx.stroke_opacity(), 0.0);
2150 }
2151
2152 #[test]
2153 fn test_uses_transparency() {
2154 let mut ctx = GraphicsContext::new();
2155
2156 assert!(!ctx.uses_transparency());
2158
2159 ctx.set_fill_opacity(0.5);
2161 assert!(ctx.uses_transparency());
2162
2163 ctx.set_fill_opacity(1.0);
2165 assert!(!ctx.uses_transparency());
2166 ctx.set_stroke_opacity(0.8);
2167 assert!(ctx.uses_transparency());
2168
2169 ctx.set_fill_opacity(0.5);
2171 assert!(ctx.uses_transparency());
2172 }
2173
2174 #[test]
2175 fn test_generate_graphics_state_dict() {
2176 let mut ctx = GraphicsContext::new();
2177
2178 assert_eq!(ctx.generate_graphics_state_dict(), None);
2180
2181 ctx.set_fill_opacity(0.5);
2183 let dict = ctx
2184 .generate_graphics_state_dict()
2185 .expect("Writing to string should never fail");
2186 assert!(dict.contains("/Type /ExtGState"));
2187 assert!(dict.contains("/ca 0.500"));
2188 assert!(!dict.contains("/CA"));
2189
2190 ctx.set_fill_opacity(1.0);
2192 ctx.set_stroke_opacity(0.75);
2193 let dict = ctx
2194 .generate_graphics_state_dict()
2195 .expect("Writing to string should never fail");
2196 assert!(dict.contains("/Type /ExtGState"));
2197 assert!(dict.contains("/CA 0.750"));
2198 assert!(!dict.contains("/ca"));
2199
2200 ctx.set_fill_opacity(0.25);
2202 let dict = ctx
2203 .generate_graphics_state_dict()
2204 .expect("Writing to string should never fail");
2205 assert!(dict.contains("/Type /ExtGState"));
2206 assert!(dict.contains("/ca 0.250"));
2207 assert!(dict.contains("/CA 0.750"));
2208 }
2209
2210 #[test]
2211 fn test_opacity_with_graphics_operations() {
2212 let mut ctx = GraphicsContext::new();
2213
2214 ctx.set_fill_color(Color::red())
2215 .set_opacity(0.5)
2216 .rect(10.0, 10.0, 100.0, 100.0)
2217 .fill();
2218
2219 assert_eq!(ctx.fill_opacity(), 0.5);
2220 assert_eq!(ctx.stroke_opacity(), 0.5);
2221
2222 let ops = ctx.operations();
2223 assert!(ops.contains("10.00 10.00 100.00 100.00 re"));
2224 assert!(ops.contains("1.000 0.000 0.000 rg")); assert!(ops.contains("f")); }
2227
2228 #[test]
2229 fn test_begin_end_text() {
2230 let mut ctx = GraphicsContext::new();
2231 ctx.begin_text();
2232 assert!(ctx.operations().contains("BT\n"));
2233
2234 ctx.end_text();
2235 assert!(ctx.operations().contains("ET\n"));
2236 }
2237
2238 #[test]
2239 fn test_set_font() {
2240 let mut ctx = GraphicsContext::new();
2241 ctx.set_font(Font::Helvetica, 12.0);
2242 assert!(ctx.operations().contains("/Helvetica 12 Tf\n"));
2243
2244 ctx.set_font(Font::TimesBold, 14.5);
2245 assert!(ctx.operations().contains("/Times-Bold 14.5 Tf\n"));
2246 }
2247
2248 #[test]
2249 fn test_set_text_position() {
2250 let mut ctx = GraphicsContext::new();
2251 ctx.set_text_position(100.0, 200.0);
2252 assert!(ctx.operations().contains("100.00 200.00 Td\n"));
2253 }
2254
2255 #[test]
2256 fn test_show_text() {
2257 let mut ctx = GraphicsContext::new();
2258 ctx.show_text("Hello World")
2259 .expect("Writing to string should never fail");
2260 assert!(ctx.operations().contains("(Hello World) Tj\n"));
2261 }
2262
2263 #[test]
2264 fn test_show_text_with_escaping() {
2265 let mut ctx = GraphicsContext::new();
2266 ctx.show_text("Test (parentheses)")
2267 .expect("Writing to string should never fail");
2268 assert!(ctx.operations().contains("(Test \\(parentheses\\)) Tj\n"));
2269
2270 ctx.clear();
2271 ctx.show_text("Back\\slash")
2272 .expect("Writing to string should never fail");
2273 assert!(ctx.operations().contains("(Back\\\\slash) Tj\n"));
2274
2275 ctx.clear();
2276 ctx.show_text("Line\nBreak")
2277 .expect("Writing to string should never fail");
2278 assert!(ctx.operations().contains("(Line\\nBreak) Tj\n"));
2279 }
2280
2281 #[test]
2282 fn test_text_operations_chaining() {
2283 let mut ctx = GraphicsContext::new();
2284 ctx.begin_text()
2285 .set_font(Font::Courier, 10.0)
2286 .set_text_position(50.0, 100.0)
2287 .show_text("Test")
2288 .unwrap()
2289 .end_text();
2290
2291 let ops = ctx.operations();
2292 assert!(ops.contains("BT\n"));
2293 assert!(ops.contains("/Courier 10 Tf\n"));
2294 assert!(ops.contains("50.00 100.00 Td\n"));
2295 assert!(ops.contains("(Test) Tj\n"));
2296 assert!(ops.contains("ET\n"));
2297 }
2298
2299 #[test]
2300 fn test_clip() {
2301 let mut ctx = GraphicsContext::new();
2302 ctx.clip();
2303 assert!(ctx.operations().contains("W\n"));
2304 }
2305
2306 #[test]
2307 fn test_clip_even_odd() {
2308 let mut ctx = GraphicsContext::new();
2309 ctx.clip_even_odd();
2310 assert!(ctx.operations().contains("W*\n"));
2311 }
2312
2313 #[test]
2314 fn test_clipping_with_path() {
2315 let mut ctx = GraphicsContext::new();
2316
2317 ctx.rect(10.0, 10.0, 100.0, 50.0).clip();
2319
2320 let ops = ctx.operations();
2321 assert!(ops.contains("10.00 10.00 100.00 50.00 re\n"));
2322 assert!(ops.contains("W\n"));
2323 }
2324
2325 #[test]
2326 fn test_clipping_even_odd_with_path() {
2327 let mut ctx = GraphicsContext::new();
2328
2329 ctx.move_to(0.0, 0.0)
2331 .line_to(100.0, 0.0)
2332 .line_to(100.0, 100.0)
2333 .line_to(0.0, 100.0)
2334 .close_path()
2335 .clip_even_odd();
2336
2337 let ops = ctx.operations();
2338 assert!(ops.contains("0.00 0.00 m\n"));
2339 assert!(ops.contains("100.00 0.00 l\n"));
2340 assert!(ops.contains("100.00 100.00 l\n"));
2341 assert!(ops.contains("0.00 100.00 l\n"));
2342 assert!(ops.contains("h\n"));
2343 assert!(ops.contains("W*\n"));
2344 }
2345
2346 #[test]
2347 fn test_clipping_chaining() {
2348 let mut ctx = GraphicsContext::new();
2349
2350 ctx.save_state()
2352 .rect(20.0, 20.0, 60.0, 60.0)
2353 .clip()
2354 .set_fill_color(Color::red())
2355 .rect(0.0, 0.0, 100.0, 100.0)
2356 .fill()
2357 .restore_state();
2358
2359 let ops = ctx.operations();
2360 assert!(ops.contains("q\n"));
2361 assert!(ops.contains("20.00 20.00 60.00 60.00 re\n"));
2362 assert!(ops.contains("W\n"));
2363 assert!(ops.contains("1.000 0.000 0.000 rg\n"));
2364 assert!(ops.contains("0.00 0.00 100.00 100.00 re\n"));
2365 assert!(ops.contains("f\n"));
2366 assert!(ops.contains("Q\n"));
2367 }
2368
2369 #[test]
2370 fn test_multiple_clipping_regions() {
2371 let mut ctx = GraphicsContext::new();
2372
2373 ctx.save_state()
2375 .rect(0.0, 0.0, 200.0, 200.0)
2376 .clip()
2377 .save_state()
2378 .circle(100.0, 100.0, 50.0)
2379 .clip_even_odd()
2380 .set_fill_color(Color::blue())
2381 .rect(50.0, 50.0, 100.0, 100.0)
2382 .fill()
2383 .restore_state()
2384 .restore_state();
2385
2386 let ops = ctx.operations();
2387 let q_count = ops.matches("q\n").count();
2389 let q_restore_count = ops.matches("Q\n").count();
2390 assert_eq!(q_count, 2);
2391 assert_eq!(q_restore_count, 2);
2392
2393 assert!(ops.contains("W\n"));
2395 assert!(ops.contains("W*\n"));
2396 }
2397
2398 #[test]
2401 fn test_move_to_and_line_to() {
2402 let mut ctx = GraphicsContext::new();
2403 ctx.move_to(100.0, 200.0).line_to(300.0, 400.0).stroke();
2404
2405 let ops = ctx
2406 .generate_operations()
2407 .expect("Writing to string should never fail");
2408 let ops_str = String::from_utf8_lossy(&ops);
2409 assert!(ops_str.contains("100.00 200.00 m"));
2410 assert!(ops_str.contains("300.00 400.00 l"));
2411 assert!(ops_str.contains("S"));
2412 }
2413
2414 #[test]
2415 fn test_bezier_curve() {
2416 let mut ctx = GraphicsContext::new();
2417 ctx.move_to(0.0, 0.0)
2418 .curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0)
2419 .stroke();
2420
2421 let ops = ctx
2422 .generate_operations()
2423 .expect("Writing to string should never fail");
2424 let ops_str = String::from_utf8_lossy(&ops);
2425 assert!(ops_str.contains("0.00 0.00 m"));
2426 assert!(ops_str.contains("10.00 20.00 30.00 40.00 50.00 60.00 c"));
2427 assert!(ops_str.contains("S"));
2428 }
2429
2430 #[test]
2431 fn test_circle_path() {
2432 let mut ctx = GraphicsContext::new();
2433 ctx.circle(100.0, 100.0, 50.0).fill();
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(" c"));
2441 assert!(ops_str.contains("f"));
2442 }
2443
2444 #[test]
2445 fn test_path_closing() {
2446 let mut ctx = GraphicsContext::new();
2447 ctx.move_to(0.0, 0.0)
2448 .line_to(100.0, 0.0)
2449 .line_to(100.0, 100.0)
2450 .close_path()
2451 .stroke();
2452
2453 let ops = ctx
2454 .generate_operations()
2455 .expect("Writing to string should never fail");
2456 let ops_str = String::from_utf8_lossy(&ops);
2457 assert!(ops_str.contains("h")); assert!(ops_str.contains("S"));
2459 }
2460
2461 #[test]
2462 fn test_fill_and_stroke() {
2463 let mut ctx = GraphicsContext::new();
2464 ctx.rect(10.0, 10.0, 50.0, 50.0).fill_stroke();
2465
2466 let ops = ctx
2467 .generate_operations()
2468 .expect("Writing to string should never fail");
2469 let ops_str = String::from_utf8_lossy(&ops);
2470 assert!(ops_str.contains("10.00 10.00 50.00 50.00 re"));
2471 assert!(ops_str.contains("B")); }
2473
2474 #[test]
2475 fn test_color_settings() {
2476 let mut ctx = GraphicsContext::new();
2477 ctx.set_fill_color(Color::rgb(1.0, 0.0, 0.0))
2478 .set_stroke_color(Color::rgb(0.0, 1.0, 0.0))
2479 .rect(10.0, 10.0, 50.0, 50.0)
2480 .fill_stroke(); assert_eq!(ctx.fill_color(), Color::rgb(1.0, 0.0, 0.0));
2483 assert_eq!(ctx.stroke_color(), Color::rgb(0.0, 1.0, 0.0));
2484
2485 let ops = ctx
2486 .generate_operations()
2487 .expect("Writing to string should never fail");
2488 let ops_str = String::from_utf8_lossy(&ops);
2489 assert!(ops_str.contains("1.000 0.000 0.000 rg")); assert!(ops_str.contains("0.000 1.000 0.000 RG")); }
2492
2493 #[test]
2494 fn test_line_styles() {
2495 let mut ctx = GraphicsContext::new();
2496 ctx.set_line_width(2.5)
2497 .set_line_cap(LineCap::Round)
2498 .set_line_join(LineJoin::Bevel);
2499
2500 assert_eq!(ctx.line_width(), 2.5);
2501
2502 let ops = ctx
2503 .generate_operations()
2504 .expect("Writing to string should never fail");
2505 let ops_str = String::from_utf8_lossy(&ops);
2506 assert!(ops_str.contains("2.50 w")); assert!(ops_str.contains("1 J")); assert!(ops_str.contains("2 j")); }
2510
2511 #[test]
2512 fn test_opacity_settings() {
2513 let mut ctx = GraphicsContext::new();
2514 ctx.set_opacity(0.5);
2515
2516 assert_eq!(ctx.fill_opacity(), 0.5);
2517 assert_eq!(ctx.stroke_opacity(), 0.5);
2518 assert!(ctx.uses_transparency());
2519
2520 ctx.set_fill_opacity(0.7).set_stroke_opacity(0.3);
2521
2522 assert_eq!(ctx.fill_opacity(), 0.7);
2523 assert_eq!(ctx.stroke_opacity(), 0.3);
2524 }
2525
2526 #[test]
2527 fn test_state_save_restore() {
2528 let mut ctx = GraphicsContext::new();
2529 ctx.save_state()
2530 .set_fill_color(Color::rgb(1.0, 0.0, 0.0))
2531 .restore_state();
2532
2533 let ops = ctx
2534 .generate_operations()
2535 .expect("Writing to string should never fail");
2536 let ops_str = String::from_utf8_lossy(&ops);
2537 assert!(ops_str.contains("q")); assert!(ops_str.contains("Q")); }
2540
2541 #[test]
2542 fn test_transformations() {
2543 let mut ctx = GraphicsContext::new();
2544 ctx.translate(100.0, 200.0).scale(2.0, 3.0).rotate(45.0);
2545
2546 let ops = ctx
2547 .generate_operations()
2548 .expect("Writing to string should never fail");
2549 let ops_str = String::from_utf8_lossy(&ops);
2550 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")); }
2556
2557 #[test]
2558 fn test_custom_transform() {
2559 let mut ctx = GraphicsContext::new();
2560 ctx.transform(1.0, 0.5, 0.5, 1.0, 10.0, 20.0);
2561
2562 let ops = ctx
2563 .generate_operations()
2564 .expect("Writing to string should never fail");
2565 let ops_str = String::from_utf8_lossy(&ops);
2566 assert!(ops_str.contains("1.00 0.50 0.50 1.00 10.00 20.00 cm"));
2567 }
2568
2569 #[test]
2570 fn test_rectangle_path() {
2571 let mut ctx = GraphicsContext::new();
2572 ctx.rectangle(25.0, 25.0, 150.0, 100.0).stroke();
2573
2574 let ops = ctx
2575 .generate_operations()
2576 .expect("Writing to string should never fail");
2577 let ops_str = String::from_utf8_lossy(&ops);
2578 assert!(ops_str.contains("25.00 25.00 150.00 100.00 re"));
2579 assert!(ops_str.contains("S"));
2580 }
2581
2582 #[test]
2583 fn test_empty_operations() {
2584 let ctx = GraphicsContext::new();
2585 let ops = ctx
2586 .generate_operations()
2587 .expect("Writing to string should never fail");
2588 assert!(ops.is_empty());
2589 }
2590
2591 #[test]
2592 fn test_complex_path_operations() {
2593 let mut ctx = GraphicsContext::new();
2594 ctx.move_to(50.0, 50.0)
2595 .line_to(100.0, 50.0)
2596 .curve_to(125.0, 50.0, 150.0, 75.0, 150.0, 100.0)
2597 .line_to(150.0, 150.0)
2598 .close_path()
2599 .fill();
2600
2601 let ops = ctx
2602 .generate_operations()
2603 .expect("Writing to string should never fail");
2604 let ops_str = String::from_utf8_lossy(&ops);
2605 assert!(ops_str.contains("50.00 50.00 m"));
2606 assert!(ops_str.contains("100.00 50.00 l"));
2607 assert!(ops_str.contains("125.00 50.00 150.00 75.00 150.00 100.00 c"));
2608 assert!(ops_str.contains("150.00 150.00 l"));
2609 assert!(ops_str.contains("h"));
2610 assert!(ops_str.contains("f"));
2611 }
2612
2613 #[test]
2614 fn test_graphics_state_dict_generation() {
2615 let mut ctx = GraphicsContext::new();
2616
2617 assert!(ctx.generate_graphics_state_dict().is_none());
2619
2620 ctx.set_opacity(0.5);
2622 let dict = ctx.generate_graphics_state_dict();
2623 assert!(dict.is_some());
2624 let dict_str = dict.expect("Writing to string should never fail");
2625 assert!(dict_str.contains("/ca 0.5"));
2626 assert!(dict_str.contains("/CA 0.5"));
2627 }
2628
2629 #[test]
2630 fn test_line_dash_pattern() {
2631 let mut ctx = GraphicsContext::new();
2632 let pattern = LineDashPattern {
2633 array: vec![3.0, 2.0],
2634 phase: 0.0,
2635 };
2636 ctx.set_line_dash_pattern(pattern);
2637
2638 let ops = ctx
2639 .generate_operations()
2640 .expect("Writing to string should never fail");
2641 let ops_str = String::from_utf8_lossy(&ops);
2642 assert!(ops_str.contains("[3.00 2.00] 0.00 d"));
2643 }
2644
2645 #[test]
2646 fn test_miter_limit_setting() {
2647 let mut ctx = GraphicsContext::new();
2648 ctx.set_miter_limit(4.0);
2649
2650 let ops = ctx
2651 .generate_operations()
2652 .expect("Writing to string should never fail");
2653 let ops_str = String::from_utf8_lossy(&ops);
2654 assert!(ops_str.contains("4.00 M"));
2655 }
2656
2657 #[test]
2658 fn test_line_cap_styles() {
2659 let mut ctx = GraphicsContext::new();
2660
2661 ctx.set_line_cap(LineCap::Butt);
2662 let ops = ctx
2663 .generate_operations()
2664 .expect("Writing to string should never fail");
2665 let ops_str = String::from_utf8_lossy(&ops);
2666 assert!(ops_str.contains("0 J"));
2667
2668 let mut ctx = GraphicsContext::new();
2669 ctx.set_line_cap(LineCap::Round);
2670 let ops = ctx
2671 .generate_operations()
2672 .expect("Writing to string should never fail");
2673 let ops_str = String::from_utf8_lossy(&ops);
2674 assert!(ops_str.contains("1 J"));
2675
2676 let mut ctx = GraphicsContext::new();
2677 ctx.set_line_cap(LineCap::Square);
2678 let ops = ctx
2679 .generate_operations()
2680 .expect("Writing to string should never fail");
2681 let ops_str = String::from_utf8_lossy(&ops);
2682 assert!(ops_str.contains("2 J"));
2683 }
2684
2685 #[test]
2686 fn test_transparency_groups() {
2687 let mut ctx = GraphicsContext::new();
2688
2689 let group = TransparencyGroup::new()
2691 .with_isolated(true)
2692 .with_opacity(0.5);
2693
2694 ctx.begin_transparency_group(group);
2695 assert!(ctx.in_transparency_group());
2696
2697 ctx.rect(10.0, 10.0, 100.0, 100.0);
2699 ctx.fill();
2700
2701 ctx.end_transparency_group();
2702 assert!(!ctx.in_transparency_group());
2703
2704 let ops = ctx.operations();
2706 assert!(ops.contains("% Begin Transparency Group"));
2707 assert!(ops.contains("% End Transparency Group"));
2708 }
2709
2710 #[test]
2711 fn test_nested_transparency_groups() {
2712 let mut ctx = GraphicsContext::new();
2713
2714 let group1 = TransparencyGroup::isolated().with_opacity(0.8);
2716 ctx.begin_transparency_group(group1);
2717 assert!(ctx.in_transparency_group());
2718
2719 let group2 = TransparencyGroup::knockout().with_blend_mode(BlendMode::Multiply);
2721 ctx.begin_transparency_group(group2);
2722
2723 ctx.circle(50.0, 50.0, 25.0);
2725 ctx.fill();
2726
2727 ctx.end_transparency_group();
2729 assert!(ctx.in_transparency_group()); ctx.end_transparency_group();
2733 assert!(!ctx.in_transparency_group());
2734 }
2735
2736 #[test]
2737 fn test_line_join_styles() {
2738 let mut ctx = GraphicsContext::new();
2739
2740 ctx.set_line_join(LineJoin::Miter);
2741 let ops = ctx
2742 .generate_operations()
2743 .expect("Writing to string should never fail");
2744 let ops_str = String::from_utf8_lossy(&ops);
2745 assert!(ops_str.contains("0 j"));
2746
2747 let mut ctx = GraphicsContext::new();
2748 ctx.set_line_join(LineJoin::Round);
2749 let ops = ctx
2750 .generate_operations()
2751 .expect("Writing to string should never fail");
2752 let ops_str = String::from_utf8_lossy(&ops);
2753 assert!(ops_str.contains("1 j"));
2754
2755 let mut ctx = GraphicsContext::new();
2756 ctx.set_line_join(LineJoin::Bevel);
2757 let ops = ctx
2758 .generate_operations()
2759 .expect("Writing to string should never fail");
2760 let ops_str = String::from_utf8_lossy(&ops);
2761 assert!(ops_str.contains("2 j"));
2762 }
2763
2764 #[test]
2765 fn test_rendering_intent() {
2766 let mut ctx = GraphicsContext::new();
2767
2768 ctx.set_rendering_intent(RenderingIntent::AbsoluteColorimetric);
2769 assert_eq!(
2770 ctx.rendering_intent(),
2771 RenderingIntent::AbsoluteColorimetric
2772 );
2773
2774 ctx.set_rendering_intent(RenderingIntent::Perceptual);
2775 assert_eq!(ctx.rendering_intent(), RenderingIntent::Perceptual);
2776
2777 ctx.set_rendering_intent(RenderingIntent::Saturation);
2778 assert_eq!(ctx.rendering_intent(), RenderingIntent::Saturation);
2779 }
2780
2781 #[test]
2782 fn test_flatness_tolerance() {
2783 let mut ctx = GraphicsContext::new();
2784
2785 ctx.set_flatness(0.5);
2786 assert_eq!(ctx.flatness(), 0.5);
2787
2788 let ops = ctx
2789 .generate_operations()
2790 .expect("Writing to string should never fail");
2791 let ops_str = String::from_utf8_lossy(&ops);
2792 assert!(ops_str.contains("0.50 i"));
2793 }
2794
2795 #[test]
2796 fn test_smoothness_tolerance() {
2797 let mut ctx = GraphicsContext::new();
2798
2799 let _ = ctx.set_smoothness(0.1);
2800 assert_eq!(ctx.smoothness(), 0.1);
2801 }
2802
2803 #[test]
2804 fn test_bezier_curves() {
2805 let mut ctx = GraphicsContext::new();
2806
2807 ctx.move_to(10.0, 10.0);
2809 ctx.curve_to(20.0, 10.0, 30.0, 20.0, 30.0, 30.0);
2810
2811 let ops = ctx
2812 .generate_operations()
2813 .expect("Writing to string should never fail");
2814 let ops_str = String::from_utf8_lossy(&ops);
2815 assert!(ops_str.contains("10.00 10.00 m"));
2816 assert!(ops_str.contains("c")); }
2818
2819 #[test]
2820 fn test_clipping_path() {
2821 let mut ctx = GraphicsContext::new();
2822
2823 ctx.rectangle(10.0, 10.0, 100.0, 100.0);
2824 ctx.clip();
2825
2826 let ops = ctx
2827 .generate_operations()
2828 .expect("Writing to string should never fail");
2829 let ops_str = String::from_utf8_lossy(&ops);
2830 assert!(ops_str.contains("W"));
2831 }
2832
2833 #[test]
2834 fn test_even_odd_clipping() {
2835 let mut ctx = GraphicsContext::new();
2836
2837 ctx.rectangle(10.0, 10.0, 100.0, 100.0);
2838 ctx.clip_even_odd();
2839
2840 let ops = ctx
2841 .generate_operations()
2842 .expect("Writing to string should never fail");
2843 let ops_str = String::from_utf8_lossy(&ops);
2844 assert!(ops_str.contains("W*"));
2845 }
2846
2847 #[test]
2848 fn test_color_creation() {
2849 let gray = Color::gray(0.5);
2851 assert_eq!(gray, Color::Gray(0.5));
2852
2853 let rgb = Color::rgb(0.2, 0.4, 0.6);
2854 assert_eq!(rgb, Color::Rgb(0.2, 0.4, 0.6));
2855
2856 let cmyk = Color::cmyk(0.1, 0.2, 0.3, 0.4);
2857 assert_eq!(cmyk, Color::Cmyk(0.1, 0.2, 0.3, 0.4));
2858
2859 assert_eq!(Color::black(), Color::Gray(0.0));
2861 assert_eq!(Color::white(), Color::Gray(1.0));
2862 assert_eq!(Color::red(), Color::Rgb(1.0, 0.0, 0.0));
2863 }
2864
2865 #[test]
2866 fn test_extended_graphics_state() {
2867 let ctx = GraphicsContext::new();
2868
2869 let _extgstate = ExtGState::new();
2871
2872 assert!(ctx.generate_operations().is_ok());
2874 }
2875
2876 #[test]
2877 fn test_path_construction_methods() {
2878 let mut ctx = GraphicsContext::new();
2879
2880 ctx.move_to(10.0, 10.0);
2882 ctx.line_to(20.0, 20.0);
2883 ctx.curve_to(30.0, 30.0, 40.0, 40.0, 50.0, 50.0);
2884 ctx.rect(60.0, 60.0, 30.0, 30.0);
2885 ctx.circle(100.0, 100.0, 25.0);
2886 ctx.close_path();
2887
2888 let ops = ctx
2889 .generate_operations()
2890 .expect("Writing to string should never fail");
2891 assert!(!ops.is_empty());
2892 }
2893
2894 #[test]
2895 fn test_graphics_context_clone_advanced() {
2896 let mut ctx = GraphicsContext::new();
2897 ctx.set_fill_color(Color::rgb(1.0, 0.0, 0.0));
2898 ctx.set_line_width(5.0);
2899
2900 let cloned = ctx.clone();
2901 assert_eq!(cloned.fill_color(), Color::rgb(1.0, 0.0, 0.0));
2902 assert_eq!(cloned.line_width(), 5.0);
2903 }
2904
2905 #[test]
2906 fn test_basic_drawing_operations() {
2907 let mut ctx = GraphicsContext::new();
2908
2909 ctx.move_to(50.0, 50.0);
2911 ctx.line_to(100.0, 100.0);
2912 ctx.stroke();
2913
2914 let ops = ctx
2915 .generate_operations()
2916 .expect("Writing to string should never fail");
2917 let ops_str = String::from_utf8_lossy(&ops);
2918 assert!(ops_str.contains("m")); assert!(ops_str.contains("l")); assert!(ops_str.contains("S")); }
2922
2923 #[test]
2924 fn test_graphics_state_stack() {
2925 let mut ctx = GraphicsContext::new();
2926
2927 ctx.set_fill_color(Color::black());
2929
2930 ctx.save_state();
2932 ctx.set_fill_color(Color::red());
2933 assert_eq!(ctx.fill_color(), Color::red());
2934
2935 ctx.save_state();
2937 ctx.set_fill_color(Color::blue());
2938 assert_eq!(ctx.fill_color(), Color::blue());
2939
2940 ctx.restore_state();
2942 assert_eq!(ctx.fill_color(), Color::red());
2943
2944 ctx.restore_state();
2946 assert_eq!(ctx.fill_color(), Color::black());
2947 }
2948
2949 #[test]
2950 fn test_word_spacing() {
2951 let mut ctx = GraphicsContext::new();
2952 ctx.set_word_spacing(2.5);
2953
2954 let ops = ctx.generate_operations().unwrap();
2955 let ops_str = String::from_utf8_lossy(&ops);
2956 assert!(ops_str.contains("2.50 Tw"));
2957 }
2958
2959 #[test]
2960 fn test_character_spacing() {
2961 let mut ctx = GraphicsContext::new();
2962 ctx.set_character_spacing(1.0);
2963
2964 let ops = ctx.generate_operations().unwrap();
2965 let ops_str = String::from_utf8_lossy(&ops);
2966 assert!(ops_str.contains("1.00 Tc"));
2967 }
2968
2969 #[test]
2970 fn test_justified_text() {
2971 let mut ctx = GraphicsContext::new();
2972 ctx.begin_text();
2973 ctx.set_text_position(100.0, 200.0);
2974 ctx.show_justified_text("Hello world from PDF", 200.0)
2975 .unwrap();
2976 ctx.end_text();
2977
2978 let ops = ctx.generate_operations().unwrap();
2979 let ops_str = String::from_utf8_lossy(&ops);
2980
2981 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")); }
2990
2991 #[test]
2992 fn test_justified_text_single_word() {
2993 let mut ctx = GraphicsContext::new();
2994 ctx.begin_text();
2995 ctx.show_justified_text("Hello", 200.0).unwrap();
2996 ctx.end_text();
2997
2998 let ops = ctx.generate_operations().unwrap();
2999 let ops_str = String::from_utf8_lossy(&ops);
3000
3001 assert!(ops_str.contains("(Hello) Tj"));
3003 assert_eq!(ops_str.matches("Tw").count(), 0);
3005 }
3006
3007 #[test]
3008 fn test_text_width_estimation() {
3009 let ctx = GraphicsContext::new();
3010 let width = ctx.estimate_text_width_simple("Hello");
3011
3012 assert!(width > 0.0);
3014 assert_eq!(width, 5.0 * 12.0 * 0.6); }
3016
3017 #[test]
3018 fn test_set_alpha_methods() {
3019 let mut ctx = GraphicsContext::new();
3020
3021 assert!(ctx.set_alpha(0.5).is_ok());
3023 assert!(ctx.set_alpha_fill(0.3).is_ok());
3024 assert!(ctx.set_alpha_stroke(0.7).is_ok());
3025
3026 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
3034 .set_alpha(0.5)
3035 .and_then(|c| c.set_alpha_fill(0.3))
3036 .and_then(|c| c.set_alpha_stroke(0.7));
3037 assert!(result.is_ok());
3038 }
3039
3040 #[test]
3041 fn test_alpha_methods_generate_extgstate() {
3042 let mut ctx = GraphicsContext::new();
3043
3044 ctx.set_alpha(0.5).unwrap();
3046
3047 ctx.rect(10.0, 10.0, 50.0, 50.0).fill();
3049
3050 let ops = ctx.generate_operations().unwrap();
3051 let ops_str = String::from_utf8_lossy(&ops);
3052
3053 assert!(ops_str.contains("/GS")); assert!(ops_str.contains(" gs\n")); ctx.clear();
3059 ctx.set_alpha_fill(0.3).unwrap();
3060 ctx.set_alpha_stroke(0.8).unwrap();
3061 ctx.rect(20.0, 20.0, 60.0, 60.0).fill_stroke();
3062
3063 let ops2 = ctx.generate_operations().unwrap();
3064 let ops_str2 = String::from_utf8_lossy(&ops2);
3065
3066 assert!(ops_str2.contains("/GS")); assert!(ops_str2.contains(" gs\n")); }
3070
3071 #[test]
3072 fn test_add_command() {
3073 let mut ctx = GraphicsContext::new();
3074
3075 ctx.add_command("1 0 0 1 100 200 cm");
3077 let ops = ctx.operations();
3078 assert!(ops.contains("1 0 0 1 100 200 cm\n"));
3079
3080 ctx.clear();
3082 ctx.add_command("q");
3083 assert_eq!(ctx.operations(), "q\n");
3084
3085 ctx.clear();
3087 ctx.add_command("");
3088 assert_eq!(ctx.operations(), "\n");
3089
3090 ctx.clear();
3092 ctx.add_command("Q\n");
3093 assert_eq!(ctx.operations(), "Q\n\n"); ctx.clear();
3097 ctx.add_command("q");
3098 ctx.add_command("1 0 0 1 50 50 cm");
3099 ctx.add_command("Q");
3100 assert_eq!(ctx.operations(), "q\n1 0 0 1 50 50 cm\nQ\n");
3101 }
3102
3103 #[test]
3104 fn test_get_operations() {
3105 let mut ctx = GraphicsContext::new();
3106 ctx.rect(10.0, 10.0, 50.0, 50.0);
3107 let ops1 = ctx.operations();
3108 let ops2 = ctx.get_operations();
3109 assert_eq!(ops1, ops2);
3110 }
3111
3112 #[test]
3113 fn test_set_line_solid() {
3114 let mut ctx = GraphicsContext::new();
3115 ctx.set_line_dash_pattern(LineDashPattern::new(vec![5.0, 3.0], 0.0));
3116 ctx.set_line_solid();
3117 let ops = ctx.operations();
3118 assert!(ops.contains("[] 0 d\n"));
3119 }
3120
3121 #[test]
3122 fn test_set_custom_font() {
3123 let mut ctx = GraphicsContext::new();
3124 ctx.set_custom_font("CustomFont", 14.0);
3125 assert_eq!(ctx.current_font_name.as_deref(), Some("CustomFont"));
3126 assert_eq!(ctx.current_font_size, 14.0);
3127 assert!(ctx.is_custom_font);
3128 }
3129
3130 #[test]
3131 fn test_show_text_standard_font_uses_literal_string() {
3132 let mut ctx = GraphicsContext::new();
3133 ctx.set_font(Font::Helvetica, 12.0);
3134 assert!(!ctx.is_custom_font);
3135
3136 ctx.begin_text();
3137 ctx.set_text_position(10.0, 20.0);
3138 ctx.show_text("Hello World").unwrap();
3139 ctx.end_text();
3140
3141 let ops = ctx.operations();
3142 assert!(ops.contains("(Hello World) Tj"));
3143 assert!(!ops.contains("<"));
3144 }
3145
3146 #[test]
3147 fn test_show_text_custom_font_uses_hex_encoding() {
3148 let mut ctx = GraphicsContext::new();
3149 ctx.set_font(Font::Custom("NotoSansCJK".to_string()), 12.0);
3150 assert!(ctx.is_custom_font);
3151
3152 ctx.begin_text();
3153 ctx.set_text_position(10.0, 20.0);
3154 ctx.show_text("你好").unwrap();
3156 ctx.end_text();
3157
3158 let ops = ctx.operations();
3159 assert!(
3161 ops.contains("<4F60597D> Tj"),
3162 "Expected hex encoding for CJK text, got: {}",
3163 ops
3164 );
3165 assert!(!ops.contains("(你好)"));
3166 }
3167
3168 #[test]
3169 fn test_show_text_custom_font_ascii_still_hex() {
3170 let mut ctx = GraphicsContext::new();
3171 ctx.set_font(Font::Custom("MyFont".to_string()), 10.0);
3172
3173 ctx.begin_text();
3174 ctx.set_text_position(0.0, 0.0);
3175 ctx.show_text("AB").unwrap();
3177 ctx.end_text();
3178
3179 let ops = ctx.operations();
3180 assert!(
3182 ops.contains("<00410042> Tj"),
3183 "Expected hex encoding for ASCII in custom font, got: {}",
3184 ops
3185 );
3186 }
3187
3188 #[test]
3189 fn test_show_text_tracks_used_characters() {
3190 let mut ctx = GraphicsContext::new();
3191 ctx.set_font(Font::Custom("CJKFont".to_string()), 12.0);
3192
3193 ctx.begin_text();
3194 ctx.show_text("你好A").unwrap();
3195 ctx.end_text();
3196
3197 let chars = ctx
3198 .get_used_characters()
3199 .expect("show_text with a custom font must record characters");
3200 assert!(chars.contains(&'你'));
3201 assert!(chars.contains(&'好'));
3202 assert!(chars.contains(&'A'));
3203 }
3204
3205 #[test]
3206 fn test_is_custom_font_toggles_correctly() {
3207 let mut ctx = GraphicsContext::new();
3208 assert!(!ctx.is_custom_font);
3209
3210 ctx.set_font(Font::Custom("CJK".to_string()), 12.0);
3211 assert!(ctx.is_custom_font);
3212
3213 ctx.set_font(Font::Helvetica, 12.0);
3214 assert!(!ctx.is_custom_font);
3215
3216 ctx.set_custom_font("AnotherCJK", 14.0);
3217 assert!(ctx.is_custom_font);
3218
3219 ctx.set_font(Font::CourierBold, 10.0);
3220 assert!(!ctx.is_custom_font);
3221 }
3222
3223 #[test]
3224 fn test_set_glyph_mapping() {
3225 let mut ctx = GraphicsContext::new();
3226
3227 assert!(ctx.glyph_mapping.is_none());
3229
3230 let mut mapping = HashMap::new();
3232 mapping.insert(65u32, 1u16); mapping.insert(66u32, 2u16); ctx.set_glyph_mapping(mapping.clone());
3235 assert!(ctx.glyph_mapping.is_some());
3236 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().len(), 2);
3237 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().get(&65), Some(&1));
3238 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().get(&66), Some(&2));
3239
3240 ctx.set_glyph_mapping(HashMap::new());
3242 assert!(ctx.glyph_mapping.is_some());
3243 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().len(), 0);
3244
3245 let mut new_mapping = HashMap::new();
3247 new_mapping.insert(67u32, 3u16); ctx.set_glyph_mapping(new_mapping);
3249 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().len(), 1);
3250 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().get(&67), Some(&3));
3251 assert_eq!(ctx.glyph_mapping.as_ref().unwrap().get(&65), None); }
3253
3254 #[test]
3255 fn test_draw_text_basic() {
3256 let mut ctx = GraphicsContext::new();
3257 ctx.set_font(Font::Helvetica, 12.0);
3258
3259 let result = ctx.draw_text("Hello", 100.0, 200.0);
3260 assert!(result.is_ok());
3261
3262 let ops = ctx.operations();
3263 assert!(ops.contains("BT\n"));
3265 assert!(ops.contains("ET\n"));
3266
3267 assert!(ops.contains("/Helvetica"));
3269 assert!(ops.contains("12"));
3270 assert!(ops.contains("Tf\n"));
3271
3272 assert!(ops.contains("100"));
3274 assert!(ops.contains("200"));
3275 assert!(ops.contains("Td\n"));
3276
3277 assert!(ops.contains("(Hello)") || ops.contains("<48656c6c6f>")); }
3280
3281 #[test]
3282 fn test_draw_text_with_special_characters() {
3283 let mut ctx = GraphicsContext::new();
3284 ctx.set_font(Font::Helvetica, 12.0);
3285
3286 let result = ctx.draw_text("Test (with) parens", 50.0, 100.0);
3288 assert!(result.is_ok());
3289
3290 let ops = ctx.operations();
3291 assert!(ops.contains("\\(") || ops.contains("\\)") || ops.contains("<"));
3293 }
3295
3296 #[test]
3297 fn test_draw_text_unicode_detection() {
3298 let mut ctx = GraphicsContext::new();
3299 ctx.set_font(Font::Helvetica, 12.0);
3300
3301 ctx.draw_text("ASCII", 0.0, 0.0).unwrap();
3303 let _ops_ascii = ctx.operations();
3304
3305 ctx.clear();
3306
3307 ctx.set_font(Font::Helvetica, 12.0);
3309 ctx.draw_text("中文", 0.0, 0.0).unwrap();
3310 let ops_unicode = ctx.operations();
3311
3312 assert!(ops_unicode.contains("<") && ops_unicode.contains(">"));
3314 }
3315
3316 #[test]
3317 #[allow(deprecated)]
3318 fn test_draw_text_hex_encoding() {
3319 let mut ctx = GraphicsContext::new();
3320 ctx.set_font(Font::Helvetica, 12.0);
3321 let result = ctx.draw_text_hex("Test", 50.0, 100.0);
3322 assert!(result.is_ok());
3323 let ops = ctx.operations();
3324 assert!(ops.contains("<"));
3325 assert!(ops.contains(">"));
3326 }
3327
3328 #[test]
3329 #[allow(deprecated)]
3330 fn test_draw_text_cid() {
3331 let mut ctx = GraphicsContext::new();
3332 ctx.set_custom_font("CustomCIDFont", 12.0);
3333 let result = ctx.draw_text_cid("Test", 50.0, 100.0);
3334 assert!(result.is_ok());
3335 let ops = ctx.operations();
3336 assert!(ops.contains("BT\n"));
3337 assert!(ops.contains("ET\n"));
3338 }
3339
3340 #[test]
3341 #[allow(deprecated)]
3342 fn test_draw_text_unicode() {
3343 let mut ctx = GraphicsContext::new();
3344 ctx.set_custom_font("UnicodeFont", 12.0);
3345 let result = ctx.draw_text_unicode("Test \u{4E2D}\u{6587}", 50.0, 100.0);
3346 assert!(result.is_ok());
3347 let ops = ctx.operations();
3348 assert!(ops.contains("BT\n"));
3349 assert!(ops.contains("ET\n"));
3350 }
3351
3352 #[test]
3353 fn test_begin_end_transparency_group() {
3354 let mut ctx = GraphicsContext::new();
3355
3356 assert!(!ctx.in_transparency_group());
3358 assert!(ctx.current_transparency_group().is_none());
3359
3360 let group = TransparencyGroup::new();
3362 ctx.begin_transparency_group(group);
3363 assert!(ctx.in_transparency_group());
3364 assert!(ctx.current_transparency_group().is_some());
3365
3366 let ops = ctx.operations();
3368 assert!(ops.contains("% Begin Transparency Group"));
3369
3370 ctx.end_transparency_group();
3372 assert!(!ctx.in_transparency_group());
3373 assert!(ctx.current_transparency_group().is_none());
3374
3375 let ops_after = ctx.operations();
3377 assert!(ops_after.contains("% End Transparency Group"));
3378 }
3379
3380 #[test]
3381 fn test_transparency_group_nesting() {
3382 let mut ctx = GraphicsContext::new();
3383
3384 let group1 = TransparencyGroup::new();
3386 let group2 = TransparencyGroup::new();
3387 let group3 = TransparencyGroup::new();
3388
3389 ctx.begin_transparency_group(group1);
3390 assert_eq!(ctx.transparency_stack.len(), 1);
3391
3392 ctx.begin_transparency_group(group2);
3393 assert_eq!(ctx.transparency_stack.len(), 2);
3394
3395 ctx.begin_transparency_group(group3);
3396 assert_eq!(ctx.transparency_stack.len(), 3);
3397
3398 ctx.end_transparency_group();
3400 assert_eq!(ctx.transparency_stack.len(), 2);
3401
3402 ctx.end_transparency_group();
3403 assert_eq!(ctx.transparency_stack.len(), 1);
3404
3405 ctx.end_transparency_group();
3406 assert_eq!(ctx.transparency_stack.len(), 0);
3407 assert!(!ctx.in_transparency_group());
3408 }
3409
3410 #[test]
3411 fn test_transparency_group_without_begin() {
3412 let mut ctx = GraphicsContext::new();
3413
3414 assert!(!ctx.in_transparency_group());
3416 ctx.end_transparency_group();
3417 assert!(!ctx.in_transparency_group());
3418 }
3419
3420 #[test]
3421 fn test_extgstate_manager_access() {
3422 let ctx = GraphicsContext::new();
3423 let manager = ctx.extgstate_manager();
3424 assert_eq!(manager.count(), 0);
3425 }
3426
3427 #[test]
3428 fn test_extgstate_manager_mut_access() {
3429 let mut ctx = GraphicsContext::new();
3430 let manager = ctx.extgstate_manager_mut();
3431 assert_eq!(manager.count(), 0);
3432 }
3433
3434 #[test]
3435 fn test_has_extgstates() {
3436 let mut ctx = GraphicsContext::new();
3437
3438 assert!(!ctx.has_extgstates());
3440 assert_eq!(ctx.extgstate_manager().count(), 0);
3441
3442 ctx.set_alpha(0.5).unwrap();
3444 ctx.rect(10.0, 10.0, 50.0, 50.0).fill();
3445 let result = ctx.generate_operations().unwrap();
3446
3447 assert!(ctx.has_extgstates());
3448 assert!(ctx.extgstate_manager().count() > 0);
3449
3450 let output = String::from_utf8_lossy(&result);
3452 assert!(output.contains("/GS")); assert!(output.contains(" gs\n")); }
3455
3456 #[test]
3457 fn test_generate_extgstate_resources() {
3458 let mut ctx = GraphicsContext::new();
3459 ctx.set_alpha(0.5).unwrap();
3460 ctx.rect(10.0, 10.0, 50.0, 50.0).fill();
3461 ctx.generate_operations().unwrap();
3462
3463 let resources = ctx.generate_extgstate_resources();
3464 assert!(resources.is_ok());
3465 }
3466
3467 #[test]
3468 fn test_apply_extgstate() {
3469 let mut ctx = GraphicsContext::new();
3470
3471 let mut state = ExtGState::new();
3473 state.alpha_fill = Some(0.5);
3474 state.alpha_stroke = Some(0.8);
3475 state.blend_mode = Some(BlendMode::Multiply);
3476
3477 let result = ctx.apply_extgstate(state);
3478 assert!(result.is_ok());
3479
3480 assert!(ctx.has_extgstates());
3482 assert_eq!(ctx.extgstate_manager().count(), 1);
3483
3484 let mut state2 = ExtGState::new();
3486 state2.alpha_fill = Some(0.3);
3487 ctx.apply_extgstate(state2).unwrap();
3488
3489 assert_eq!(ctx.extgstate_manager().count(), 2);
3491 }
3492
3493 #[test]
3494 fn test_with_extgstate() {
3495 let mut ctx = GraphicsContext::new();
3496 let result = ctx.with_extgstate(|mut state| {
3497 state.alpha_fill = Some(0.5);
3498 state.alpha_stroke = Some(0.8);
3499 state
3500 });
3501 assert!(result.is_ok());
3502 }
3503
3504 #[test]
3505 fn test_set_blend_mode() {
3506 let mut ctx = GraphicsContext::new();
3507
3508 let result = ctx.set_blend_mode(BlendMode::Multiply);
3510 assert!(result.is_ok());
3511 assert!(ctx.has_extgstates());
3512
3513 ctx.clear();
3515 ctx.set_blend_mode(BlendMode::Screen).unwrap();
3516 ctx.rect(0.0, 0.0, 10.0, 10.0).fill();
3517 let ops = ctx.generate_operations().unwrap();
3518 let output = String::from_utf8_lossy(&ops);
3519
3520 assert!(output.contains("/GS"));
3522 assert!(output.contains(" gs\n"));
3523 }
3524
3525 #[test]
3526 fn test_render_table() {
3527 let mut ctx = GraphicsContext::new();
3528 let table = Table::with_equal_columns(2, 200.0);
3529 let result = ctx.render_table(&table);
3530 assert!(result.is_ok());
3531 }
3532
3533 #[test]
3534 fn test_render_list() {
3535 let mut ctx = GraphicsContext::new();
3536 use crate::text::{OrderedList, OrderedListStyle};
3537 let ordered = OrderedList::new(OrderedListStyle::Decimal);
3538 let list = ListElement::Ordered(ordered);
3539 let result = ctx.render_list(&list);
3540 assert!(result.is_ok());
3541 }
3542
3543 #[test]
3544 fn test_render_column_layout() {
3545 let mut ctx = GraphicsContext::new();
3546 use crate::text::ColumnContent;
3547 let layout = ColumnLayout::new(2, 100.0, 200.0);
3548 let content = ColumnContent::new("Test content");
3549 let result = ctx.render_column_layout(&layout, &content, 50.0, 50.0, 400.0);
3550 assert!(result.is_ok());
3551 }
3552
3553 #[test]
3554 fn test_clip_ellipse() {
3555 let mut ctx = GraphicsContext::new();
3556
3557 assert!(!ctx.has_clipping());
3559 assert!(ctx.clipping_path().is_none());
3560
3561 let result = ctx.clip_ellipse(100.0, 100.0, 50.0, 30.0);
3563 assert!(result.is_ok());
3564 assert!(ctx.has_clipping());
3565 assert!(ctx.clipping_path().is_some());
3566
3567 let ops = ctx.operations();
3569 assert!(ops.contains("W\n") || ops.contains("W*\n")); ctx.clear_clipping();
3573 assert!(!ctx.has_clipping());
3574 }
3575
3576 #[test]
3577 fn test_clipping_path_access() {
3578 let mut ctx = GraphicsContext::new();
3579
3580 assert!(ctx.clipping_path().is_none());
3582
3583 ctx.clip_rect(10.0, 10.0, 50.0, 50.0).unwrap();
3585 assert!(ctx.clipping_path().is_some());
3586
3587 ctx.clip_circle(100.0, 100.0, 25.0).unwrap();
3589 assert!(ctx.clipping_path().is_some());
3590
3591 ctx.save_state();
3593 ctx.clear_clipping();
3594 assert!(!ctx.has_clipping());
3595
3596 ctx.restore_state();
3597 assert!(ctx.has_clipping());
3599 }
3600
3601 #[test]
3604 fn test_edge_case_move_to_negative() {
3605 let mut ctx = GraphicsContext::new();
3606 ctx.move_to(-100.5, -200.25);
3607 assert!(ctx.operations().contains("-100.50 -200.25 m\n"));
3608 }
3609
3610 #[test]
3611 fn test_edge_case_opacity_out_of_range() {
3612 let mut ctx = GraphicsContext::new();
3613
3614 let _ = ctx.set_opacity(2.5);
3616 assert_eq!(ctx.fill_opacity(), 1.0);
3617
3618 let _ = ctx.set_opacity(-0.5);
3620 assert_eq!(ctx.fill_opacity(), 0.0);
3621 }
3622
3623 #[test]
3624 fn test_edge_case_line_width_extremes() {
3625 let mut ctx = GraphicsContext::new();
3626
3627 ctx.set_line_width(0.0);
3628 assert_eq!(ctx.line_width(), 0.0);
3629
3630 ctx.set_line_width(10000.0);
3631 assert_eq!(ctx.line_width(), 10000.0);
3632 }
3633
3634 #[test]
3637 fn test_interaction_transparency_plus_clipping() {
3638 let mut ctx = GraphicsContext::new();
3639
3640 ctx.set_alpha(0.5).unwrap();
3641 ctx.clip_rect(10.0, 10.0, 100.0, 100.0).unwrap();
3642 ctx.rect(20.0, 20.0, 80.0, 80.0).fill();
3643
3644 let ops = ctx.generate_operations().unwrap();
3645 let output = String::from_utf8_lossy(&ops);
3646
3647 assert!(output.contains("W\n") || output.contains("W*\n"));
3649 assert!(output.contains("/GS"));
3650 }
3651
3652 #[test]
3653 fn test_interaction_extgstate_plus_text() {
3654 let mut ctx = GraphicsContext::new();
3655
3656 let mut state = ExtGState::new();
3657 state.alpha_fill = Some(0.7);
3658 ctx.apply_extgstate(state).unwrap();
3659
3660 ctx.set_font(Font::Helvetica, 14.0);
3661 ctx.draw_text("Test", 100.0, 200.0).unwrap();
3662
3663 let ops = ctx.generate_operations().unwrap();
3664 let output = String::from_utf8_lossy(&ops);
3665
3666 assert!(output.contains("/GS"));
3667 assert!(output.contains("BT\n"));
3668 }
3669
3670 #[test]
3671 fn test_interaction_chained_transformations() {
3672 let mut ctx = GraphicsContext::new();
3673
3674 ctx.translate(50.0, 100.0);
3675 ctx.rotate(45.0);
3676 ctx.scale(2.0, 2.0);
3677
3678 let ops = ctx.operations();
3679 assert_eq!(ops.matches("cm\n").count(), 3);
3680 }
3681
3682 #[test]
3685 fn test_e2e_complete_page_with_header() {
3686 use crate::{Document, Page};
3687
3688 let mut doc = Document::new();
3689 let mut page = Page::a4();
3690 let ctx = page.graphics();
3691
3692 ctx.save_state();
3694 let _ = ctx.set_fill_opacity(0.3);
3695 ctx.set_fill_color(Color::rgb(200.0, 200.0, 255.0));
3696 ctx.rect(0.0, 750.0, 595.0, 42.0).fill();
3697 ctx.restore_state();
3698
3699 ctx.save_state();
3701 ctx.clip_rect(50.0, 50.0, 495.0, 692.0).unwrap();
3702 ctx.rect(60.0, 60.0, 100.0, 100.0).fill();
3703 ctx.restore_state();
3704
3705 let ops = ctx.generate_operations().unwrap();
3706 let output = String::from_utf8_lossy(&ops);
3707
3708 assert!(output.contains("q\n"));
3709 assert!(output.contains("Q\n"));
3710 assert!(output.contains("f\n"));
3711
3712 doc.add_page(page);
3713 assert!(doc.to_bytes().unwrap().len() > 0);
3714 }
3715
3716 #[test]
3717 fn test_e2e_watermark_workflow() {
3718 let mut ctx = GraphicsContext::new();
3719
3720 ctx.save_state();
3721 let _ = ctx.set_fill_opacity(0.2);
3722 ctx.translate(300.0, 400.0);
3723 ctx.rotate(45.0);
3724 ctx.set_font(Font::HelveticaBold, 72.0);
3725 ctx.draw_text("DRAFT", 0.0, 0.0).unwrap();
3726 ctx.restore_state();
3727
3728 let ops = ctx.generate_operations().unwrap();
3729 let output = String::from_utf8_lossy(&ops);
3730
3731 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")); }
3738
3739 #[test]
3742 fn test_set_custom_font_emits_tf_operator() {
3743 let mut ctx = GraphicsContext::new();
3744 ctx.set_custom_font("NotoSansCJK", 14.0);
3745
3746 let ops = ctx.operations();
3747 assert!(
3748 ops.contains("/NotoSansCJK 14 Tf"),
3749 "set_custom_font should emit Tf operator, got: {}",
3750 ops
3751 );
3752 }
3753
3754 #[test]
3757 fn test_draw_text_uses_is_custom_font_flag() {
3758 let mut ctx = GraphicsContext::new();
3759 ctx.set_custom_font("Helvetica", 12.0);
3761 ctx.clear(); ctx.draw_text("A", 10.0, 20.0).unwrap();
3764 let ops = ctx.operations();
3765 assert!(
3767 ops.contains("<0041> Tj"),
3768 "draw_text with is_custom_font=true should use hex, got: {}",
3769 ops
3770 );
3771 }
3772
3773 #[test]
3774 fn test_draw_text_standard_font_uses_literal() {
3775 let mut ctx = GraphicsContext::new();
3776 ctx.set_font(Font::Helvetica, 12.0);
3777 ctx.clear();
3778
3779 ctx.draw_text("Hello", 10.0, 20.0).unwrap();
3780 let ops = ctx.operations();
3781 assert!(
3782 ops.contains("(Hello) Tj"),
3783 "draw_text with standard font should use literal, got: {}",
3784 ops
3785 );
3786 }
3787
3788 #[test]
3791 fn test_show_text_smp_character_uses_surrogate_pairs() {
3792 let mut ctx = GraphicsContext::new();
3793 ctx.set_font(Font::Custom("Emoji".to_string()), 12.0);
3794
3795 ctx.begin_text();
3796 ctx.set_text_position(0.0, 0.0);
3797 ctx.show_text("\u{1F600}").unwrap();
3799 ctx.end_text();
3800
3801 let ops = ctx.operations();
3802 assert!(
3803 ops.contains("<D83DDE00> Tj"),
3804 "SMP character should use UTF-16BE surrogate pair, got: {}",
3805 ops
3806 );
3807 assert!(
3808 !ops.contains("FFFD"),
3809 "SMP character must NOT be replaced with FFFD"
3810 );
3811 }
3812
3813 #[test]
3816 fn test_save_restore_preserves_font_state() {
3817 let mut ctx = GraphicsContext::new();
3818 ctx.set_font(Font::Custom("CJK".to_string()), 12.0);
3819 assert!(ctx.is_custom_font);
3820 assert_eq!(ctx.current_font_name.as_deref(), Some("CJK"));
3821 assert_eq!(ctx.current_font_size, 12.0);
3822
3823 ctx.save_state();
3824 ctx.set_font(Font::Helvetica, 10.0);
3825 assert!(!ctx.is_custom_font);
3826 assert_eq!(ctx.current_font_name.as_deref(), Some("Helvetica"));
3827
3828 ctx.restore_state();
3829 assert!(
3830 ctx.is_custom_font,
3831 "is_custom_font must be restored after restore_state"
3832 );
3833 assert_eq!(ctx.current_font_name.as_deref(), Some("CJK"));
3834 assert_eq!(ctx.current_font_size, 12.0);
3835 }
3836
3837 #[test]
3838 fn test_save_restore_mixed_font_encoding() {
3839 let mut ctx = GraphicsContext::new();
3840 ctx.set_font(Font::Custom("CJK".to_string()), 12.0);
3841
3842 ctx.save_state();
3844 ctx.set_font(Font::Helvetica, 10.0);
3845 ctx.begin_text();
3846 ctx.show_text("Hello").unwrap();
3847 ctx.end_text();
3848 ctx.restore_state();
3849
3850 ctx.begin_text();
3852 ctx.show_text("你好").unwrap();
3853 ctx.end_text();
3854
3855 let ops = ctx.operations();
3856 assert!(
3858 ops.contains("<4F60597D> Tj"),
3859 "After restore_state, CJK text should use hex encoding, got: {}",
3860 ops
3861 );
3862 }
3863
3864 #[test]
3865 fn test_graphics_state_arc_str_save_restore() {
3866 let mut ctx = GraphicsContext::new();
3869
3870 ctx.set_font(Font::Custom("TestFont".to_string()), 14.0);
3872 assert_eq!(ctx.current_font_name.as_deref(), Some("TestFont"));
3873 assert!(ctx.is_custom_font);
3874
3875 ctx.save_state();
3877 ctx.set_font(Font::Custom("Other".to_string()), 10.0);
3878 assert_eq!(ctx.current_font_name.as_deref(), Some("Other"));
3879
3880 ctx.restore_state();
3882 assert_eq!(
3883 ctx.current_font_name.as_deref(),
3884 Some("TestFont"),
3885 "Font name must be restored to TestFont after restore_state"
3886 );
3887 assert_eq!(ctx.current_font_size, 14.0);
3888 assert!(
3889 ctx.is_custom_font,
3890 "is_custom_font must be restored to true"
3891 );
3892
3893 if let Some(ref arc) = ctx.current_font_name {
3895 let cloned = arc.clone();
3896 assert_eq!(arc.as_ref(), cloned.as_ref());
3897 assert!(Arc::ptr_eq(arc, &cloned));
3899 }
3900 }
3901
3902 #[test]
3910 fn nan_line_width_sanitised_at_emission() {
3911 let mut ctx = GraphicsContext::new();
3912 ctx.set_line_width(f64::NAN);
3913 let ops = ctx.operations();
3914 assert!(
3915 ops.contains("0.00 w\n"),
3916 "NaN line width must emit `0.00 w`, got: {ops:?}"
3917 );
3918 assert!(
3919 !ops.contains("NaN"),
3920 "`NaN` must not appear in any content-stream output, got: {ops:?}"
3921 );
3922 }
3923
3924 #[test]
3925 fn pos_inf_line_width_sanitised_at_emission() {
3926 let mut ctx = GraphicsContext::new();
3927 ctx.set_line_width(f64::INFINITY);
3928 let ops = ctx.operations();
3929 assert!(
3930 ops.contains("0.00 w\n"),
3931 "+inf line width must emit `0.00 w`, got: {ops:?}"
3932 );
3933 assert!(
3934 !ops.contains("inf"),
3935 "`inf` must not appear in any content-stream output, got: {ops:?}"
3936 );
3937 }
3938
3939 #[test]
3940 fn nan_path_coords_sanitised_at_emission() {
3941 let mut ctx = GraphicsContext::new();
3942 ctx.move_to(f64::NAN, 20.0);
3943 ctx.line_to(30.0, f64::NEG_INFINITY);
3944 ctx.rect(f64::NAN, f64::INFINITY, 100.0, f64::NEG_INFINITY);
3945 let ops = ctx.operations();
3946 assert!(
3947 !ops.contains("NaN") && !ops.contains("inf"),
3948 "non-finite floats must not appear in path operators, got: {ops:?}"
3949 );
3950 assert!(
3951 ops.contains("0.00 20.00 m\n"),
3952 "NaN x must clamp to 0.00 in `m` op, got: {ops:?}"
3953 );
3954 assert!(
3955 ops.contains("30.00 0.00 l\n"),
3956 "-inf y must clamp to 0.00 in `l` op, got: {ops:?}"
3957 );
3958 assert!(
3959 ops.contains("0.00 0.00 100.00 0.00 re\n"),
3960 "non-finite components must clamp to 0.00 in `re` op, got: {ops:?}"
3961 );
3962 }
3963
3964 fn expected_components(values: &[f64], op: &str) -> String {
3977 let mut s = String::new();
3978 for v in values {
3979 s.push_str(&format!("{v:.4} "));
3980 }
3981 s.push_str(op);
3982 s.push('\n');
3983 s
3984 }
3985
3986 #[test]
3987 fn set_fill_color_icc_emits_named_cs_then_components() {
3988 let mut ctx = GraphicsContext::new();
3989 ctx.set_fill_color_icc("ICCRGB1", vec![0.25, 0.5, 0.75]);
3990 let expected = format!(
3991 "/ICCRGB1 cs\n{}",
3992 expected_components(&[0.25, 0.5, 0.75], "sc")
3993 );
3994 assert_eq!(
3995 ctx.operations(),
3996 expected,
3997 "ICC fill must emit `/ICCRGB1 cs` then `0.2500 0.5000 0.7500 sc`"
3998 );
3999 }
4000
4001 #[test]
4002 fn set_stroke_color_icc_emits_named_cs_then_components() {
4003 let mut ctx = GraphicsContext::new();
4004 ctx.set_stroke_color_icc("ICCRGB1", vec![0.25, 0.5, 0.75]);
4005 let expected = format!(
4006 "/ICCRGB1 CS\n{}",
4007 expected_components(&[0.25, 0.5, 0.75], "SC")
4008 );
4009 assert_eq!(
4010 ctx.operations(),
4011 expected,
4012 "ICC stroke must emit `/ICCRGB1 CS` then `0.2500 0.5000 0.7500 SC`"
4013 );
4014 }
4015
4016 #[test]
4017 fn set_fill_color_icc_single_channel_gray() {
4018 let mut ctx = GraphicsContext::new();
4019 ctx.set_fill_color_icc("ICCGray1", vec![0.42]);
4020 let expected = format!("/ICCGray1 cs\n{}", expected_components(&[0.42], "sc"));
4021 assert_eq!(ctx.operations(), expected);
4022 }
4023
4024 #[test]
4025 fn set_fill_color_icc_cmyk_four_channels() {
4026 let mut ctx = GraphicsContext::new();
4027 ctx.set_fill_color_icc("ICCCmyk1", vec![0.1, 0.2, 0.3, 0.4]);
4028 let expected = format!(
4029 "/ICCCmyk1 cs\n{}",
4030 expected_components(&[0.1, 0.2, 0.3, 0.4], "sc")
4031 );
4032 assert_eq!(ctx.operations(), expected);
4033 }
4034
4035 #[test]
4036 fn set_fill_color_calibrated_named_uses_caller_name() {
4037 let color = CalibratedColor::cal_rgb([0.1, 0.2, 0.3], CalRgbColorSpace::new());
4038 let mut ctx = GraphicsContext::new();
4039 ctx.set_fill_color_calibrated_named("MyCalRgb", color.clone());
4040 let expected = format!(
4041 "/MyCalRgb cs\n{}",
4042 expected_components(&color.values(), "sc")
4043 );
4044 assert_eq!(ctx.operations(), expected);
4045 }
4046
4047 #[test]
4048 fn set_stroke_color_calibrated_named_uses_caller_name() {
4049 let color = CalibratedColor::cal_gray(0.6, CalGrayColorSpace::new());
4050 let mut ctx = GraphicsContext::new();
4051 ctx.set_stroke_color_calibrated_named("MyCalGray", color.clone());
4052 let expected = format!(
4053 "/MyCalGray CS\n{}",
4054 expected_components(&color.values(), "SC")
4055 );
4056 assert_eq!(ctx.operations(), expected);
4057 }
4058
4059 #[test]
4060 fn set_fill_color_lab_named_uses_caller_name() {
4061 let color = LabColor::with_default(50.0, 12.0, -8.0);
4062 let mut ctx = GraphicsContext::new();
4063 ctx.set_fill_color_lab_named("MyLab", color.clone());
4064 let expected = format!("/MyLab cs\n{}", expected_components(&color.values(), "sc"));
4065 assert_eq!(ctx.operations(), expected);
4066 }
4067
4068 #[test]
4069 fn set_stroke_color_lab_named_uses_caller_name() {
4070 let color = LabColor::with_default(50.0, 12.0, -8.0);
4071 let mut ctx = GraphicsContext::new();
4072 ctx.set_stroke_color_lab_named("MyLab", color.clone());
4073 let expected = format!("/MyLab CS\n{}", expected_components(&color.values(), "SC"));
4074 assert_eq!(ctx.operations(), expected);
4075 }
4076
4077 #[test]
4081 fn legacy_calibrated_rgb_still_emits_calrgb1() {
4082 let color = CalibratedColor::cal_rgb([0.1, 0.2, 0.3], CalRgbColorSpace::new());
4083 let mut ctx = GraphicsContext::new();
4084 ctx.set_fill_color_calibrated(color.clone());
4085 let expected = format!(
4086 "/CalRGB1 cs\n{}",
4087 expected_components(&color.values(), "sc")
4088 );
4089 assert_eq!(ctx.operations(), expected);
4090 }
4091
4092 #[test]
4093 fn legacy_calibrated_gray_still_emits_calgray1() {
4094 let color = CalibratedColor::cal_gray(0.6, CalGrayColorSpace::new());
4095 let mut ctx = GraphicsContext::new();
4096 ctx.set_stroke_color_calibrated(color.clone());
4097 let expected = format!(
4098 "/CalGray1 CS\n{}",
4099 expected_components(&color.values(), "SC")
4100 );
4101 assert_eq!(ctx.operations(), expected);
4102 }
4103
4104 #[test]
4105 fn legacy_lab_still_emits_lab1() {
4106 let color = LabColor::with_default(50.0, 12.0, -8.0);
4107 let mut ctx = GraphicsContext::new();
4108 ctx.set_fill_color_lab(color.clone());
4109 let expected = format!("/Lab1 cs\n{}", expected_components(&color.values(), "sc"));
4110 assert_eq!(ctx.operations(), expected);
4111 }
4112
4113 #[test]
4114 fn legacy_lab_stroke_still_emits_lab1() {
4115 let color = LabColor::with_default(50.0, 12.0, -8.0);
4116 let mut ctx = GraphicsContext::new();
4117 ctx.set_stroke_color_lab(color.clone());
4118 let expected = format!("/Lab1 CS\n{}", expected_components(&color.values(), "SC"));
4119 assert_eq!(ctx.operations(), expected);
4120 }
4121
4122 #[test]
4127 #[cfg(debug_assertions)]
4128 #[should_panic(expected = "ICC fill colour components must not be empty")]
4129 fn set_fill_color_icc_empty_components_panics_in_debug() {
4130 let mut ctx = GraphicsContext::new();
4134 ctx.set_fill_color_icc("ICCRGB1", vec![]);
4135 }
4136
4137 #[test]
4138 #[cfg(debug_assertions)]
4139 #[should_panic(expected = "ICC stroke colour components must not be empty")]
4140 fn set_stroke_color_icc_empty_components_panics_in_debug() {
4141 let mut ctx = GraphicsContext::new();
4142 ctx.set_stroke_color_icc("ICCRGB1", vec![]);
4143 }
4144}