1use crate::elements::{
7 ContentElement, ImageContent, PathContent, PathOperation, StructureElement, TableCellAlign,
8 TableContent, TextContent,
9};
10use crate::error::Result;
11use crate::fonts::GlyphRemapper;
12use crate::layout::Color;
13use std::collections::HashMap;
14use std::io::Write;
15
16pub(crate) fn map_base14_font_name(name: &str, bold: bool) -> String {
33 let lower = name.to_lowercase();
34
35 enum Family {
41 Helvetica,
42 Times,
43 Courier,
44 }
45 let family = if lower.contains("courier") || lower.contains("mono") {
46 Family::Courier
47 } else if lower.contains("sans") || lower.contains("helvetica") || lower.contains("arial") {
48 Family::Helvetica
49 } else if lower.contains("times") || lower.contains("serif") {
50 Family::Times
51 } else {
52 Family::Helvetica
53 };
54
55 let want_bold = bold || lower.contains("bold");
60 let want_italic = lower.contains("italic") || lower.contains("oblique");
61
62 match family {
63 Family::Helvetica => match (want_bold, want_italic) {
64 (false, false) => "Helvetica",
65 (true, false) => "Helvetica-Bold",
66 (false, true) => "Helvetica-Oblique",
67 (true, true) => "Helvetica-BoldOblique",
68 },
69 Family::Times => match (want_bold, want_italic) {
70 (false, false) => "Times-Roman",
71 (true, false) => "Times-Bold",
72 (false, true) => "Times-Italic",
73 (true, true) => "Times-BoldItalic",
74 },
75 Family::Courier => match (want_bold, want_italic) {
76 (false, false) => "Courier",
77 (true, false) => "Courier-Bold",
78 (false, true) => "Courier-Oblique",
79 (true, true) => "Courier-BoldOblique",
80 },
81 }
82 .to_string()
83}
84
85#[derive(Debug, Clone)]
87pub enum ContentStreamOp {
88 SaveState,
90 RestoreState,
92 Transform(f32, f32, f32, f32, f32, f32),
94 BeginText,
96 EndText,
98 SetFont(String, f32),
100 MoveText(f32, f32),
102 SetTextMatrix(f32, f32, f32, f32, f32, f32),
104 ShowText(String),
106 ShowHexText(String),
108 ShowEmbeddedText {
117 font_name: String,
119 glyph_ids: Vec<u16>,
121 },
122 ShowTextArray(Vec<TextArrayItem>),
124 SetCharacterSpacing(f32),
126 SetWordSpacing(f32),
128 SetTextLeading(f32),
130 NextLine,
132 SetFillColorRGB(f32, f32, f32),
134 SetStrokeColorRGB(f32, f32, f32),
136 SetFillColorGray(f32),
138 SetStrokeColorGray(f32),
140 SetLineWidth(f32),
142 MoveTo(f32, f32),
144 LineTo(f32, f32),
146 CurveTo(f32, f32, f32, f32, f32, f32),
148 Rectangle(f32, f32, f32, f32),
150 ClosePath,
152 Stroke,
154 Fill,
156 FillStroke,
158 CloseStroke,
160 EndPath,
162 PaintXObject(String),
164
165 BeginMarkedContentDict {
168 tag: String,
170 mcid: u32,
172 },
173 EndMarkedContent,
175
176 BeginArtifact {
180 artifact_type: String,
182 subtype: Option<String>,
184 },
185 EndArtifact,
187
188 Clip,
191 ClipEvenOdd,
193
194 SetExtGState(String),
197
198 SetFillColorSpace(String),
201 SetStrokeColorSpace(String),
203 SetFillColorN(Vec<f32>),
205 SetStrokeColorN(Vec<f32>),
207 SetFillPattern(String, Vec<f32>),
209 SetStrokePattern(String, Vec<f32>),
211
212 PaintShading(String),
215
216 CurveToV(f32, f32, f32, f32),
219 CurveToY(f32, f32, f32, f32),
221 FillEvenOdd,
223 FillStrokeEvenOdd,
225 CloseFillStroke,
227 CloseFillStrokeEvenOdd,
229
230 SetLineCap(LineCap),
233 SetLineJoin(LineJoin),
235 SetMiterLimit(f32),
237 SetDashPattern(Vec<f32>, f32),
239
240 SetFillColorCMYK(f32, f32, f32, f32),
243 SetStrokeColorCMYK(f32, f32, f32, f32),
245
246 Raw(String),
248}
249
250#[derive(Debug, Clone, Copy, Default)]
252pub enum LineCap {
253 #[default]
255 Butt = 0,
256 Round = 1,
258 Square = 2,
260}
261
262#[derive(Debug, Clone, Copy, Default)]
264pub enum LineJoin {
265 #[default]
267 Miter = 0,
268 Round = 1,
270 Bevel = 2,
272}
273
274#[derive(Debug, Clone, Copy, Default)]
276pub enum BlendMode {
277 #[default]
279 Normal,
280 Multiply,
282 Screen,
284 Overlay,
286 Darken,
288 Lighten,
290 ColorDodge,
292 ColorBurn,
294 HardLight,
296 SoftLight,
298 Difference,
300 Exclusion,
302}
303
304impl BlendMode {
305 pub fn as_pdf_name(&self) -> &'static str {
307 match self {
308 BlendMode::Normal => "Normal",
309 BlendMode::Multiply => "Multiply",
310 BlendMode::Screen => "Screen",
311 BlendMode::Overlay => "Overlay",
312 BlendMode::Darken => "Darken",
313 BlendMode::Lighten => "Lighten",
314 BlendMode::ColorDodge => "ColorDodge",
315 BlendMode::ColorBurn => "ColorBurn",
316 BlendMode::HardLight => "HardLight",
317 BlendMode::SoftLight => "SoftLight",
318 BlendMode::Difference => "Difference",
319 BlendMode::Exclusion => "Exclusion",
320 }
321 }
322}
323
324#[derive(Debug, Clone)]
326pub enum TextArrayItem {
327 Text(String),
329 HexText(String),
331 Adjustment(f32),
333}
334
335#[derive(Debug, Clone)]
343pub struct StructElemRecord {
344 pub structure_type: String,
346 pub mcid: u32,
348 pub alt_text: Option<String>,
350 pub language: Option<String>,
352 pub children: Vec<StructElemRecord>,
354}
355
356#[derive(Debug, Clone)]
362pub struct PendingImage {
363 pub image: ImageContent,
365 pub resource_id: String,
367}
368
369#[derive(Debug, Default)]
374pub struct ContentStreamBuilder {
375 operations: Vec<ContentStreamOp>,
377 current_font: Option<String>,
379 current_font_size: f32,
381 in_text_object: bool,
383 mcid_counter: u32,
385 pending_images: Vec<PendingImage>,
387 next_image_id: u32,
389 struct_records: Vec<StructElemRecord>,
393}
394
395impl ContentStreamBuilder {
396 pub fn new() -> Self {
398 Self::default()
399 }
400
401 pub fn op(&mut self, op: ContentStreamOp) -> &mut Self {
403 self.operations.push(op);
404 self
405 }
406
407 pub fn ops(&mut self, ops: impl IntoIterator<Item = ContentStreamOp>) -> &mut Self {
409 self.operations.extend(ops);
410 self
411 }
412
413 pub fn begin_text(&mut self) -> &mut Self {
415 if !self.in_text_object {
416 self.op(ContentStreamOp::BeginText);
417 self.in_text_object = true;
418 }
419 self
420 }
421
422 pub fn end_text(&mut self) -> &mut Self {
424 if self.in_text_object {
425 self.op(ContentStreamOp::EndText);
426 self.in_text_object = false;
427 }
428 self
429 }
430
431 pub fn set_font(&mut self, font_name: &str, size: f32) -> &mut Self {
433 if self.current_font.as_deref() != Some(font_name) || self.current_font_size != size {
434 self.op(ContentStreamOp::SetFont(font_name.to_string(), size));
435 self.current_font = Some(font_name.to_string());
436 self.current_font_size = size;
437 }
438 self
439 }
440
441 pub fn text(&mut self, text: &str, x: f32, y: f32) -> &mut Self {
443 self.begin_text();
444 self.op(ContentStreamOp::SetTextMatrix(1.0, 0.0, 0.0, 1.0, x, y));
445 self.op(ContentStreamOp::ShowText(text.to_string()));
446 self
447 }
448
449 pub fn hex_text(&mut self, hex_string: &str, x: f32, y: f32) -> &mut Self {
454 self.begin_text();
455 self.op(ContentStreamOp::SetTextMatrix(1.0, 0.0, 0.0, 1.0, x, y));
456 self.op(ContentStreamOp::ShowHexText(hex_string.to_string()));
457 self
458 }
459
460 pub fn embedded_text(
470 &mut self,
471 font_name: &str,
472 glyph_ids: Vec<u16>,
473 x: f32,
474 y: f32,
475 ) -> &mut Self {
476 self.begin_text();
477 self.op(ContentStreamOp::SetTextMatrix(1.0, 0.0, 0.0, 1.0, x, y));
478 self.op(ContentStreamOp::ShowEmbeddedText {
479 font_name: font_name.to_string(),
480 glyph_ids,
481 });
482 self
483 }
484
485 pub fn fill_color(&mut self, color: Color) -> &mut Self {
487 self.op(ContentStreamOp::SetFillColorRGB(color.r, color.g, color.b))
488 }
489
490 pub fn draw_image(
499 &mut self,
500 resource_id: &str,
501 x: f32,
502 y: f32,
503 width: f32,
504 height: f32,
505 ) -> &mut Self {
506 self.end_text();
508
509 self.op(ContentStreamOp::SaveState);
511 self.op(ContentStreamOp::Transform(width, 0.0, 0.0, height, x, y));
512 self.op(ContentStreamOp::PaintXObject(resource_id.to_string()));
513 self.op(ContentStreamOp::RestoreState);
514 self
515 }
516
517 pub fn draw_image_at(
519 &mut self,
520 resource_id: &str,
521 placement: &super::image_handler::ImagePlacement,
522 ) -> &mut Self {
523 self.draw_image(resource_id, placement.x, placement.y, placement.width, placement.height)
524 }
525
526 pub fn stroke_color(&mut self, color: Color) -> &mut Self {
528 self.op(ContentStreamOp::SetStrokeColorRGB(color.r, color.g, color.b))
529 }
530
531 pub fn set_fill_color(&mut self, r: f32, g: f32, b: f32) -> &mut Self {
533 self.op(ContentStreamOp::SetFillColorRGB(r, g, b))
534 }
535
536 pub fn set_stroke_color(&mut self, r: f32, g: f32, b: f32) -> &mut Self {
538 self.op(ContentStreamOp::SetStrokeColorRGB(r, g, b))
539 }
540
541 pub fn set_line_width(&mut self, width: f32) -> &mut Self {
543 self.op(ContentStreamOp::SetLineWidth(width))
544 }
545
546 pub fn move_to(&mut self, x: f32, y: f32) -> &mut Self {
548 self.op(ContentStreamOp::MoveTo(x, y))
549 }
550
551 pub fn line_to(&mut self, x: f32, y: f32) -> &mut Self {
553 self.op(ContentStreamOp::LineTo(x, y))
554 }
555
556 pub fn rect(&mut self, x: f32, y: f32, width: f32, height: f32) -> &mut Self {
558 self.op(ContentStreamOp::Rectangle(x, y, width, height))
559 }
560
561 pub fn stroke(&mut self) -> &mut Self {
563 self.op(ContentStreamOp::Stroke)
564 }
565
566 pub fn fill(&mut self) -> &mut Self {
568 self.op(ContentStreamOp::Fill)
569 }
570
571 pub fn fill_even_odd(&mut self) -> &mut Self {
573 self.op(ContentStreamOp::FillEvenOdd)
574 }
575
576 pub fn fill_stroke(&mut self) -> &mut Self {
578 self.op(ContentStreamOp::FillStroke)
579 }
580
581 pub fn fill_stroke_even_odd(&mut self) -> &mut Self {
583 self.op(ContentStreamOp::FillStrokeEvenOdd)
584 }
585
586 pub fn close_fill_stroke(&mut self) -> &mut Self {
588 self.op(ContentStreamOp::CloseFillStroke)
589 }
590
591 pub fn close_path(&mut self) -> &mut Self {
593 self.op(ContentStreamOp::ClosePath)
594 }
595
596 pub fn clip(&mut self) -> &mut Self {
603 self.op(ContentStreamOp::Clip)
604 }
605
606 pub fn clip_even_odd(&mut self) -> &mut Self {
608 self.op(ContentStreamOp::ClipEvenOdd)
609 }
610
611 pub fn end_path(&mut self) -> &mut Self {
613 self.op(ContentStreamOp::EndPath)
614 }
615
616 pub fn clip_rect(&mut self, x: f32, y: f32, width: f32, height: f32) -> &mut Self {
620 self.rect(x, y, width, height).clip().end_path()
621 }
622
623 pub fn save_state(&mut self) -> &mut Self {
627 self.op(ContentStreamOp::SaveState)
628 }
629
630 pub fn restore_state(&mut self) -> &mut Self {
632 self.op(ContentStreamOp::RestoreState)
633 }
634
635 pub fn set_ext_gstate(&mut self, gs_name: &str) -> &mut Self {
639 self.op(ContentStreamOp::SetExtGState(gs_name.to_string()))
640 }
641
642 pub fn transform(&mut self, a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> &mut Self {
651 self.op(ContentStreamOp::Transform(a, b, c, d, e, f))
652 }
653
654 pub fn translate(&mut self, tx: f32, ty: f32) -> &mut Self {
656 self.transform(1.0, 0.0, 0.0, 1.0, tx, ty)
657 }
658
659 pub fn scale(&mut self, sx: f32, sy: f32) -> &mut Self {
661 self.transform(sx, 0.0, 0.0, sy, 0.0, 0.0)
662 }
663
664 pub fn rotate(&mut self, angle: f32) -> &mut Self {
666 let cos = angle.cos();
667 let sin = angle.sin();
668 self.transform(cos, sin, -sin, cos, 0.0, 0.0)
669 }
670
671 pub fn rotate_degrees(&mut self, degrees: f32) -> &mut Self {
673 self.rotate(degrees * std::f32::consts::PI / 180.0)
674 }
675
676 pub fn set_line_cap(&mut self, cap: LineCap) -> &mut Self {
680 self.op(ContentStreamOp::SetLineCap(cap))
681 }
682
683 pub fn set_line_join(&mut self, join: LineJoin) -> &mut Self {
685 self.op(ContentStreamOp::SetLineJoin(join))
686 }
687
688 pub fn set_miter_limit(&mut self, limit: f32) -> &mut Self {
690 self.op(ContentStreamOp::SetMiterLimit(limit))
691 }
692
693 pub fn set_dash_pattern(&mut self, pattern: Vec<f32>, phase: f32) -> &mut Self {
699 self.op(ContentStreamOp::SetDashPattern(pattern, phase))
700 }
701
702 pub fn set_solid_line(&mut self) -> &mut Self {
704 self.set_dash_pattern(vec![], 0.0)
705 }
706
707 pub fn set_fill_color_space(&mut self, name: &str) -> &mut Self {
711 self.op(ContentStreamOp::SetFillColorSpace(name.to_string()))
712 }
713
714 pub fn set_stroke_color_space(&mut self, name: &str) -> &mut Self {
716 self.op(ContentStreamOp::SetStrokeColorSpace(name.to_string()))
717 }
718
719 pub fn set_fill_color_n(&mut self, components: Vec<f32>) -> &mut Self {
721 self.op(ContentStreamOp::SetFillColorN(components))
722 }
723
724 pub fn set_stroke_color_n(&mut self, components: Vec<f32>) -> &mut Self {
726 self.op(ContentStreamOp::SetStrokeColorN(components))
727 }
728
729 pub fn set_fill_color_cmyk(&mut self, c: f32, m: f32, y: f32, k: f32) -> &mut Self {
731 self.op(ContentStreamOp::SetFillColorCMYK(c, m, y, k))
732 }
733
734 pub fn set_stroke_color_cmyk(&mut self, c: f32, m: f32, y: f32, k: f32) -> &mut Self {
736 self.op(ContentStreamOp::SetStrokeColorCMYK(c, m, y, k))
737 }
738
739 pub fn set_fill_pattern(&mut self, pattern_name: &str, components: Vec<f32>) -> &mut Self {
747 self.op(ContentStreamOp::SetFillPattern(pattern_name.to_string(), components))
748 }
749
750 pub fn set_stroke_pattern(&mut self, pattern_name: &str, components: Vec<f32>) -> &mut Self {
752 self.op(ContentStreamOp::SetStrokePattern(pattern_name.to_string(), components))
753 }
754
755 pub fn paint_shading(&mut self, shading_name: &str) -> &mut Self {
762 self.op(ContentStreamOp::PaintShading(shading_name.to_string()))
763 }
764
765 pub fn draw_gradient_rect(
770 &mut self,
771 shading_name: &str,
772 x: f32,
773 y: f32,
774 width: f32,
775 height: f32,
776 ) -> &mut Self {
777 self.save_state()
778 .rect(x, y, width, height)
779 .clip()
780 .end_path()
781 .paint_shading(shading_name)
782 .restore_state()
783 }
784
785 pub fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) -> &mut Self {
789 self.op(ContentStreamOp::CurveTo(x1, y1, x2, y2, x3, y3))
790 }
791
792 pub fn curve_to_v(&mut self, x2: f32, y2: f32, x3: f32, y3: f32) -> &mut Self {
794 self.op(ContentStreamOp::CurveToV(x2, y2, x3, y3))
795 }
796
797 pub fn curve_to_y(&mut self, x1: f32, y1: f32, x3: f32, y3: f32) -> &mut Self {
799 self.op(ContentStreamOp::CurveToY(x1, y1, x3, y3))
800 }
801
802 pub fn circle(&mut self, cx: f32, cy: f32, radius: f32) -> &mut Self {
806 let k = 0.552_284_8; let c = radius * k;
809
810 self.move_to(cx + radius, cy)
811 .curve_to(cx + radius, cy + c, cx + c, cy + radius, cx, cy + radius)
812 .curve_to(cx - c, cy + radius, cx - radius, cy + c, cx - radius, cy)
813 .curve_to(cx - radius, cy - c, cx - c, cy - radius, cx, cy - radius)
814 .curve_to(cx + c, cy - radius, cx + radius, cy - c, cx + radius, cy)
815 .close_path()
816 }
817
818 pub fn ellipse(&mut self, cx: f32, cy: f32, rx: f32, ry: f32) -> &mut Self {
820 let kx = rx * 0.552_284_8;
821 let ky = ry * 0.552_284_8;
822
823 self.move_to(cx + rx, cy)
824 .curve_to(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry)
825 .curve_to(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy)
826 .curve_to(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry)
827 .curve_to(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy)
828 .close_path()
829 }
830
831 pub fn rounded_rect(
833 &mut self,
834 x: f32,
835 y: f32,
836 width: f32,
837 height: f32,
838 radius: f32,
839 ) -> &mut Self {
840 let r = radius.min(width / 2.0).min(height / 2.0);
841 let k = r * 0.552_284_8;
842
843 self.move_to(x + r, y)
845 .line_to(x + width - r, y)
847 .curve_to(x + width - r + k, y, x + width, y + k, x + width, y + r)
849 .line_to(x + width, y + height - r)
851 .curve_to(
853 x + width,
854 y + height - r + k,
855 x + width - k,
856 y + height,
857 x + width - r,
858 y + height,
859 )
860 .line_to(x + r, y + height)
862 .curve_to(x + r - k, y + height, x, y + height - k, x, y + height - r)
864 .line_to(x, y + r)
866 .curve_to(x, y + r - k, x + r - k, y, x + r, y)
868 .close_path()
869 }
870
871 pub fn add_element(&mut self, element: &ContentElement) -> &mut Self {
873 match element {
874 ContentElement::Text(text) => self.add_text_content(text),
875 ContentElement::Path(path) => self.add_path_content(path),
876 ContentElement::Image(image) => self.add_image_content(image),
877 ContentElement::Structure(s) => {
878 let record = self.add_structure_element_impl(s);
881 self.struct_records.push(record);
882 self
883 },
884 ContentElement::Table(table) => self.add_table_content(table),
885 }
886 }
887
888 fn add_text_content(&mut self, text: &TextContent) -> &mut Self {
890 let is_artifact = text.artifact_type.is_some();
893 if is_artifact {
894 use crate::extractors::text::ArtifactType;
895 self.end_text();
897 let (artifact_type, subtype) = match &text.artifact_type {
898 Some(ArtifactType::Pagination(sub)) => {
899 use crate::extractors::text::PaginationSubtype;
900 let sub_str = match sub {
901 PaginationSubtype::Header => Some("Header".to_string()),
902 PaginationSubtype::Footer => Some("Footer".to_string()),
903 PaginationSubtype::PageNumber => Some("PageNum".to_string()),
904 PaginationSubtype::Watermark => Some("Watermark".to_string()),
905 PaginationSubtype::Other => None,
906 };
907 ("Pagination".to_string(), sub_str)
908 },
909 Some(ArtifactType::Layout) => ("Layout".to_string(), None),
910 Some(ArtifactType::Page) => ("Page".to_string(), None),
911 Some(ArtifactType::Background) => ("Background".to_string(), None),
912 None => unreachable!(),
913 };
914 self.op(ContentStreamOp::BeginArtifact {
915 artifact_type,
916 subtype,
917 });
918 }
919
920 self.begin_text();
921
922 self.fill_color(text.style.color);
928
929 let font_name = self.map_font_name(&text.font.name, text.style.weight.is_bold());
931 self.set_font(&font_name, text.font.size);
932
933 self.op(ContentStreamOp::SetTextMatrix(1.0, 0.0, 0.0, 1.0, text.bbox.x, text.bbox.y));
935 self.op(ContentStreamOp::ShowText(text.text.clone()));
936
937 if is_artifact {
938 self.end_text();
939 self.op(ContentStreamOp::EndArtifact);
940 }
941
942 self
943 }
944
945 fn map_font_name(&self, name: &str, bold: bool) -> String {
947 map_base14_font_name(name, bold)
948 }
949
950 fn add_path_content(&mut self, path: &PathContent) -> &mut Self {
952 self.end_text();
954
955 let is_artifact = path.artifact_type.is_some();
957 if is_artifact {
958 use crate::extractors::text::ArtifactType;
959 let (artifact_type, subtype) = match &path.artifact_type {
960 Some(ArtifactType::Pagination(sub)) => {
961 use crate::extractors::text::PaginationSubtype;
962 let sub_str = match sub {
963 PaginationSubtype::Header => Some("Header".to_string()),
964 PaginationSubtype::Footer => Some("Footer".to_string()),
965 PaginationSubtype::PageNumber => Some("PageNum".to_string()),
966 PaginationSubtype::Watermark => Some("Watermark".to_string()),
967 PaginationSubtype::Other => None,
968 };
969 ("Pagination".to_string(), sub_str)
970 },
971 Some(ArtifactType::Layout) => ("Layout".to_string(), None),
972 Some(ArtifactType::Page) => ("Page".to_string(), None),
973 Some(ArtifactType::Background) => ("Background".to_string(), None),
974 None => unreachable!(),
975 };
976 self.op(ContentStreamOp::BeginArtifact {
977 artifact_type,
978 subtype,
979 });
980 }
981
982 let had_matrix = if let Some(m) = path.matrix {
987 self.op(ContentStreamOp::SaveState);
988 self.op(ContentStreamOp::Transform(m[0], m[1], m[2], m[3], m[4], m[5]));
989 true
990 } else {
991 false
992 };
993
994 if let Some(color) = path.stroke_color {
996 self.stroke_color(color);
997 }
998 if let Some(color) = path.fill_color {
999 self.fill_color(color);
1000 }
1001 self.op(ContentStreamOp::SetLineWidth(path.stroke_width));
1002
1003 let had_dash = if let Some((dashes, phase)) = path.dash_pattern.as_ref() {
1008 self.set_dash_pattern(dashes.clone(), *phase);
1009 true
1010 } else {
1011 false
1012 };
1013
1014 for op in &path.operations {
1016 match op {
1017 PathOperation::MoveTo(x, y) => {
1018 self.op(ContentStreamOp::MoveTo(*x, *y));
1019 },
1020 PathOperation::LineTo(x, y) => {
1021 self.op(ContentStreamOp::LineTo(*x, *y));
1022 },
1023 PathOperation::CurveTo(x1, y1, x2, y2, x3, y3) => {
1024 self.op(ContentStreamOp::CurveTo(*x1, *y1, *x2, *y2, *x3, *y3));
1025 },
1026 PathOperation::Rectangle(x, y, w, h) => {
1027 self.op(ContentStreamOp::Rectangle(*x, *y, *w, *h));
1028 },
1029 PathOperation::ClosePath => {
1030 self.op(ContentStreamOp::ClosePath);
1031 },
1032 }
1033 }
1034
1035 match (path.stroke_color.is_some(), path.fill_color.is_some()) {
1037 (true, true) => self.op(ContentStreamOp::FillStroke),
1038 (true, false) => self.op(ContentStreamOp::Stroke),
1039 (false, true) => self.op(ContentStreamOp::Fill),
1040 (false, false) => self.op(ContentStreamOp::EndPath),
1041 };
1042
1043 if had_dash {
1045 self.set_dash_pattern(Vec::new(), 0.0);
1046 }
1047
1048 if had_matrix {
1054 self.op(ContentStreamOp::RestoreState);
1055 }
1056
1057 if is_artifact {
1058 self.op(ContentStreamOp::EndArtifact);
1059 }
1060
1061 self
1062 }
1063
1064 fn add_table_content(&mut self, table: &TableContent) -> &mut Self {
1071 self.end_text();
1073
1074 let style = &table.style;
1075 let padding = style.cell_padding;
1076
1077 self.op(ContentStreamOp::SaveState);
1079
1080 let mut current_y = table.bbox.y + table.bbox.height;
1082
1083 for (row_idx, row) in table.rows.iter().enumerate() {
1084 let row_height = row
1085 .height
1086 .unwrap_or_else(|| table.bbox.height / table.rows.len() as f32);
1087 current_y -= row_height;
1088
1089 let mut current_x = table.bbox.x;
1090
1091 if let Some((r, g, b)) = row.background {
1093 self.op(ContentStreamOp::SetFillColorRGB(r, g, b));
1094 self.op(ContentStreamOp::Rectangle(
1095 table.bbox.x,
1096 current_y,
1097 table.bbox.width,
1098 row_height,
1099 ));
1100 self.op(ContentStreamOp::Fill);
1101 }
1102
1103 if row_idx % 2 == 1 {
1105 if let Some((r, g, b)) = style.stripe_background {
1106 self.op(ContentStreamOp::SetFillColorRGB(r, g, b));
1107 self.op(ContentStreamOp::Rectangle(
1108 table.bbox.x,
1109 current_y,
1110 table.bbox.width,
1111 row_height,
1112 ));
1113 self.op(ContentStreamOp::Fill);
1114 }
1115 }
1116
1117 if row.is_header {
1119 if let Some((r, g, b)) = style.header_background {
1120 self.op(ContentStreamOp::SetFillColorRGB(r, g, b));
1121 self.op(ContentStreamOp::Rectangle(
1122 table.bbox.x,
1123 current_y,
1124 table.bbox.width,
1125 row_height,
1126 ));
1127 self.op(ContentStreamOp::Fill);
1128 }
1129 }
1130
1131 for (col_idx, cell) in row.cells.iter().enumerate() {
1132 let cell_width = if col_idx < table.column_widths.len() {
1134 table.column_widths[col_idx] * cell.colspan as f32
1135 } else if !table.column_widths.is_empty() {
1136 table.column_widths[0]
1137 } else {
1138 table.bbox.width / row.cells.len() as f32
1139 };
1140
1141 if let Some((r, g, b)) = cell.background {
1143 self.op(ContentStreamOp::SetFillColorRGB(r, g, b));
1144 self.op(ContentStreamOp::Rectangle(
1145 current_x, current_y, cell_width, row_height,
1146 ));
1147 self.op(ContentStreamOp::Fill);
1148 }
1149
1150 if !cell.text.is_empty() {
1152 let font_size = cell.font_size.unwrap_or(10.0);
1153 let font_name = if cell.bold {
1154 "Helvetica-Bold"
1155 } else {
1156 "Helvetica"
1157 };
1158
1159 let text_x = match cell.align {
1161 TableCellAlign::Left => current_x + padding,
1162 TableCellAlign::Center => current_x + cell_width / 2.0,
1163 TableCellAlign::Right => current_x + cell_width - padding,
1164 };
1165
1166 let text_y = current_y + row_height - padding - font_size;
1168
1169 self.begin_text();
1170 self.op(ContentStreamOp::SetFillColorRGB(0.0, 0.0, 0.0)); self.set_font(font_name, font_size);
1172 self.op(ContentStreamOp::SetTextMatrix(1.0, 0.0, 0.0, 1.0, text_x, text_y));
1173 self.op(ContentStreamOp::ShowText(cell.text.clone()));
1174 self.end_text();
1175 }
1176
1177 current_x += cell_width;
1178 }
1179 }
1180
1181 if style.border_width > 0.0 {
1183 let (r, g, b) = style.border_color;
1184 self.op(ContentStreamOp::SetStrokeColorRGB(r, g, b));
1185 self.op(ContentStreamOp::SetLineWidth(style.border_width));
1186
1187 if style.outer_border {
1189 self.op(ContentStreamOp::Rectangle(
1190 table.bbox.x,
1191 table.bbox.y,
1192 table.bbox.width,
1193 table.bbox.height,
1194 ));
1195 self.op(ContentStreamOp::Stroke);
1196 }
1197
1198 if style.horizontal_borders {
1200 let mut y = table.bbox.y + table.bbox.height;
1201 for row in &table.rows {
1202 let row_height = row
1203 .height
1204 .unwrap_or_else(|| table.bbox.height / table.rows.len() as f32);
1205 y -= row_height;
1206 if y > table.bbox.y {
1207 self.op(ContentStreamOp::MoveTo(table.bbox.x, y));
1208 self.op(ContentStreamOp::LineTo(table.bbox.x + table.bbox.width, y));
1209 self.op(ContentStreamOp::Stroke);
1210 }
1211 }
1212 }
1213
1214 if style.vertical_borders && !table.column_widths.is_empty() {
1216 let mut x = table.bbox.x;
1217 for (i, &width) in table.column_widths.iter().enumerate() {
1218 x += width;
1219 if i < table.column_widths.len() - 1 {
1220 self.op(ContentStreamOp::MoveTo(x, table.bbox.y));
1221 self.op(ContentStreamOp::LineTo(x, table.bbox.y + table.bbox.height));
1222 self.op(ContentStreamOp::Stroke);
1223 }
1224 }
1225 }
1226 }
1227
1228 self.op(ContentStreamOp::RestoreState);
1230
1231 self
1232 }
1233
1234 fn add_image_content(&mut self, image: &ImageContent) -> &mut Self {
1242 self.end_text();
1244
1245 let is_artifact = image.is_artifact;
1248 let has_alt = image.alt_text.is_some() && !is_artifact;
1249
1250 let mcid = if has_alt {
1251 let mcid = self.next_mcid();
1252 self.op(ContentStreamOp::BeginMarkedContentDict {
1253 tag: "Figure".to_string(),
1254 mcid,
1255 });
1256 Some(mcid)
1257 } else if is_artifact {
1258 self.op(ContentStreamOp::BeginArtifact {
1259 artifact_type: "Layout".to_string(),
1260 subtype: None,
1261 });
1262 None
1263 } else {
1264 None
1265 };
1266
1267 let had_matrix = if let Some(m) = image.matrix {
1270 self.op(ContentStreamOp::SaveState);
1271 self.op(ContentStreamOp::Transform(m[0], m[1], m[2], m[3], m[4], m[5]));
1272 true
1273 } else {
1274 false
1275 };
1276
1277 self.next_image_id += 1;
1279 let resource_id = format!("Im{}", self.next_image_id);
1280
1281 self.pending_images.push(PendingImage {
1283 image: image.clone(),
1284 resource_id: resource_id.clone(),
1285 });
1286
1287 self.draw_image(
1289 &resource_id,
1290 image.bbox.x,
1291 image.bbox.y,
1292 image.bbox.width,
1293 image.bbox.height,
1294 );
1295
1296 if had_matrix {
1297 self.op(ContentStreamOp::RestoreState);
1298 }
1299
1300 if has_alt {
1301 self.op(ContentStreamOp::EndMarkedContent);
1302 self.struct_records.push(StructElemRecord {
1305 structure_type: "Figure".to_string(),
1306 mcid: mcid.unwrap(),
1307 alt_text: image.alt_text.clone(),
1308 language: None,
1309 children: Vec::new(),
1310 });
1311 } else if is_artifact {
1312 self.op(ContentStreamOp::EndArtifact);
1313 }
1314
1315 self
1316 }
1317
1318 pub fn take_pending_images(&mut self) -> Vec<PendingImage> {
1323 std::mem::take(&mut self.pending_images)
1324 }
1325
1326 pub fn pending_images(&self) -> &[PendingImage] {
1328 &self.pending_images
1329 }
1330
1331 pub fn add_elements(&mut self, elements: &[ContentElement]) -> &mut Self {
1333 for element in elements {
1334 self.add_element(element);
1335 }
1336 self.end_text();
1338 self
1339 }
1340
1341 pub fn next_mcid(&mut self) -> u32 {
1343 let mcid = self.mcid_counter;
1344 self.mcid_counter += 1;
1345 mcid
1346 }
1347
1348 pub fn add_structure_element(&mut self, elem: &StructureElement) -> &mut Self {
1363 let record = self.add_structure_element_impl(elem);
1364 self.struct_records.push(record);
1365 self
1366 }
1367
1368 fn add_structure_element_impl(&mut self, elem: &StructureElement) -> StructElemRecord {
1374 let mcid = self.next_mcid();
1376
1377 self.op(ContentStreamOp::BeginMarkedContentDict {
1379 tag: elem.structure_type.clone(),
1380 mcid,
1381 });
1382
1383 let mut child_records: Vec<StructElemRecord> = Vec::new();
1385 for child in &elem.children {
1386 match child {
1387 ContentElement::Structure(nested_elem) => {
1388 let child_record = self.add_structure_element_impl(nested_elem);
1390 child_records.push(child_record);
1391 },
1392 _ => {
1393 self.add_element(child);
1395 },
1396 }
1397 }
1398
1399 self.op(ContentStreamOp::EndMarkedContent);
1401
1402 StructElemRecord {
1403 structure_type: elem.structure_type.clone(),
1404 mcid,
1405 alt_text: elem.alt_text.clone(),
1406 language: elem.language.clone(),
1407 children: child_records,
1408 }
1409 }
1410
1411 pub fn take_struct_records(&mut self) -> Vec<StructElemRecord> {
1416 std::mem::take(&mut self.struct_records)
1417 }
1418
1419 pub fn build(&self) -> Result<Vec<u8>> {
1427 self.build_with_remappers(&HashMap::new())
1428 }
1429
1430 pub fn build_with_remappers(
1439 &self,
1440 remappers: &HashMap<String, GlyphRemapper>,
1441 ) -> Result<Vec<u8>> {
1442 let mut buf = Vec::new();
1443
1444 for op in &self.operations {
1445 self.write_op(&mut buf, op, remappers)?;
1446 writeln!(buf)?;
1447 }
1448
1449 Ok(buf)
1450 }
1451
1452 fn write_op<W: Write>(
1454 &self,
1455 w: &mut W,
1456 op: &ContentStreamOp,
1457 remappers: &HashMap<String, GlyphRemapper>,
1458 ) -> std::io::Result<()> {
1459 match op {
1460 ContentStreamOp::SaveState => write!(w, "q"),
1461 ContentStreamOp::RestoreState => write!(w, "Q"),
1462 ContentStreamOp::Transform(a, b, c, d, e, f) => {
1463 write!(w, "{} {} {} {} {} {} cm", a, b, c, d, e, f)
1464 },
1465 ContentStreamOp::BeginText => write!(w, "BT"),
1466 ContentStreamOp::EndText => write!(w, "ET"),
1467 ContentStreamOp::SetFont(name, size) => write!(w, "/{} {} Tf", name, size),
1468 ContentStreamOp::MoveText(tx, ty) => write!(w, "{} {} Td", tx, ty),
1469 ContentStreamOp::SetTextMatrix(a, b, c, d, e, f) => {
1470 write!(w, "{} {} {} {} {} {} Tm", a, b, c, d, e, f)
1471 },
1472 ContentStreamOp::ShowText(text) => {
1473 write!(w, "(")?;
1474 self.write_escaped_string(w, text)?;
1475 write!(w, ") Tj")
1476 },
1477 ContentStreamOp::ShowHexText(hex) => {
1478 write!(w, "{} Tj", hex)
1480 },
1481 ContentStreamOp::ShowEmbeddedText {
1482 font_name,
1483 glyph_ids,
1484 } => {
1485 let remapper = remappers.get(font_name);
1489 write!(w, "<")?;
1490 for &orig in glyph_ids {
1491 let emitted = remapper.and_then(|r| r.get(orig)).unwrap_or(orig);
1492 write!(w, "{:04X}", emitted)?;
1493 }
1494 write!(w, "> Tj")
1495 },
1496 ContentStreamOp::ShowTextArray(items) => {
1497 write!(w, "[")?;
1498 for item in items {
1499 match item {
1500 TextArrayItem::Text(t) => {
1501 write!(w, "(")?;
1502 self.write_escaped_string(w, t)?;
1503 write!(w, ")")?;
1504 },
1505 TextArrayItem::HexText(hex) => {
1506 write!(w, "{}", hex)?;
1508 },
1509 TextArrayItem::Adjustment(adj) => {
1510 write!(w, "{}", adj)?;
1511 },
1512 }
1513 write!(w, " ")?;
1514 }
1515 write!(w, "] TJ")
1516 },
1517 ContentStreamOp::SetCharacterSpacing(spacing) => write!(w, "{} Tc", spacing),
1518 ContentStreamOp::SetWordSpacing(spacing) => write!(w, "{} Tw", spacing),
1519 ContentStreamOp::SetTextLeading(leading) => write!(w, "{} TL", leading),
1520 ContentStreamOp::NextLine => write!(w, "T*"),
1521 ContentStreamOp::SetFillColorRGB(r, g, b) => write!(w, "{} {} {} rg", r, g, b),
1522 ContentStreamOp::SetStrokeColorRGB(r, g, b) => write!(w, "{} {} {} RG", r, g, b),
1523 ContentStreamOp::SetFillColorGray(g) => write!(w, "{} g", g),
1524 ContentStreamOp::SetStrokeColorGray(g) => write!(w, "{} G", g),
1525 ContentStreamOp::SetLineWidth(width) => write!(w, "{} w", width),
1526 ContentStreamOp::MoveTo(x, y) => write!(w, "{} {} m", x, y),
1527 ContentStreamOp::LineTo(x, y) => write!(w, "{} {} l", x, y),
1528 ContentStreamOp::CurveTo(x1, y1, x2, y2, x3, y3) => {
1529 write!(w, "{} {} {} {} {} {} c", x1, y1, x2, y2, x3, y3)
1530 },
1531 ContentStreamOp::Rectangle(x, y, w_val, h) => {
1532 write!(w, "{} {} {} {} re", x, y, w_val, h)
1533 },
1534 ContentStreamOp::ClosePath => write!(w, "h"),
1535 ContentStreamOp::Stroke => write!(w, "S"),
1536 ContentStreamOp::Fill => write!(w, "f"),
1537 ContentStreamOp::FillStroke => write!(w, "B"),
1538 ContentStreamOp::CloseStroke => write!(w, "s"),
1539 ContentStreamOp::EndPath => write!(w, "n"),
1540 ContentStreamOp::PaintXObject(name) => write!(w, "/{} Do", name),
1541
1542 ContentStreamOp::BeginMarkedContentDict { tag, mcid } => {
1544 write!(w, "/{} <</MCID {}>> BDC", tag, mcid)
1545 },
1546 ContentStreamOp::EndMarkedContent => write!(w, "EMC"),
1547
1548 ContentStreamOp::BeginArtifact {
1550 artifact_type,
1551 subtype,
1552 } => {
1553 write!(w, "/Artifact <<")?;
1554 write!(w, "/Type /{}", artifact_type)?;
1555 if let Some(sub) = subtype {
1556 write!(w, " /Subtype /{}", sub)?;
1557 }
1558 write!(w, ">> BDC")
1559 },
1560 ContentStreamOp::EndArtifact => write!(w, "EMC"),
1561
1562 ContentStreamOp::Clip => write!(w, "W"),
1564 ContentStreamOp::ClipEvenOdd => write!(w, "W*"),
1565
1566 ContentStreamOp::SetExtGState(name) => write!(w, "/{} gs", name),
1568
1569 ContentStreamOp::SetFillColorSpace(name) => write!(w, "/{} cs", name),
1571 ContentStreamOp::SetStrokeColorSpace(name) => write!(w, "/{} CS", name),
1572 ContentStreamOp::SetFillColorN(components) => {
1573 for c in components {
1574 write!(w, "{} ", c)?;
1575 }
1576 write!(w, "scn")
1577 },
1578 ContentStreamOp::SetStrokeColorN(components) => {
1579 for c in components {
1580 write!(w, "{} ", c)?;
1581 }
1582 write!(w, "SCN")
1583 },
1584 ContentStreamOp::SetFillPattern(name, components) => {
1585 for c in components {
1586 write!(w, "{} ", c)?;
1587 }
1588 write!(w, "/{} scn", name)
1589 },
1590 ContentStreamOp::SetStrokePattern(name, components) => {
1591 for c in components {
1592 write!(w, "{} ", c)?;
1593 }
1594 write!(w, "/{} SCN", name)
1595 },
1596
1597 ContentStreamOp::PaintShading(name) => write!(w, "/{} sh", name),
1599
1600 ContentStreamOp::CurveToV(x2, y2, x3, y3) => {
1602 write!(w, "{} {} {} {} v", x2, y2, x3, y3)
1603 },
1604 ContentStreamOp::CurveToY(x1, y1, x3, y3) => {
1605 write!(w, "{} {} {} {} y", x1, y1, x3, y3)
1606 },
1607 ContentStreamOp::FillEvenOdd => write!(w, "f*"),
1608 ContentStreamOp::FillStrokeEvenOdd => write!(w, "B*"),
1609 ContentStreamOp::CloseFillStroke => write!(w, "b"),
1610 ContentStreamOp::CloseFillStrokeEvenOdd => write!(w, "b*"),
1611
1612 ContentStreamOp::SetLineCap(cap) => write!(w, "{} J", *cap as u8),
1614 ContentStreamOp::SetLineJoin(join) => write!(w, "{} j", *join as u8),
1615 ContentStreamOp::SetMiterLimit(limit) => write!(w, "{} M", limit),
1616 ContentStreamOp::SetDashPattern(pattern, phase) => {
1617 write!(w, "[")?;
1618 for (i, p) in pattern.iter().enumerate() {
1619 if i > 0 {
1620 write!(w, " ")?;
1621 }
1622 write!(w, "{}", p)?;
1623 }
1624 write!(w, "] {} d", phase)
1625 },
1626
1627 ContentStreamOp::SetFillColorCMYK(c, m, y, k) => {
1629 write!(w, "{} {} {} {} k", c, m, y, k)
1630 },
1631 ContentStreamOp::SetStrokeColorCMYK(c, m, y, k) => {
1632 write!(w, "{} {} {} {} K", c, m, y, k)
1633 },
1634
1635 ContentStreamOp::Raw(raw) => write!(w, "{}", raw),
1636 }
1637 }
1638
1639 fn write_escaped_string<W: Write>(&self, w: &mut W, text: &str) -> std::io::Result<()> {
1646 for ch in text.chars() {
1647 let cp = ch as u32;
1648 let cp = crate::fonts::encoding::math_alphanumeric_base(cp).unwrap_or(cp);
1654 let b = match crate::fonts::encoding::unicode_to_winansi(cp) {
1660 Some(b) => b,
1661 None => {
1662 w.write_all(b"?")?;
1663 continue;
1664 },
1665 };
1666 match b {
1667 b'(' => write!(w, "\\(")?,
1668 b')' => write!(w, "\\)")?,
1669 b'\\' => write!(w, "\\\\")?,
1670 b'\n' => write!(w, "\\n")?,
1671 b'\r' => write!(w, "\\r")?,
1672 b'\t' => write!(w, "\\t")?,
1673 _ => w.write_all(&[b])?,
1674 }
1675 }
1676 Ok(())
1677 }
1678}
1679
1680#[cfg(test)]
1681mod tests {
1682 use super::*;
1683 use crate::elements::{FontSpec, TextStyle};
1684 use crate::geometry::Rect;
1685
1686 #[test]
1687 fn test_simple_text() {
1688 let mut builder = ContentStreamBuilder::new();
1689 builder
1690 .begin_text()
1691 .set_font("Helvetica", 12.0)
1692 .text("Hello, World!", 72.0, 720.0)
1693 .end_text();
1694
1695 let bytes = builder.build().unwrap();
1696 let content = String::from_utf8_lossy(&bytes);
1697
1698 assert!(content.contains("BT"));
1699 assert!(content.contains("/Helvetica 12 Tf"));
1700 assert!(content.contains("(Hello, World!) Tj"));
1701 assert!(content.contains("ET"));
1702 }
1703
1704 #[test]
1705 fn test_text_content_element() {
1706 let text_content = TextContent {
1707 artifact_type: None,
1708 text: "Test".to_string(),
1709 bbox: Rect::new(100.0, 700.0, 50.0, 12.0),
1710 font: FontSpec::new("Helvetica", 12.0),
1711 style: TextStyle::default(),
1712 reading_order: Some(0),
1713 origin: None,
1714 rotation_degrees: None,
1715 matrix: None,
1716 };
1717
1718 let mut builder = ContentStreamBuilder::new();
1719 builder.add_element(&ContentElement::Text(text_content));
1720 builder.end_text();
1721
1722 let bytes = builder.build().unwrap();
1723 let content = String::from_utf8_lossy(&bytes);
1724
1725 assert!(content.contains("BT"));
1726 assert!(content.contains("100 700"));
1727 assert!(content.contains("(Test) Tj"));
1728 assert!(content.contains("ET"));
1729 }
1730
1731 #[test]
1732 fn test_path_operations() {
1733 let mut builder = ContentStreamBuilder::new();
1734 builder
1735 .stroke_color(Color::black())
1736 .op(ContentStreamOp::SetLineWidth(1.0))
1737 .op(ContentStreamOp::MoveTo(0.0, 0.0))
1738 .op(ContentStreamOp::LineTo(100.0, 100.0))
1739 .stroke();
1740
1741 let bytes = builder.build().unwrap();
1742 let content = String::from_utf8_lossy(&bytes);
1743
1744 assert!(content.contains("0 0 0 RG"));
1745 assert!(content.contains("1 w"));
1746 assert!(content.contains("0 0 m"));
1747 assert!(content.contains("100 100 l"));
1748 assert!(content.contains("S"));
1749 }
1750
1751 #[test]
1752 fn test_marked_content_operators() {
1753 let mut builder = ContentStreamBuilder::new();
1754
1755 builder
1756 .op(ContentStreamOp::BeginMarkedContentDict {
1757 tag: "P".to_string(),
1758 mcid: 0,
1759 })
1760 .op(ContentStreamOp::EndMarkedContent);
1761
1762 let bytes = builder.build().unwrap();
1763 let content = String::from_utf8_lossy(&bytes);
1764
1765 assert!(content.contains("/P <</MCID 0>> BDC"));
1766 assert!(content.contains("EMC"));
1767 }
1768
1769 #[test]
1770 fn test_mcid_allocation() {
1771 let mut builder = ContentStreamBuilder::new();
1772 assert_eq!(builder.next_mcid(), 0);
1773 assert_eq!(builder.next_mcid(), 1);
1774 assert_eq!(builder.next_mcid(), 2);
1775 }
1776
1777 #[test]
1778 fn test_structure_element_with_text() {
1779 use crate::elements::FontSpec;
1780 use crate::geometry::Rect;
1781
1782 let text_content = TextContent {
1783 artifact_type: None,
1784 text: "Hello".to_string(),
1785 bbox: Rect::new(100.0, 700.0, 50.0, 12.0),
1786 font: FontSpec::new("Helvetica", 12.0),
1787 style: TextStyle::default(),
1788 reading_order: Some(0),
1789 origin: None,
1790 rotation_degrees: None,
1791 matrix: None,
1792 };
1793
1794 let structure = StructureElement {
1795 structure_type: "P".to_string(),
1796 bbox: Rect::new(100.0, 700.0, 200.0, 50.0),
1797 children: vec![ContentElement::Text(text_content)],
1798 reading_order: Some(0),
1799 alt_text: None,
1800 language: None,
1801 };
1802
1803 let mut builder = ContentStreamBuilder::new();
1804 builder.add_structure_element(&structure);
1805 builder.end_text();
1806
1807 let bytes = builder.build().unwrap();
1808 let content = String::from_utf8_lossy(&bytes);
1809
1810 assert!(content.contains("/P <</MCID 0>> BDC"));
1811 assert!(content.contains("EMC"));
1812 assert!(content.contains("(Hello) Tj"));
1813 }
1814
1815 #[test]
1816 fn test_nested_structure_elements() {
1817 use crate::geometry::Rect;
1818
1819 let inner_structure = StructureElement {
1820 structure_type: "Span".to_string(),
1821 bbox: Rect::new(100.0, 700.0, 50.0, 12.0),
1822 children: vec![],
1823 reading_order: None,
1824 alt_text: None,
1825 language: None,
1826 };
1827
1828 let outer_structure = StructureElement {
1829 structure_type: "P".to_string(),
1830 bbox: Rect::new(100.0, 700.0, 200.0, 50.0),
1831 children: vec![ContentElement::Structure(inner_structure)],
1832 reading_order: Some(0),
1833 alt_text: None,
1834 language: None,
1835 };
1836
1837 let mut builder = ContentStreamBuilder::new();
1838 builder.add_structure_element(&outer_structure);
1839
1840 let bytes = builder.build().unwrap();
1841 let content = String::from_utf8_lossy(&bytes);
1842
1843 assert!(content.contains("/P <</MCID 0>> BDC"));
1845 assert!(content.contains("/Span <</MCID 1>> BDC"));
1846
1847 let emc_count = content.matches("EMC").count();
1849 assert_eq!(emc_count, 2);
1850 }
1851
1852 #[test]
1853 fn test_rectangle() {
1854 let mut builder = ContentStreamBuilder::new();
1855 builder.rect(72.0, 72.0, 468.0, 648.0).stroke();
1856
1857 let bytes = builder.build().unwrap();
1858 let content = String::from_utf8_lossy(&bytes);
1859
1860 assert!(content.contains("72 72 468 648 re"));
1861 assert!(content.contains("S"));
1862 }
1863
1864 #[test]
1865 fn test_escaped_text() {
1866 let mut builder = ContentStreamBuilder::new();
1867 builder
1868 .begin_text()
1869 .set_font("Helvetica", 12.0)
1870 .text("Text with (parens) and \\backslash", 72.0, 720.0)
1871 .end_text();
1872
1873 let bytes = builder.build().unwrap();
1874 let content = String::from_utf8_lossy(&bytes);
1875
1876 assert!(content.contains("\\(parens\\)"));
1877 assert!(content.contains("\\\\backslash"));
1878 }
1879
1880 #[test]
1881 fn test_font_mapping() {
1882 let builder = ContentStreamBuilder::new();
1883
1884 assert_eq!(builder.map_font_name("Arial", false), "Helvetica");
1885 assert_eq!(builder.map_font_name("Arial", true), "Helvetica-Bold");
1886 assert_eq!(builder.map_font_name("Times New Roman", false), "Times-Roman");
1887 assert_eq!(builder.map_font_name("Courier", false), "Courier");
1888 }
1889
1890 #[test]
1897 fn test_font_mapping_explicit_standard14() {
1898 let b = ContentStreamBuilder::new();
1899
1900 for f in [
1902 "Helvetica",
1903 "Helvetica-Bold",
1904 "Helvetica-Oblique",
1905 "Helvetica-BoldOblique",
1906 "Times-Roman",
1907 "Times-Bold",
1908 "Times-Italic",
1909 "Times-BoldItalic",
1910 "Courier",
1911 "Courier-Bold",
1912 "Courier-Oblique",
1913 "Courier-BoldOblique",
1914 ] {
1915 assert_eq!(b.map_font_name(f, false), f, "{f} did not round-trip");
1916 }
1917
1918 assert_eq!(b.map_font_name("Helvetica", true), "Helvetica-Bold");
1920 assert_eq!(b.map_font_name("Helvetica-Oblique", true), "Helvetica-BoldOblique");
1921
1922 assert_eq!(b.map_font_name("helvetica-bold", false), "Helvetica-Bold");
1924 assert_eq!(b.map_font_name("Arial Bold", false), "Helvetica-Bold");
1925 assert_eq!(b.map_font_name("Times New Roman Italic", false), "Times-Italic");
1926
1927 assert_eq!(b.map_font_name("Symbol", false), "Helvetica");
1934 assert_eq!(b.map_font_name("Symbol", true), "Helvetica-Bold");
1935 assert_eq!(b.map_font_name("ZapfDingbats", true), "Helvetica-Bold");
1936 }
1937
1938 #[test]
1939 fn test_table_content_rendering() {
1940 use crate::elements::{TableCellContent, TableContent, TableContentStyle, TableRowContent};
1941
1942 let mut table = TableContent::new(Rect::new(72.0, 600.0, 200.0, 100.0));
1944 table.column_widths = vec![100.0, 100.0];
1945 table.style = TableContentStyle::bordered();
1946
1947 let header = TableRowContent::header(vec![
1949 TableCellContent::header("Name"),
1950 TableCellContent::header("Value"),
1951 ]);
1952 table.add_row(header);
1953
1954 let row =
1956 TableRowContent::new(vec![TableCellContent::new("Item"), TableCellContent::new("100")]);
1957 table.add_row(row);
1958
1959 let mut builder = ContentStreamBuilder::new();
1960 builder.add_element(&ContentElement::Table(table));
1961
1962 let bytes = builder.build().unwrap();
1963 let content = String::from_utf8_lossy(&bytes);
1964
1965 assert!(content.contains("q")); assert!(content.contains("Q")); assert!(content.contains("(Name) Tj"));
1971 assert!(content.contains("(Value) Tj"));
1972 assert!(content.contains("(Item) Tj"));
1973 assert!(content.contains("(100) Tj"));
1974
1975 assert!(content.contains("re")); assert!(content.contains("S")); assert!(builder.pending_images().is_empty());
1981 }
1982
1983 #[test]
1984 fn test_image_content_rendering() {
1985 use crate::elements::{ColorSpace, ImageContent, ImageFormat};
1986
1987 let image = ImageContent {
1989 bbox: Rect::new(100.0, 500.0, 200.0, 150.0),
1990 format: ImageFormat::Jpeg,
1991 data: vec![0xFF, 0xD8, 0xFF, 0xE0], width: 800,
1993 height: 600,
1994 bits_per_component: 8,
1995 color_space: ColorSpace::RGB,
1996 reading_order: Some(0),
1997 alt_text: Some("Test image".to_string()),
1998 horizontal_dpi: None,
1999 vertical_dpi: None,
2000 soft_mask: None,
2001 matrix: None,
2002 is_artifact: false,
2003 };
2004
2005 let mut builder = ContentStreamBuilder::new();
2006 builder.add_element(&ContentElement::Image(image));
2007
2008 let bytes = builder.build().unwrap();
2009 let content = String::from_utf8_lossy(&bytes);
2010
2011 assert!(content.contains("q")); assert!(content.contains("Q")); assert!(content.contains("cm")); assert!(content.contains("Do")); let pending = builder.pending_images();
2019 assert_eq!(pending.len(), 1);
2020 assert_eq!(pending[0].resource_id, "Im1");
2021 assert_eq!(pending[0].image.width, 800);
2022 assert_eq!(pending[0].image.height, 600);
2023 }
2024
2025 #[test]
2026 fn test_mixed_content_elements() {
2027 use crate::elements::{
2028 ColorSpace, ImageContent, ImageFormat, TableCellContent, TableContent,
2029 TableContentStyle, TableRowContent,
2030 };
2031
2032 let mut builder = ContentStreamBuilder::new();
2033
2034 let text_content = TextContent {
2036 artifact_type: None,
2037 text: "Header".to_string(),
2038 bbox: Rect::new(72.0, 720.0, 100.0, 14.0),
2039 font: FontSpec::new("Helvetica", 14.0),
2040 style: TextStyle::default(),
2041 reading_order: Some(0),
2042 origin: None,
2043 rotation_degrees: None,
2044 matrix: None,
2045 };
2046 builder.add_element(&ContentElement::Text(text_content));
2047
2048 let mut table = TableContent::new(Rect::new(72.0, 600.0, 200.0, 50.0));
2050 table.column_widths = vec![200.0];
2051 table.style = TableContentStyle::minimal();
2052 table.add_row(TableRowContent::new(vec![TableCellContent::new("Row 1")]));
2053 builder.add_element(&ContentElement::Table(table));
2054
2055 let image = ImageContent {
2057 bbox: Rect::new(72.0, 400.0, 100.0, 100.0),
2058 format: ImageFormat::Png,
2059 data: vec![0x89, 0x50, 0x4E, 0x47], width: 200,
2061 height: 200,
2062 bits_per_component: 8,
2063 color_space: ColorSpace::RGB,
2064 reading_order: Some(2),
2065 alt_text: None,
2066 horizontal_dpi: None,
2067 vertical_dpi: None,
2068 soft_mask: None,
2069 matrix: None,
2070 is_artifact: false,
2071 };
2072 builder.add_element(&ContentElement::Image(image));
2073
2074 let bytes = builder.build().unwrap();
2075 let content = String::from_utf8_lossy(&bytes);
2076
2077 assert!(content.contains("(Header) Tj")); assert!(content.contains("(Row 1) Tj")); assert!(content.contains("/Im1 Do")); assert_eq!(builder.pending_images().len(), 1);
2084 }
2085
2086 #[test]
2087 fn test_take_pending_images() {
2088 use crate::elements::{ColorSpace, ImageContent, ImageFormat};
2089
2090 let image = ImageContent {
2091 bbox: Rect::new(0.0, 0.0, 100.0, 100.0),
2092 format: ImageFormat::Jpeg,
2093 data: vec![0xFF, 0xD8],
2094 width: 100,
2095 height: 100,
2096 bits_per_component: 8,
2097 color_space: ColorSpace::RGB,
2098 reading_order: None,
2099 alt_text: None,
2100 horizontal_dpi: None,
2101 vertical_dpi: None,
2102 soft_mask: None,
2103 matrix: None,
2104 is_artifact: false,
2105 };
2106
2107 let mut builder = ContentStreamBuilder::new();
2108 builder.add_element(&ContentElement::Image(image));
2109
2110 let pending = builder.take_pending_images();
2112 assert_eq!(pending.len(), 1);
2113
2114 assert!(builder.pending_images().is_empty());
2116 assert!(builder.take_pending_images().is_empty());
2117 }
2118
2119 #[test]
2122 fn test_save_restore_state() {
2123 let mut builder = ContentStreamBuilder::new();
2124 builder.save_state().restore_state();
2125
2126 let bytes = builder.build().unwrap();
2127 let content = String::from_utf8_lossy(&bytes);
2128 assert!(content.contains("q\n"));
2129 assert!(content.contains("Q\n"));
2130 }
2131
2132 #[test]
2133 fn test_transform_matrix() {
2134 let mut builder = ContentStreamBuilder::new();
2135 builder.transform(1.0, 0.0, 0.0, 1.0, 100.0, 200.0);
2136
2137 let bytes = builder.build().unwrap();
2138 let content = String::from_utf8_lossy(&bytes);
2139 assert!(content.contains("1 0 0 1 100 200 cm"));
2140 }
2141
2142 #[test]
2143 fn test_translate() {
2144 let mut builder = ContentStreamBuilder::new();
2145 builder.translate(50.0, 75.0);
2146
2147 let bytes = builder.build().unwrap();
2148 let content = String::from_utf8_lossy(&bytes);
2149 assert!(content.contains("1 0 0 1 50 75 cm"));
2150 }
2151
2152 #[test]
2153 fn test_scale() {
2154 let mut builder = ContentStreamBuilder::new();
2155 builder.scale(2.0, 3.0);
2156
2157 let bytes = builder.build().unwrap();
2158 let content = String::from_utf8_lossy(&bytes);
2159 assert!(content.contains("2 0 0 3 0 0 cm"));
2160 }
2161
2162 #[test]
2163 fn test_rotate() {
2164 let mut builder = ContentStreamBuilder::new();
2165 builder.rotate(std::f32::consts::PI / 2.0);
2166
2167 let bytes = builder.build().unwrap();
2168 let content = String::from_utf8_lossy(&bytes);
2169 assert!(content.contains("cm"));
2170 }
2171
2172 #[test]
2173 fn test_rotate_degrees() {
2174 let mut builder = ContentStreamBuilder::new();
2175 builder.rotate_degrees(90.0);
2176
2177 let bytes = builder.build().unwrap();
2178 let content = String::from_utf8_lossy(&bytes);
2179 assert!(content.contains("cm"));
2180 }
2181
2182 #[test]
2183 fn test_fill_color() {
2184 let mut builder = ContentStreamBuilder::new();
2185 builder.fill_color(Color {
2186 r: 1.0,
2187 g: 0.0,
2188 b: 0.0,
2189 });
2190
2191 let bytes = builder.build().unwrap();
2192 let content = String::from_utf8_lossy(&bytes);
2193 assert!(content.contains("1 0 0 rg"));
2194 }
2195
2196 #[test]
2197 fn test_stroke_color() {
2198 let mut builder = ContentStreamBuilder::new();
2199 builder.stroke_color(Color {
2200 r: 0.0,
2201 g: 1.0,
2202 b: 0.0,
2203 });
2204
2205 let bytes = builder.build().unwrap();
2206 let content = String::from_utf8_lossy(&bytes);
2207 assert!(content.contains("0 1 0 RG"));
2208 }
2209
2210 #[test]
2211 fn test_set_fill_color_rgb() {
2212 let mut builder = ContentStreamBuilder::new();
2213 builder.set_fill_color(0.5, 0.6, 0.7);
2214
2215 let bytes = builder.build().unwrap();
2216 let content = String::from_utf8_lossy(&bytes);
2217 assert!(content.contains("0.5 0.6 0.7 rg"));
2218 }
2219
2220 #[test]
2221 fn test_set_stroke_color_rgb() {
2222 let mut builder = ContentStreamBuilder::new();
2223 builder.set_stroke_color(0.1, 0.2, 0.3);
2224
2225 let bytes = builder.build().unwrap();
2226 let content = String::from_utf8_lossy(&bytes);
2227 assert!(content.contains("0.1 0.2 0.3 RG"));
2228 }
2229
2230 #[test]
2231 fn test_set_line_width() {
2232 let mut builder = ContentStreamBuilder::new();
2233 builder.set_line_width(2.5);
2234
2235 let bytes = builder.build().unwrap();
2236 let content = String::from_utf8_lossy(&bytes);
2237 assert!(content.contains("2.5 w"));
2238 }
2239
2240 #[test]
2241 fn test_move_to_and_line_to() {
2242 let mut builder = ContentStreamBuilder::new();
2243 builder.move_to(10.0, 20.0).line_to(30.0, 40.0);
2244
2245 let bytes = builder.build().unwrap();
2246 let content = String::from_utf8_lossy(&bytes);
2247 assert!(content.contains("10 20 m"));
2248 assert!(content.contains("30 40 l"));
2249 }
2250
2251 #[test]
2252 fn test_close_path() {
2253 let mut builder = ContentStreamBuilder::new();
2254 builder
2255 .move_to(0.0, 0.0)
2256 .line_to(100.0, 0.0)
2257 .line_to(100.0, 100.0)
2258 .close_path();
2259
2260 let bytes = builder.build().unwrap();
2261 let content = String::from_utf8_lossy(&bytes);
2262 assert!(content.contains("h\n"));
2263 }
2264
2265 #[test]
2266 fn test_fill() {
2267 let mut builder = ContentStreamBuilder::new();
2268 builder.rect(0.0, 0.0, 100.0, 100.0).fill();
2269
2270 let bytes = builder.build().unwrap();
2271 let content = String::from_utf8_lossy(&bytes);
2272 assert!(content.contains("re\n"));
2273 assert!(content.contains("f\n"));
2274 }
2275
2276 #[test]
2277 fn test_fill_stroke() {
2278 let mut builder = ContentStreamBuilder::new();
2279 builder.rect(0.0, 0.0, 100.0, 100.0).fill_stroke();
2280
2281 let bytes = builder.build().unwrap();
2282 let content = String::from_utf8_lossy(&bytes);
2283 assert!(content.contains("B\n"));
2284 }
2285
2286 #[test]
2287 fn test_fill_even_odd() {
2288 let mut builder = ContentStreamBuilder::new();
2289 builder.rect(0.0, 0.0, 100.0, 100.0).fill_even_odd();
2290
2291 let bytes = builder.build().unwrap();
2292 let content = String::from_utf8_lossy(&bytes);
2293 assert!(content.contains("f*\n"));
2294 }
2295
2296 #[test]
2297 fn test_fill_stroke_even_odd() {
2298 let mut builder = ContentStreamBuilder::new();
2299 builder.rect(0.0, 0.0, 100.0, 100.0).fill_stroke_even_odd();
2300
2301 let bytes = builder.build().unwrap();
2302 let content = String::from_utf8_lossy(&bytes);
2303 assert!(content.contains("B*\n"));
2304 }
2305
2306 #[test]
2307 fn test_close_fill_stroke() {
2308 let mut builder = ContentStreamBuilder::new();
2309 builder
2310 .move_to(0.0, 0.0)
2311 .line_to(100.0, 0.0)
2312 .close_fill_stroke();
2313
2314 let bytes = builder.build().unwrap();
2315 let content = String::from_utf8_lossy(&bytes);
2316 assert!(content.contains("b\n"));
2317 }
2318
2319 #[test]
2320 fn test_clip() {
2321 let mut builder = ContentStreamBuilder::new();
2322 builder.rect(10.0, 10.0, 200.0, 200.0).clip().end_path();
2323
2324 let bytes = builder.build().unwrap();
2325 let content = String::from_utf8_lossy(&bytes);
2326 assert!(content.contains("W\n"));
2327 assert!(content.contains("n\n"));
2328 }
2329
2330 #[test]
2331 fn test_clip_even_odd() {
2332 let mut builder = ContentStreamBuilder::new();
2333 builder
2334 .rect(10.0, 10.0, 200.0, 200.0)
2335 .clip_even_odd()
2336 .end_path();
2337
2338 let bytes = builder.build().unwrap();
2339 let content = String::from_utf8_lossy(&bytes);
2340 assert!(content.contains("W*\n"));
2341 }
2342
2343 #[test]
2344 fn test_clip_rect() {
2345 let mut builder = ContentStreamBuilder::new();
2346 builder.clip_rect(10.0, 10.0, 200.0, 200.0);
2347
2348 let bytes = builder.build().unwrap();
2349 let content = String::from_utf8_lossy(&bytes);
2350 assert!(content.contains("10 10 200 200 re"));
2351 assert!(content.contains("W\n"));
2352 assert!(content.contains("n\n"));
2353 }
2354
2355 #[test]
2356 fn test_end_path() {
2357 let mut builder = ContentStreamBuilder::new();
2358 builder.rect(0.0, 0.0, 100.0, 100.0).end_path();
2359
2360 let bytes = builder.build().unwrap();
2361 let content = String::from_utf8_lossy(&bytes);
2362 assert!(content.contains("n\n"));
2363 }
2364
2365 #[test]
2366 fn test_set_ext_gstate() {
2367 let mut builder = ContentStreamBuilder::new();
2368 builder.set_ext_gstate("GS0");
2369
2370 let bytes = builder.build().unwrap();
2371 let content = String::from_utf8_lossy(&bytes);
2372 assert!(content.contains("/GS0 gs"));
2373 }
2374
2375 #[test]
2376 fn test_curve_to() {
2377 let mut builder = ContentStreamBuilder::new();
2378 builder
2379 .move_to(0.0, 0.0)
2380 .curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0);
2381
2382 let bytes = builder.build().unwrap();
2383 let content = String::from_utf8_lossy(&bytes);
2384 assert!(content.contains("10 20 30 40 50 60 c"));
2385 }
2386
2387 #[test]
2388 fn test_curve_to_v() {
2389 let mut builder = ContentStreamBuilder::new();
2390 builder.move_to(0.0, 0.0).curve_to_v(10.0, 20.0, 30.0, 40.0);
2391
2392 let bytes = builder.build().unwrap();
2393 let content = String::from_utf8_lossy(&bytes);
2394 assert!(content.contains("10 20 30 40 v"));
2395 }
2396
2397 #[test]
2398 fn test_curve_to_y() {
2399 let mut builder = ContentStreamBuilder::new();
2400 builder.move_to(0.0, 0.0).curve_to_y(10.0, 20.0, 30.0, 40.0);
2401
2402 let bytes = builder.build().unwrap();
2403 let content = String::from_utf8_lossy(&bytes);
2404 assert!(content.contains("10 20 30 40 y"));
2405 }
2406
2407 #[test]
2408 fn test_circle() {
2409 let mut builder = ContentStreamBuilder::new();
2410 builder.circle(100.0, 100.0, 50.0);
2411
2412 let bytes = builder.build().unwrap();
2413 let content = String::from_utf8_lossy(&bytes);
2414 assert!(content.contains("m\n"));
2416 assert!(content.contains("c\n"));
2417 assert!(content.contains("h\n")); }
2419
2420 #[test]
2421 fn test_ellipse() {
2422 let mut builder = ContentStreamBuilder::new();
2423 builder.ellipse(200.0, 200.0, 80.0, 40.0);
2424
2425 let bytes = builder.build().unwrap();
2426 let content = String::from_utf8_lossy(&bytes);
2427 assert!(content.contains("m\n"));
2428 assert!(content.contains("c\n"));
2429 assert!(content.contains("h\n"));
2430 }
2431
2432 #[test]
2433 fn test_rounded_rect() {
2434 let mut builder = ContentStreamBuilder::new();
2435 builder.rounded_rect(50.0, 50.0, 200.0, 100.0, 10.0);
2436
2437 let bytes = builder.build().unwrap();
2438 let content = String::from_utf8_lossy(&bytes);
2439 assert!(content.contains("m\n"));
2441 assert!(content.contains("l\n"));
2442 assert!(content.contains("c\n"));
2443 assert!(content.contains("h\n"));
2444 }
2445
2446 #[test]
2447 fn test_rounded_rect_large_radius() {
2448 let mut builder = ContentStreamBuilder::new();
2449 builder.rounded_rect(0.0, 0.0, 20.0, 40.0, 50.0);
2451
2452 let bytes = builder.build().unwrap();
2453 let content = String::from_utf8_lossy(&bytes);
2454 assert!(content.contains("m\n"));
2455 }
2456
2457 #[test]
2458 fn test_set_line_cap() {
2459 let mut builder = ContentStreamBuilder::new();
2460 builder.set_line_cap(LineCap::Round);
2461
2462 let bytes = builder.build().unwrap();
2463 let content = String::from_utf8_lossy(&bytes);
2464 assert!(content.contains("1 J"));
2465 }
2466
2467 #[test]
2468 fn test_set_line_cap_square() {
2469 let mut builder = ContentStreamBuilder::new();
2470 builder.set_line_cap(LineCap::Square);
2471
2472 let bytes = builder.build().unwrap();
2473 let content = String::from_utf8_lossy(&bytes);
2474 assert!(content.contains("2 J"));
2475 }
2476
2477 #[test]
2478 fn test_set_line_join() {
2479 let mut builder = ContentStreamBuilder::new();
2480 builder.set_line_join(LineJoin::Round);
2481
2482 let bytes = builder.build().unwrap();
2483 let content = String::from_utf8_lossy(&bytes);
2484 assert!(content.contains("1 j"));
2485 }
2486
2487 #[test]
2488 fn test_set_line_join_bevel() {
2489 let mut builder = ContentStreamBuilder::new();
2490 builder.set_line_join(LineJoin::Bevel);
2491
2492 let bytes = builder.build().unwrap();
2493 let content = String::from_utf8_lossy(&bytes);
2494 assert!(content.contains("2 j"));
2495 }
2496
2497 #[test]
2498 fn test_set_miter_limit() {
2499 let mut builder = ContentStreamBuilder::new();
2500 builder.set_miter_limit(10.0);
2501
2502 let bytes = builder.build().unwrap();
2503 let content = String::from_utf8_lossy(&bytes);
2504 assert!(content.contains("10 M"));
2505 }
2506
2507 #[test]
2508 fn test_set_dash_pattern() {
2509 let mut builder = ContentStreamBuilder::new();
2510 builder.set_dash_pattern(vec![3.0, 2.0], 0.0);
2511
2512 let bytes = builder.build().unwrap();
2513 let content = String::from_utf8_lossy(&bytes);
2514 assert!(content.contains("[3 2] 0 d"));
2515 }
2516
2517 #[test]
2518 fn test_set_solid_line() {
2519 let mut builder = ContentStreamBuilder::new();
2520 builder.set_solid_line();
2521
2522 let bytes = builder.build().unwrap();
2523 let content = String::from_utf8_lossy(&bytes);
2524 assert!(content.contains("[] 0 d"));
2525 }
2526
2527 #[test]
2528 fn test_set_fill_color_space() {
2529 let mut builder = ContentStreamBuilder::new();
2530 builder.set_fill_color_space("DeviceRGB");
2531
2532 let bytes = builder.build().unwrap();
2533 let content = String::from_utf8_lossy(&bytes);
2534 assert!(content.contains("/DeviceRGB cs"));
2535 }
2536
2537 #[test]
2538 fn test_set_stroke_color_space() {
2539 let mut builder = ContentStreamBuilder::new();
2540 builder.set_stroke_color_space("DeviceCMYK");
2541
2542 let bytes = builder.build().unwrap();
2543 let content = String::from_utf8_lossy(&bytes);
2544 assert!(content.contains("/DeviceCMYK CS"));
2545 }
2546
2547 #[test]
2548 fn test_set_fill_color_n() {
2549 let mut builder = ContentStreamBuilder::new();
2550 builder.set_fill_color_n(vec![0.1, 0.2, 0.3]);
2551
2552 let bytes = builder.build().unwrap();
2553 let content = String::from_utf8_lossy(&bytes);
2554 assert!(content.contains("0.1 0.2 0.3 scn"));
2555 }
2556
2557 #[test]
2558 fn test_set_stroke_color_n() {
2559 let mut builder = ContentStreamBuilder::new();
2560 builder.set_stroke_color_n(vec![0.4, 0.5]);
2561
2562 let bytes = builder.build().unwrap();
2563 let content = String::from_utf8_lossy(&bytes);
2564 assert!(content.contains("0.4 0.5 SCN"));
2565 }
2566
2567 #[test]
2568 fn test_set_fill_color_cmyk() {
2569 let mut builder = ContentStreamBuilder::new();
2570 builder.set_fill_color_cmyk(0.0, 1.0, 1.0, 0.0);
2571
2572 let bytes = builder.build().unwrap();
2573 let content = String::from_utf8_lossy(&bytes);
2574 assert!(content.contains("0 1 1 0 k"));
2575 }
2576
2577 #[test]
2578 fn test_set_stroke_color_cmyk() {
2579 let mut builder = ContentStreamBuilder::new();
2580 builder.set_stroke_color_cmyk(1.0, 0.0, 0.0, 0.0);
2581
2582 let bytes = builder.build().unwrap();
2583 let content = String::from_utf8_lossy(&bytes);
2584 assert!(content.contains("1 0 0 0 K"));
2585 }
2586
2587 #[test]
2588 fn test_set_fill_pattern() {
2589 let mut builder = ContentStreamBuilder::new();
2590 builder.set_fill_pattern("P1", vec![]);
2591
2592 let bytes = builder.build().unwrap();
2593 let content = String::from_utf8_lossy(&bytes);
2594 assert!(content.contains("/P1 scn"));
2595 }
2596
2597 #[test]
2598 fn test_set_stroke_pattern() {
2599 let mut builder = ContentStreamBuilder::new();
2600 builder.set_stroke_pattern("P2", vec![0.5]);
2601
2602 let bytes = builder.build().unwrap();
2603 let content = String::from_utf8_lossy(&bytes);
2604 assert!(content.contains("0.5 /P2 SCN"));
2605 }
2606
2607 #[test]
2608 fn test_paint_shading() {
2609 let mut builder = ContentStreamBuilder::new();
2610 builder.paint_shading("Sh1");
2611
2612 let bytes = builder.build().unwrap();
2613 let content = String::from_utf8_lossy(&bytes);
2614 assert!(content.contains("/Sh1 sh"));
2615 }
2616
2617 #[test]
2618 fn test_draw_gradient_rect() {
2619 let mut builder = ContentStreamBuilder::new();
2620 builder.draw_gradient_rect("Sh0", 10.0, 20.0, 200.0, 100.0);
2621
2622 let bytes = builder.build().unwrap();
2623 let content = String::from_utf8_lossy(&bytes);
2624 assert!(content.contains("q\n")); assert!(content.contains("10 20 200 100 re"));
2626 assert!(content.contains("W\n")); assert!(content.contains("n\n")); assert!(content.contains("/Sh0 sh"));
2629 assert!(content.contains("Q\n")); }
2631
2632 #[test]
2633 fn test_paint_xobject() {
2634 let mut builder = ContentStreamBuilder::new();
2635 builder.op(ContentStreamOp::PaintXObject("Img0".to_string()));
2636
2637 let bytes = builder.build().unwrap();
2638 let content = String::from_utf8_lossy(&bytes);
2639 assert!(content.contains("/Img0 Do"));
2640 }
2641
2642 #[test]
2643 fn test_close_stroke() {
2644 let mut builder = ContentStreamBuilder::new();
2645 builder.op(ContentStreamOp::CloseStroke);
2646
2647 let bytes = builder.build().unwrap();
2648 let content = String::from_utf8_lossy(&bytes);
2649 assert!(content.contains("s\n"));
2650 }
2651
2652 #[test]
2653 fn test_close_fill_stroke_even_odd() {
2654 let mut builder = ContentStreamBuilder::new();
2655 builder.op(ContentStreamOp::CloseFillStrokeEvenOdd);
2656
2657 let bytes = builder.build().unwrap();
2658 let content = String::from_utf8_lossy(&bytes);
2659 assert!(content.contains("b*\n"));
2660 }
2661
2662 #[test]
2663 fn test_set_fill_color_gray() {
2664 let mut builder = ContentStreamBuilder::new();
2665 builder.op(ContentStreamOp::SetFillColorGray(0.5));
2666
2667 let bytes = builder.build().unwrap();
2668 let content = String::from_utf8_lossy(&bytes);
2669 assert!(content.contains("0.5 g"));
2670 }
2671
2672 #[test]
2673 fn test_set_stroke_color_gray() {
2674 let mut builder = ContentStreamBuilder::new();
2675 builder.op(ContentStreamOp::SetStrokeColorGray(0.75));
2676
2677 let bytes = builder.build().unwrap();
2678 let content = String::from_utf8_lossy(&bytes);
2679 assert!(content.contains("0.75 G"));
2680 }
2681
2682 #[test]
2683 fn test_set_character_spacing() {
2684 let mut builder = ContentStreamBuilder::new();
2685 builder.op(ContentStreamOp::SetCharacterSpacing(2.0));
2686
2687 let bytes = builder.build().unwrap();
2688 let content = String::from_utf8_lossy(&bytes);
2689 assert!(content.contains("2 Tc"));
2690 }
2691
2692 #[test]
2693 fn test_set_word_spacing() {
2694 let mut builder = ContentStreamBuilder::new();
2695 builder.op(ContentStreamOp::SetWordSpacing(5.0));
2696
2697 let bytes = builder.build().unwrap();
2698 let content = String::from_utf8_lossy(&bytes);
2699 assert!(content.contains("5 Tw"));
2700 }
2701
2702 #[test]
2703 fn test_set_text_leading() {
2704 let mut builder = ContentStreamBuilder::new();
2705 builder.op(ContentStreamOp::SetTextLeading(14.0));
2706
2707 let bytes = builder.build().unwrap();
2708 let content = String::from_utf8_lossy(&bytes);
2709 assert!(content.contains("14 TL"));
2710 }
2711
2712 #[test]
2713 fn test_next_line() {
2714 let mut builder = ContentStreamBuilder::new();
2715 builder.op(ContentStreamOp::NextLine);
2716
2717 let bytes = builder.build().unwrap();
2718 let content = String::from_utf8_lossy(&bytes);
2719 assert!(content.contains("T*"));
2720 }
2721
2722 #[test]
2723 fn test_move_text() {
2724 let mut builder = ContentStreamBuilder::new();
2725 builder.op(ContentStreamOp::MoveText(10.0, -14.0));
2726
2727 let bytes = builder.build().unwrap();
2728 let content = String::from_utf8_lossy(&bytes);
2729 assert!(content.contains("10 -14 Td"));
2730 }
2731
2732 #[test]
2733 fn test_set_text_matrix() {
2734 let mut builder = ContentStreamBuilder::new();
2735 builder.op(ContentStreamOp::SetTextMatrix(1.0, 0.0, 0.0, 1.0, 72.0, 720.0));
2736
2737 let bytes = builder.build().unwrap();
2738 let content = String::from_utf8_lossy(&bytes);
2739 assert!(content.contains("1 0 0 1 72 720 Tm"));
2740 }
2741
2742 #[test]
2743 fn test_show_hex_text() {
2744 let mut builder = ContentStreamBuilder::new();
2745 builder.begin_text();
2746 builder.op(ContentStreamOp::ShowHexText("<0041004200>".to_string()));
2747 builder.end_text();
2748
2749 let bytes = builder.build().unwrap();
2750 let content = String::from_utf8_lossy(&bytes);
2751 assert!(content.contains("<0041004200> Tj"));
2752 }
2753
2754 #[test]
2755 fn test_show_text_array() {
2756 let mut builder = ContentStreamBuilder::new();
2757 builder.begin_text();
2758 builder.op(ContentStreamOp::ShowTextArray(vec![
2759 TextArrayItem::Text("Hello".to_string()),
2760 TextArrayItem::Adjustment(-10.0),
2761 TextArrayItem::Text("World".to_string()),
2762 ]));
2763 builder.end_text();
2764
2765 let bytes = builder.build().unwrap();
2766 let content = String::from_utf8_lossy(&bytes);
2767 assert!(content.contains("[(Hello) -10 (World) ] TJ"));
2768 }
2769
2770 #[test]
2771 fn test_show_text_array_with_hex() {
2772 let mut builder = ContentStreamBuilder::new();
2773 builder.begin_text();
2774 builder.op(ContentStreamOp::ShowTextArray(vec![
2775 TextArrayItem::HexText("<0041>".to_string()),
2776 TextArrayItem::Adjustment(-50.0),
2777 TextArrayItem::HexText("<0042>".to_string()),
2778 ]));
2779 builder.end_text();
2780
2781 let bytes = builder.build().unwrap();
2782 let content = String::from_utf8_lossy(&bytes);
2783 assert!(content.contains("<0041>"));
2784 assert!(content.contains("<0042>"));
2785 assert!(content.contains("TJ"));
2786 }
2787
2788 #[test]
2789 fn test_raw_operator() {
2790 let mut builder = ContentStreamBuilder::new();
2791 builder.op(ContentStreamOp::Raw("% custom comment".to_string()));
2792
2793 let bytes = builder.build().unwrap();
2794 let content = String::from_utf8_lossy(&bytes);
2795 assert!(content.contains("% custom comment"));
2796 }
2797
2798 #[test]
2799 fn test_draw_image() {
2800 let mut builder = ContentStreamBuilder::new();
2801 builder.draw_image("Im1", 100.0, 200.0, 300.0, 400.0);
2802
2803 let bytes = builder.build().unwrap();
2804 let content = String::from_utf8_lossy(&bytes);
2805 assert!(content.contains("q\n"));
2806 assert!(content.contains("300 0 0 400 100 200 cm"));
2807 assert!(content.contains("/Im1 Do"));
2808 assert!(content.contains("Q\n"));
2809 }
2810
2811 #[test]
2812 fn test_hex_text_method() {
2813 let mut builder = ContentStreamBuilder::new();
2814 builder.begin_text();
2815 builder.set_font("F1", 12.0);
2816 builder.hex_text("<00410042>", 72.0, 720.0);
2817 builder.end_text();
2818
2819 let bytes = builder.build().unwrap();
2820 let content = String::from_utf8_lossy(&bytes);
2821 assert!(content.contains("<00410042> Tj"));
2822 }
2823
2824 #[test]
2825 fn test_begin_text_idempotent() {
2826 let mut builder = ContentStreamBuilder::new();
2827 builder.begin_text();
2828 builder.begin_text(); builder.end_text();
2830
2831 let bytes = builder.build().unwrap();
2832 let content = String::from_utf8_lossy(&bytes);
2833 let bt_count = content.matches("BT\n").count();
2834 assert_eq!(bt_count, 1);
2835 }
2836
2837 #[test]
2838 fn test_end_text_idempotent() {
2839 let mut builder = ContentStreamBuilder::new();
2840 builder.end_text(); builder.begin_text();
2842 builder.end_text();
2843 builder.end_text(); let bytes = builder.build().unwrap();
2846 let content = String::from_utf8_lossy(&bytes);
2847 let et_count = content.matches("ET\n").count();
2848 assert_eq!(et_count, 1);
2849 }
2850
2851 #[test]
2852 fn test_set_font_caching() {
2853 let mut builder = ContentStreamBuilder::new();
2854 builder.begin_text();
2855 builder.set_font("Helvetica", 12.0);
2856 builder.set_font("Helvetica", 12.0); builder.set_font("Helvetica", 14.0); builder.end_text();
2859
2860 let bytes = builder.build().unwrap();
2861 let content = String::from_utf8_lossy(&bytes);
2862 let tf_count = content.matches("Tf\n").count();
2864 assert_eq!(tf_count, 2);
2865 }
2866
2867 #[test]
2868 fn test_ops_method() {
2869 let mut builder = ContentStreamBuilder::new();
2870 builder.ops(vec![
2871 ContentStreamOp::SaveState,
2872 ContentStreamOp::SetLineWidth(2.0),
2873 ContentStreamOp::RestoreState,
2874 ]);
2875
2876 let bytes = builder.build().unwrap();
2877 let content = String::from_utf8_lossy(&bytes);
2878 assert!(content.contains("q\n"));
2879 assert!(content.contains("2 w\n"));
2880 assert!(content.contains("Q\n"));
2881 }
2882
2883 #[test]
2884 fn test_add_elements() {
2885 let text1 = TextContent {
2886 artifact_type: None,
2887 text: "First".to_string(),
2888 bbox: Rect::new(72.0, 720.0, 50.0, 12.0),
2889 font: FontSpec::new("Helvetica", 12.0),
2890 style: TextStyle::default(),
2891 reading_order: Some(0),
2892 origin: None,
2893 rotation_degrees: None,
2894 matrix: None,
2895 };
2896 let text2 = TextContent {
2897 artifact_type: None,
2898 text: "Second".to_string(),
2899 bbox: Rect::new(72.0, 700.0, 50.0, 12.0),
2900 font: FontSpec::new("Helvetica", 12.0),
2901 style: TextStyle::default(),
2902 reading_order: Some(1),
2903 origin: None,
2904 rotation_degrees: None,
2905 matrix: None,
2906 };
2907
2908 let mut builder = ContentStreamBuilder::new();
2909 builder.add_elements(&[ContentElement::Text(text1), ContentElement::Text(text2)]);
2910
2911 let bytes = builder.build().unwrap();
2912 let content = String::from_utf8_lossy(&bytes);
2913 assert!(content.contains("(First) Tj"));
2914 assert!(content.contains("(Second) Tj"));
2915 }
2916
2917 #[test]
2918 fn test_escaped_special_chars() {
2919 let mut builder = ContentStreamBuilder::new();
2920 builder
2921 .begin_text()
2922 .set_font("Helvetica", 12.0)
2923 .text("line1\nline2\rtab\there", 72.0, 720.0)
2924 .end_text();
2925
2926 let bytes = builder.build().unwrap();
2927 let content = String::from_utf8_lossy(&bytes);
2928 assert!(content.contains("\\n"));
2929 assert!(content.contains("\\r"));
2930 assert!(content.contains("\\t"));
2931 }
2932
2933 #[test]
2934 fn test_font_mapping_sans_serif() {
2935 let builder = ContentStreamBuilder::new();
2936 assert_eq!(builder.map_font_name("sans-serif", false), "Helvetica");
2937 }
2938
2939 #[test]
2940 fn test_font_mapping_serif() {
2941 let builder = ContentStreamBuilder::new();
2942 assert_eq!(builder.map_font_name("serif", false), "Times-Roman");
2943 assert_eq!(builder.map_font_name("serif", true), "Times-Bold");
2947 }
2948
2949 #[test]
2950 fn test_font_mapping_monospace() {
2951 let builder = ContentStreamBuilder::new();
2952 assert_eq!(builder.map_font_name("monospace", false), "Courier");
2953 assert_eq!(builder.map_font_name("monospace", true), "Courier-Bold");
2954 }
2955
2956 #[test]
2957 fn test_font_mapping_unknown() {
2958 let builder = ContentStreamBuilder::new();
2959 assert_eq!(builder.map_font_name("Unknown Font", false), "Helvetica");
2960 assert_eq!(builder.map_font_name("Unknown Font", true), "Helvetica-Bold");
2961 }
2962
2963 #[test]
2964 fn test_blend_mode_names() {
2965 assert_eq!(BlendMode::Normal.as_pdf_name(), "Normal");
2966 assert_eq!(BlendMode::Multiply.as_pdf_name(), "Multiply");
2967 assert_eq!(BlendMode::Screen.as_pdf_name(), "Screen");
2968 assert_eq!(BlendMode::Overlay.as_pdf_name(), "Overlay");
2969 assert_eq!(BlendMode::Darken.as_pdf_name(), "Darken");
2970 assert_eq!(BlendMode::Lighten.as_pdf_name(), "Lighten");
2971 assert_eq!(BlendMode::ColorDodge.as_pdf_name(), "ColorDodge");
2972 assert_eq!(BlendMode::ColorBurn.as_pdf_name(), "ColorBurn");
2973 assert_eq!(BlendMode::HardLight.as_pdf_name(), "HardLight");
2974 assert_eq!(BlendMode::SoftLight.as_pdf_name(), "SoftLight");
2975 assert_eq!(BlendMode::Difference.as_pdf_name(), "Difference");
2976 assert_eq!(BlendMode::Exclusion.as_pdf_name(), "Exclusion");
2977 }
2978
2979 #[test]
2980 fn test_blend_mode_default() {
2981 let mode = BlendMode::default();
2982 assert_eq!(mode.as_pdf_name(), "Normal");
2983 }
2984
2985 #[test]
2986 fn test_line_cap_default() {
2987 let cap = LineCap::default();
2988 assert_eq!(cap as u8, 0);
2989 }
2990
2991 #[test]
2992 fn test_line_join_default() {
2993 let join = LineJoin::default();
2994 assert_eq!(join as u8, 0);
2995 }
2996
2997 #[test]
2998 fn test_path_content_stroke_and_fill() {
2999 use crate::elements::PathContent;
3000
3001 let path = PathContent {
3002 operations: vec![
3003 PathOperation::MoveTo(0.0, 0.0),
3004 PathOperation::LineTo(100.0, 0.0),
3005 PathOperation::LineTo(100.0, 100.0),
3006 PathOperation::ClosePath,
3007 ],
3008 stroke_color: Some(Color::black()),
3009 fill_color: Some(Color {
3010 r: 1.0,
3011 g: 0.0,
3012 b: 0.0,
3013 }),
3014 stroke_width: 2.0,
3015 bbox: Rect::new(0.0, 0.0, 100.0, 100.0),
3016 line_cap: Default::default(),
3017 line_join: Default::default(),
3018 dash_pattern: None,
3019 matrix: None,
3020 reading_order: None,
3021 artifact_type: None,
3022 layer: None,
3023 };
3024
3025 let mut builder = ContentStreamBuilder::new();
3026 builder.add_element(&ContentElement::Path(path));
3027
3028 let bytes = builder.build().unwrap();
3029 let content = String::from_utf8_lossy(&bytes);
3030 assert!(content.contains("B\n")); }
3032
3033 #[test]
3034 fn test_path_content_stroke_only() {
3035 use crate::elements::PathContent;
3036
3037 let path = PathContent {
3038 operations: vec![
3039 PathOperation::MoveTo(0.0, 0.0),
3040 PathOperation::LineTo(100.0, 100.0),
3041 ],
3042 stroke_color: Some(Color::black()),
3043 fill_color: None,
3044 stroke_width: 1.0,
3045 bbox: Rect::new(0.0, 0.0, 100.0, 100.0),
3046 line_cap: Default::default(),
3047 line_join: Default::default(),
3048 dash_pattern: None,
3049 matrix: None,
3050 reading_order: None,
3051 artifact_type: None,
3052 layer: None,
3053 };
3054
3055 let mut builder = ContentStreamBuilder::new();
3056 builder.add_element(&ContentElement::Path(path));
3057
3058 let bytes = builder.build().unwrap();
3059 let content = String::from_utf8_lossy(&bytes);
3060 assert!(content.contains("S\n")); }
3062
3063 #[test]
3064 fn test_path_content_fill_only() {
3065 use crate::elements::PathContent;
3066
3067 let path = PathContent {
3068 operations: vec![PathOperation::Rectangle(0.0, 0.0, 100.0, 100.0)],
3069 stroke_color: None,
3070 fill_color: Some(Color {
3071 r: 0.0,
3072 g: 0.0,
3073 b: 1.0,
3074 }),
3075 stroke_width: 0.0,
3076 bbox: Rect::new(0.0, 0.0, 100.0, 100.0),
3077 line_cap: Default::default(),
3078 line_join: Default::default(),
3079 dash_pattern: None,
3080 matrix: None,
3081 reading_order: None,
3082 artifact_type: None,
3083 layer: None,
3084 };
3085
3086 let mut builder = ContentStreamBuilder::new();
3087 builder.add_element(&ContentElement::Path(path));
3088
3089 let bytes = builder.build().unwrap();
3090 let content = String::from_utf8_lossy(&bytes);
3091 assert!(content.contains("f\n")); }
3093
3094 #[test]
3095 fn test_path_content_no_stroke_no_fill() {
3096 use crate::elements::PathContent;
3097
3098 let path = PathContent {
3099 operations: vec![
3100 PathOperation::MoveTo(0.0, 0.0),
3101 PathOperation::CurveTo(10.0, 20.0, 30.0, 40.0, 50.0, 60.0),
3102 ],
3103 stroke_color: None,
3104 fill_color: None,
3105 stroke_width: 0.0,
3106 bbox: Rect::new(0.0, 0.0, 50.0, 60.0),
3107 line_cap: Default::default(),
3108 line_join: Default::default(),
3109 dash_pattern: None,
3110 matrix: None,
3111 reading_order: None,
3112 artifact_type: None,
3113 layer: None,
3114 };
3115
3116 let mut builder = ContentStreamBuilder::new();
3117 builder.add_element(&ContentElement::Path(path));
3118
3119 let bytes = builder.build().unwrap();
3120 let content = String::from_utf8_lossy(&bytes);
3121 assert!(content.contains("n\n")); }
3123
3124 #[test]
3125 fn test_empty_build() {
3126 let builder = ContentStreamBuilder::new();
3127 let bytes = builder.build().unwrap();
3128 assert!(bytes.is_empty());
3129 }
3130}