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