1use crate::error::Result;
7use crate::graphics::Color;
8use crate::objects::{Dictionary, Object, Stream};
9
10#[derive(Debug, Clone)]
12pub struct AppearanceCharacteristics {
13 pub rotation: i32,
15 pub border_color: Option<Color>,
17 pub background_color: Option<Color>,
19 pub normal_caption: Option<String>,
21 pub rollover_caption: Option<String>,
23 pub down_caption: Option<String>,
25 pub normal_icon: Option<Stream>,
27 pub rollover_icon: Option<Stream>,
29 pub down_icon: Option<Stream>,
31 pub icon_fit: Option<IconFit>,
33 pub text_position: TextPosition,
35}
36
37impl Default for AppearanceCharacteristics {
38 fn default() -> Self {
39 Self {
40 rotation: 0,
41 border_color: None,
42 background_color: None,
43 normal_caption: None,
44 rollover_caption: None,
45 down_caption: None,
46 normal_icon: None,
47 rollover_icon: None,
48 down_icon: None,
49 icon_fit: None,
50 text_position: TextPosition::CaptionOnly,
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct IconFit {
58 pub scale_type: IconScaleType,
60 pub scale_when: IconScaleWhen,
62 pub align_x: f64,
64 pub align_y: f64,
66 pub fit_bounds: bool,
68}
69
70#[derive(Debug, Clone, PartialEq)]
72pub enum IconScaleType {
73 Always,
75 Bigger,
77 Smaller,
79 Never,
81}
82
83#[derive(Debug, Clone, PartialEq)]
85pub enum IconScaleWhen {
86 Always,
88 IconBigger,
90 IconSmaller,
92 Never,
94}
95
96#[derive(Debug, Clone, PartialEq)]
98pub enum TextPosition {
99 CaptionOnly,
101 IconOnly,
103 CaptionBelowIcon,
105 CaptionAboveIcon,
107 CaptionRightIcon,
109 CaptionLeftIcon,
111 CaptionOverlayIcon,
113}
114
115impl AppearanceCharacteristics {
116 pub fn to_dict(&self) -> Dictionary {
118 let mut dict = Dictionary::new();
119
120 if self.rotation != 0 {
121 dict.set("R", Object::Integer(self.rotation as i64));
122 }
123
124 if let Some(color) = &self.border_color {
125 dict.set("BC", color.to_array());
126 }
127
128 if let Some(color) = &self.background_color {
129 dict.set("BG", color.to_array());
130 }
131
132 if let Some(caption) = &self.normal_caption {
133 dict.set("CA", Object::String(caption.clone()));
134 }
135
136 if let Some(caption) = &self.rollover_caption {
137 dict.set("RC", Object::String(caption.clone()));
138 }
139
140 if let Some(caption) = &self.down_caption {
141 dict.set("AC", Object::String(caption.clone()));
142 }
143
144 if let Some(fit) = &self.icon_fit {
145 dict.set("IF", fit.to_dict());
146 }
147
148 dict.set("TP", Object::Integer(self.text_position.to_int()));
149
150 dict
151 }
152}
153
154impl IconFit {
155 pub fn to_dict(&self) -> Object {
157 let mut dict = Dictionary::new();
158
159 dict.set(
160 "SW",
161 Object::Name(
162 match self.scale_when {
163 IconScaleWhen::Always => "A",
164 IconScaleWhen::IconBigger => "B",
165 IconScaleWhen::IconSmaller => "S",
166 IconScaleWhen::Never => "N",
167 }
168 .to_string(),
169 ),
170 );
171
172 dict.set(
173 "S",
174 Object::Name(
175 match self.scale_type {
176 IconScaleType::Always => "A",
177 IconScaleType::Bigger => "B",
178 IconScaleType::Smaller => "S",
179 IconScaleType::Never => "N",
180 }
181 .to_string(),
182 ),
183 );
184
185 dict.set(
186 "A",
187 Object::Array(vec![Object::Real(self.align_x), Object::Real(self.align_y)]),
188 );
189
190 if self.fit_bounds {
191 dict.set("FB", Object::Boolean(true));
192 }
193
194 Object::Dictionary(dict)
195 }
196}
197
198impl TextPosition {
199 pub fn to_int(&self) -> i64 {
200 match self {
201 TextPosition::CaptionOnly => 0,
202 TextPosition::IconOnly => 1,
203 TextPosition::CaptionBelowIcon => 2,
204 TextPosition::CaptionAboveIcon => 3,
205 TextPosition::CaptionRightIcon => 4,
206 TextPosition::CaptionLeftIcon => 5,
207 TextPosition::CaptionOverlayIcon => 6,
208 }
209 }
210}
211
212pub struct FieldAppearanceGenerator {
214 pub value: String,
216 pub font: String,
218 pub font_size: f64,
220 pub text_color: Color,
222 pub background_color: Option<Color>,
224 pub border_color: Option<Color>,
226 pub border_width: f64,
228 pub rect: [f64; 4],
230 pub alignment: TextAlignment,
232 pub multiline: bool,
234 pub max_length: Option<usize>,
236 pub comb: bool,
238}
239
240#[derive(Debug, Clone, Copy, PartialEq)]
242pub enum TextAlignment {
243 Left,
244 Center,
245 Right,
246}
247
248impl FieldAppearanceGenerator {
249 pub fn generate_text_field(&self) -> Result<Stream> {
251 let mut ops = Vec::new();
252 let width = self.rect[2] - self.rect[0];
253 let height = self.rect[3] - self.rect[1];
254
255 ops.push("q".to_string());
257
258 if let Some(bg_color) = &self.background_color {
260 ops.push(crate::graphics::color::fill_color_op(*bg_color));
261 ops.push(format!("0 0 {} {} re", width, height));
262 ops.push("f".to_string());
263 }
264
265 if let Some(border_color) = &self.border_color {
267 if self.border_width > 0.0 {
268 ops.push(format!("{} w", self.border_width));
269 ops.push(crate::graphics::color::stroke_color_op(*border_color));
270 ops.push(format!(
271 "{} {} {} {} re",
272 self.border_width / 2.0,
273 self.border_width / 2.0,
274 width - self.border_width,
275 height - self.border_width
276 ));
277 ops.push("S".to_string());
278 }
279 }
280
281 ops.push("BT".to_string());
283
284 ops.push(format!("/{} {} Tf", self.font, self.font_size));
286
287 ops.push(crate::graphics::color::fill_color_op(self.text_color));
289
290 let padding = 2.0;
292 let text_y = height / 2.0 - self.font_size / 2.0;
293
294 if self.comb {
295 if let Some(max_len) = self.max_length {
296 let char_width = (width - 2.0 * padding) / max_len as f64;
298
299 for (i, ch) in self.value.chars().take(max_len).enumerate() {
300 let x = padding + (i as f64 + 0.5) * char_width;
301 ops.push(format!("{} {} Td", x, text_y));
302 ops.push(format!("({}) Tj", escape_string(&ch.to_string())));
303 if i < self.value.len() - 1 {
304 ops.push(format!("{} 0 Td", -x));
305 }
306 }
307 }
308 } else if self.multiline {
309 let lines = self.value.lines();
311 let line_height = self.font_size * 1.2;
312 let mut y = height - padding - self.font_size;
313
314 for line in lines {
315 let x = match self.alignment {
316 TextAlignment::Left => padding,
317 TextAlignment::Center => width / 2.0,
318 TextAlignment::Right => width - padding,
319 };
320
321 ops.push(format!("{} {} Td", x, y));
322 ops.push(format!("({}) Tj", escape_string(line)));
323
324 y -= line_height;
325 if y < padding {
326 break;
327 }
328 }
329 } else {
330 let x = match self.alignment {
332 TextAlignment::Left => padding,
333 TextAlignment::Center => width / 2.0,
334 TextAlignment::Right => width - padding,
335 };
336
337 ops.push(format!("{} {} Td", x, text_y));
338 ops.push(format!("({}) Tj", escape_string(&self.value)));
339 }
340
341 ops.push("ET".to_string());
343
344 ops.push("Q".to_string());
346
347 let content = ops.join("\n");
348
349 let mut stream = Stream::new(content.into_bytes());
350 stream
351 .dictionary_mut()
352 .set("Type", Object::Name("XObject".to_string()));
353 stream
354 .dictionary_mut()
355 .set("Subtype", Object::Name("Form".to_string()));
356 stream.dictionary_mut().set(
357 "BBox",
358 Object::Array(vec![
359 Object::Real(0.0),
360 Object::Real(0.0),
361 Object::Real(width),
362 Object::Real(height),
363 ]),
364 );
365
366 Ok(stream)
367 }
368}
369
370pub struct ButtonAppearanceGenerator {
372 pub style: ButtonStyle,
374 pub size: f64,
376 pub border_color: Color,
378 pub background_color: Color,
380 pub check_color: Color,
382 pub border_width: f64,
384}
385
386#[derive(Debug, Clone, Copy, PartialEq)]
388pub enum ButtonStyle {
389 Check,
391 Cross,
393 Diamond,
395 Circle,
397 Star,
399 Square,
401 Radio,
403}
404
405impl ButtonAppearanceGenerator {
406 pub fn generate_checked(&self) -> Result<Stream> {
408 let mut ops = Vec::new();
409
410 ops.push("q".to_string());
412
413 ops.push(crate::graphics::color::fill_color_op(self.background_color));
415
416 match self.style {
417 ButtonStyle::Radio => {
418 self.draw_circle(&mut ops, self.size / 2.0, self.size / 2.0, self.size / 2.0);
420 ops.push("f".to_string());
421
422 if self.border_width > 0.0 {
424 ops.push(format!("{} w", self.border_width));
425 ops.push(crate::graphics::color::stroke_color_op(self.border_color));
426 ops.push("s".to_string());
427 }
428
429 let dot_size = self.size * 0.3;
431 ops.push(crate::graphics::color::fill_color_op(self.check_color));
432 self.draw_circle(&mut ops, self.size / 2.0, self.size / 2.0, dot_size);
433 ops.push("f".to_string());
434 }
435 _ => {
436 ops.push(format!("0 0 {} {} re", self.size, self.size));
438 ops.push("f".to_string());
439
440 if self.border_width > 0.0 {
442 ops.push(format!("{} w", self.border_width));
443 ops.push(crate::graphics::color::stroke_color_op(self.border_color));
444 ops.push(format!(
445 "{} {} {} {} re",
446 self.border_width / 2.0,
447 self.border_width / 2.0,
448 self.size - self.border_width,
449 self.size - self.border_width
450 ));
451 ops.push("S".to_string());
452 }
453
454 ops.push(crate::graphics::color::fill_color_op(self.check_color));
456
457 self.draw_check_style(&mut ops);
458 }
459 }
460
461 ops.push("Q".to_string());
463
464 let content = ops.join("\n");
465
466 let mut stream = Stream::new(content.into_bytes());
467 stream
468 .dictionary_mut()
469 .set("Type", Object::Name("XObject".to_string()));
470 stream
471 .dictionary_mut()
472 .set("Subtype", Object::Name("Form".to_string()));
473 stream.dictionary_mut().set(
474 "BBox",
475 Object::Array(vec![
476 Object::Real(0.0),
477 Object::Real(0.0),
478 Object::Real(self.size),
479 Object::Real(self.size),
480 ]),
481 );
482
483 Ok(stream)
484 }
485
486 pub fn generate_unchecked(&self) -> Result<Stream> {
488 let mut ops = Vec::new();
489
490 ops.push("q".to_string());
492
493 ops.push(crate::graphics::color::fill_color_op(self.background_color));
495
496 if self.style == ButtonStyle::Radio {
497 self.draw_circle(&mut ops, self.size / 2.0, self.size / 2.0, self.size / 2.0);
499 ops.push("f".to_string());
500
501 if self.border_width > 0.0 {
503 ops.push(format!("{} w", self.border_width));
504 ops.push(crate::graphics::color::stroke_color_op(self.border_color));
505 ops.push("s".to_string());
506 }
507 } else {
508 ops.push(format!("0 0 {} {} re", self.size, self.size));
510 ops.push("f".to_string());
511
512 if self.border_width > 0.0 {
514 ops.push(format!("{} w", self.border_width));
515 ops.push(crate::graphics::color::stroke_color_op(self.border_color));
516 ops.push(format!(
517 "{} {} {} {} re",
518 self.border_width / 2.0,
519 self.border_width / 2.0,
520 self.size - self.border_width,
521 self.size - self.border_width
522 ));
523 ops.push("S".to_string());
524 }
525 }
526
527 ops.push("Q".to_string());
529
530 let content = ops.join("\n");
531
532 let mut stream = Stream::new(content.into_bytes());
533 stream
534 .dictionary_mut()
535 .set("Type", Object::Name("XObject".to_string()));
536 stream
537 .dictionary_mut()
538 .set("Subtype", Object::Name("Form".to_string()));
539 stream.dictionary_mut().set(
540 "BBox",
541 Object::Array(vec![
542 Object::Real(0.0),
543 Object::Real(0.0),
544 Object::Real(self.size),
545 Object::Real(self.size),
546 ]),
547 );
548
549 Ok(stream)
550 }
551
552 fn draw_circle(&self, ops: &mut Vec<String>, cx: f64, cy: f64, r: f64) {
553 let k = 0.552284749831; ops.push(format!("{} {} m", cx + r, cy));
557 ops.push(format!(
558 "{} {} {} {} {} {} c",
559 cx + r,
560 cy + r * k,
561 cx + r * k,
562 cy + r,
563 cx,
564 cy + r
565 ));
566 ops.push(format!(
567 "{} {} {} {} {} {} c",
568 cx - r * k,
569 cy + r,
570 cx - r,
571 cy + r * k,
572 cx - r,
573 cy
574 ));
575 ops.push(format!(
576 "{} {} {} {} {} {} c",
577 cx - r,
578 cy - r * k,
579 cx - r * k,
580 cy - r,
581 cx,
582 cy - r
583 ));
584 ops.push(format!(
585 "{} {} {} {} {} {} c",
586 cx + r * k,
587 cy - r,
588 cx + r,
589 cy - r * k,
590 cx + r,
591 cy
592 ));
593 }
594
595 fn draw_check_style(&self, ops: &mut Vec<String>) {
596 match self.style {
597 ButtonStyle::Check => {
598 ops.push(format!("{} w", self.size * 0.1));
600 ops.push(format!("{} {} m", self.size * 0.2, self.size * 0.5));
601 ops.push(format!("{} {} l", self.size * 0.4, self.size * 0.3));
602 ops.push(format!("{} {} l", self.size * 0.8, self.size * 0.7));
603 ops.push("S".to_string());
604 }
605 ButtonStyle::Cross => {
606 ops.push(format!("{} w", self.size * 0.1));
608 ops.push(format!("{} {} m", self.size * 0.2, self.size * 0.2));
609 ops.push(format!("{} {} l", self.size * 0.8, self.size * 0.8));
610 ops.push(format!("{} {} m", self.size * 0.2, self.size * 0.8));
611 ops.push(format!("{} {} l", self.size * 0.8, self.size * 0.2));
612 ops.push("S".to_string());
613 }
614 ButtonStyle::Diamond => {
615 ops.push(format!("{} {} m", self.size * 0.5, self.size * 0.8));
617 ops.push(format!("{} {} l", self.size * 0.8, self.size * 0.5));
618 ops.push(format!("{} {} l", self.size * 0.5, self.size * 0.2));
619 ops.push(format!("{} {} l", self.size * 0.2, self.size * 0.5));
620 ops.push("f".to_string());
621 }
622 ButtonStyle::Circle => {
623 self.draw_circle(ops, self.size / 2.0, self.size / 2.0, self.size * 0.3);
625 ops.push("f".to_string());
626 }
627 ButtonStyle::Star => {
628 let cx = self.size / 2.0;
630 let cy = self.size / 2.0;
631 let r = self.size * 0.4;
632
633 ops.push(format!("{} {} m", cx, cy + r));
634 for i in 1..10 {
635 let angle = i as f64 * 36.0 * std::f64::consts::PI / 180.0;
636 let radius = if i % 2 == 0 { r } else { r * 0.5 };
637 let x = cx + radius * angle.sin();
638 let y = cy + radius * angle.cos();
639 ops.push(format!("{} {} l", x, y));
640 }
641 ops.push("f".to_string());
642 }
643 ButtonStyle::Square => {
644 let inset = self.size * 0.25;
646 ops.push(format!(
647 "{} {} {} {} re",
648 inset,
649 inset,
650 self.size - 2.0 * inset,
651 self.size - 2.0 * inset
652 ));
653 ops.push("f".to_string());
654 }
655 _ => {}
656 }
657 }
658}
659
660fn escape_string(s: &str) -> String {
662 s.chars()
663 .map(|c| match c {
664 '(' => "\\(".to_string(),
665 ')' => "\\)".to_string(),
666 '\\' => "\\\\".to_string(),
667 '\n' => "\\n".to_string(),
668 '\r' => "\\r".to_string(),
669 '\t' => "\\t".to_string(),
670 c => c.to_string(),
671 })
672 .collect()
673}
674
675pub struct PushButtonAppearanceGenerator {
677 pub caption: String,
679 pub font: String,
681 pub font_size: f64,
683 pub text_color: Color,
685 pub background_color: Color,
687 pub border_color: Color,
689 pub border_width: f64,
691 pub size: [f64; 2],
693 pub border_style: ButtonBorderStyle,
695}
696
697#[derive(Debug, Clone, Copy, PartialEq)]
699pub enum ButtonBorderStyle {
700 Solid,
702 Dashed,
704 Beveled,
706 Inset,
708 Underline,
710}
711
712impl PushButtonAppearanceGenerator {
713 pub fn generate_normal(&self) -> Result<Stream> {
715 self.generate_appearance(false)
716 }
717
718 pub fn generate_rollover(&self) -> Result<Stream> {
720 let mut appearance = self.clone();
722 appearance.background_color = appearance.background_color.lighten(0.1);
723 appearance.generate_appearance(false)
724 }
725
726 pub fn generate_down(&self) -> Result<Stream> {
728 self.generate_appearance(true)
729 }
730
731 fn generate_appearance(&self, pressed: bool) -> Result<Stream> {
732 let mut ops = Vec::new();
733 let [width, height] = self.size;
734
735 ops.push("q".to_string());
737
738 let bg_color = if pressed {
740 self.background_color.darken(0.1)
741 } else {
742 self.background_color
743 };
744
745 ops.push(crate::graphics::color::fill_color_op(bg_color));
746 ops.push(format!("0 0 {} {} re", width, height));
747 ops.push("f".to_string());
748
749 self.draw_border(&mut ops, width, height, pressed);
751
752 if !self.caption.is_empty() {
754 ops.push("BT".to_string());
755 ops.push(format!("/{} {} Tf", self.font, self.font_size));
756 ops.push(crate::graphics::color::fill_color_op(self.text_color));
757
758 let text_x = width / 2.0;
760 let text_y = height / 2.0 - self.font_size / 2.0;
761
762 ops.push(format!("{} {} Td", text_x, text_y));
763 ops.push(format!("({}) Tj", escape_string(&self.caption)));
764 ops.push("ET".to_string());
765 }
766
767 ops.push("Q".to_string());
769
770 let content = ops.join("\n");
771
772 let mut stream = Stream::new(content.into_bytes());
773 stream
774 .dictionary_mut()
775 .set("Type", Object::Name("XObject".to_string()));
776 stream
777 .dictionary_mut()
778 .set("Subtype", Object::Name("Form".to_string()));
779 stream.dictionary_mut().set(
780 "BBox",
781 Object::Array(vec![
782 Object::Real(0.0),
783 Object::Real(0.0),
784 Object::Real(width),
785 Object::Real(height),
786 ]),
787 );
788
789 Ok(stream)
790 }
791
792 fn draw_border(&self, ops: &mut Vec<String>, width: f64, height: f64, pressed: bool) {
793 match self.border_style {
794 ButtonBorderStyle::Solid => {
795 if self.border_width > 0.0 {
796 ops.push(format!("{} w", self.border_width));
797 ops.push(crate::graphics::color::stroke_color_op(self.border_color));
798 ops.push(format!(
799 "{} {} {} {} re",
800 self.border_width / 2.0,
801 self.border_width / 2.0,
802 width - self.border_width,
803 height - self.border_width
804 ));
805 ops.push("S".to_string());
806 }
807 }
808 ButtonBorderStyle::Dashed => {
809 if self.border_width > 0.0 {
810 ops.push(format!("{} w", self.border_width));
811 ops.push("[3 3] 0 d".to_string()); ops.push(crate::graphics::color::stroke_color_op(self.border_color));
813 ops.push(format!(
814 "{} {} {} {} re",
815 self.border_width / 2.0,
816 self.border_width / 2.0,
817 width - self.border_width,
818 height - self.border_width
819 ));
820 ops.push("S".to_string());
821 }
822 }
823 ButtonBorderStyle::Beveled | ButtonBorderStyle::Inset => {
824 let is_inset = self.border_style == ButtonBorderStyle::Inset || pressed;
825 let light_color = if is_inset {
826 self.border_color.darken(0.3)
827 } else {
828 self.border_color.lighten(0.3)
829 };
830 let dark_color = if is_inset {
831 self.border_color.lighten(0.3)
832 } else {
833 self.border_color.darken(0.3)
834 };
835
836 ops.push(format!("{} w", self.border_width));
838 ops.push(crate::graphics::color::stroke_color_op(light_color));
839 ops.push(format!("{} {} m", 0.0, 0.0));
840 ops.push(format!("{} {} l", 0.0, height));
841 ops.push(format!("{} {} l", width, height));
842 ops.push("S".to_string());
843
844 ops.push(crate::graphics::color::stroke_color_op(dark_color));
846 ops.push(format!("{} {} m", width, height));
847 ops.push(format!("{} {} l", width, 0.0));
848 ops.push(format!("{} {} l", 0.0, 0.0));
849 ops.push("S".to_string());
850 }
851 ButtonBorderStyle::Underline => {
852 if self.border_width > 0.0 {
853 ops.push(format!("{} w", self.border_width));
854 ops.push(crate::graphics::color::stroke_color_op(self.border_color));
855 ops.push(format!("{} {} m", 0.0, self.border_width / 2.0));
856 ops.push(format!("{} {} l", width, self.border_width / 2.0));
857 ops.push("S".to_string());
858 }
859 }
860 }
861 }
862}
863
864impl Clone for PushButtonAppearanceGenerator {
865 fn clone(&self) -> Self {
866 Self {
867 caption: self.caption.clone(),
868 font: self.font.clone(),
869 font_size: self.font_size,
870 text_color: self.text_color,
871 background_color: self.background_color,
872 border_color: self.border_color,
873 border_width: self.border_width,
874 size: self.size,
875 border_style: self.border_style,
876 }
877 }
878}
879
880impl Color {
881 pub fn lighten(&self, amount: f64) -> Color {
882 Color::rgb(
883 (self.r() + amount).min(1.0),
884 (self.g() + amount).min(1.0),
885 (self.b() + amount).min(1.0),
886 )
887 }
888
889 pub fn darken(&self, amount: f64) -> Color {
890 Color::rgb(
891 (self.r() - amount).max(0.0),
892 (self.g() - amount).max(0.0),
893 (self.b() - amount).max(0.0),
894 )
895 }
896
897 pub fn to_array(&self) -> Object {
898 Object::Array(vec![
899 Object::Real(self.r()),
900 Object::Real(self.g()),
901 Object::Real(self.b()),
902 ])
903 }
904}