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