1use crate::error::Result;
7use crate::forms::{BorderStyle, FieldType, Widget};
8use crate::graphics::Color;
9use crate::objects::{Dictionary, Object, Stream};
10use crate::text::Font;
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum AppearanceState {
16 Normal,
18 Rollover,
20 Down,
22}
23
24impl AppearanceState {
25 pub fn pdf_name(&self) -> &'static str {
27 match self {
28 AppearanceState::Normal => "N",
29 AppearanceState::Rollover => "R",
30 AppearanceState::Down => "D",
31 }
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct AppearanceStream {
38 pub content: Vec<u8>,
40 pub resources: Dictionary,
42 pub bbox: [f64; 4],
44}
45
46impl AppearanceStream {
47 pub fn new(content: Vec<u8>, bbox: [f64; 4]) -> Self {
49 Self {
50 content,
51 resources: Dictionary::new(),
52 bbox,
53 }
54 }
55
56 pub fn with_resources(mut self, resources: Dictionary) -> Self {
58 self.resources = resources;
59 self
60 }
61
62 pub fn to_stream(&self) -> Stream {
64 let mut dict = Dictionary::new();
65 dict.set("Type", Object::Name("XObject".to_string()));
66 dict.set("Subtype", Object::Name("Form".to_string()));
67
68 let bbox_array = vec![
70 Object::Real(self.bbox[0]),
71 Object::Real(self.bbox[1]),
72 Object::Real(self.bbox[2]),
73 Object::Real(self.bbox[3]),
74 ];
75 dict.set("BBox", Object::Array(bbox_array));
76
77 if !self.resources.is_empty() {
79 dict.set("Resources", Object::Dictionary(self.resources.clone()));
80 }
81
82 Stream::with_dictionary(dict, self.content.clone())
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct AppearanceDictionary {
90 appearances: HashMap<AppearanceState, AppearanceStream>,
92 down_appearances: HashMap<String, AppearanceStream>,
94}
95
96impl AppearanceDictionary {
97 pub fn new() -> Self {
99 Self {
100 appearances: HashMap::new(),
101 down_appearances: HashMap::new(),
102 }
103 }
104
105 pub fn set_appearance(&mut self, state: AppearanceState, stream: AppearanceStream) {
107 self.appearances.insert(state, stream);
108 }
109
110 pub fn set_down_appearance(&mut self, value: String, stream: AppearanceStream) {
112 self.down_appearances.insert(value, stream);
113 }
114
115 pub fn get_appearance(&self, state: AppearanceState) -> Option<&AppearanceStream> {
117 self.appearances.get(&state)
118 }
119
120 pub fn to_dict(&self) -> Dictionary {
122 let mut dict = Dictionary::new();
123
124 for (state, stream) in &self.appearances {
126 let stream_obj = stream.to_stream();
127 dict.set(
128 state.pdf_name(),
129 Object::Stream(stream_obj.dictionary().clone(), stream_obj.data().to_vec()),
130 );
131 }
132
133 if !self.down_appearances.is_empty() {
135 let mut down_dict = Dictionary::new();
136 for (value, stream) in &self.down_appearances {
137 let stream_obj = stream.to_stream();
138 down_dict.set(
139 value,
140 Object::Stream(stream_obj.dictionary().clone(), stream_obj.data().to_vec()),
141 );
142 }
143 dict.set("D", Object::Dictionary(down_dict));
144 }
145
146 dict
147 }
148}
149
150impl Default for AppearanceDictionary {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156pub trait AppearanceGenerator {
158 fn generate_appearance(
160 &self,
161 widget: &Widget,
162 value: Option<&str>,
163 state: AppearanceState,
164 ) -> Result<AppearanceStream>;
165}
166
167pub struct TextFieldAppearance {
169 pub font: Font,
171 pub font_size: f64,
173 pub text_color: Color,
175 pub justification: i32,
177 pub multiline: bool,
179}
180
181impl Default for TextFieldAppearance {
182 fn default() -> Self {
183 Self {
184 font: Font::Helvetica,
185 font_size: 12.0,
186 text_color: Color::black(),
187 justification: 0,
188 multiline: false,
189 }
190 }
191}
192
193impl AppearanceGenerator for TextFieldAppearance {
194 fn generate_appearance(
195 &self,
196 widget: &Widget,
197 value: Option<&str>,
198 _state: AppearanceState,
199 ) -> Result<AppearanceStream> {
200 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
201 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
202
203 let mut content = String::new();
204
205 content.push_str("q\n");
207
208 if let Some(bg_color) = &widget.appearance.background_color {
210 match bg_color {
211 Color::Gray(g) => content.push_str(&format!("{g} g\n")),
212 Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
213 Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
214 }
215 content.push_str(&format!("0 0 {width} {height} re f\n"));
216 }
217
218 if let Some(border_color) = &widget.appearance.border_color {
220 match border_color {
221 Color::Gray(g) => content.push_str(&format!("{g} G\n")),
222 Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} RG\n")),
223 Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} K\n")),
224 }
225 content.push_str(&format!("{} w\n", widget.appearance.border_width));
226
227 match widget.appearance.border_style {
228 BorderStyle::Solid => {
229 content.push_str(&format!("0 0 {width} {height} re S\n"));
230 }
231 BorderStyle::Dashed => {
232 content.push_str("[3 2] 0 d\n");
233 content.push_str(&format!("0 0 {width} {height} re S\n"));
234 }
235 BorderStyle::Beveled | BorderStyle::Inset => {
236 content.push_str(&format!("0 0 {width} {height} re S\n"));
238 }
239 BorderStyle::Underline => {
240 content.push_str(&format!("0 0 m {width} 0 l S\n"));
241 }
242 }
243 }
244
245 if let Some(text) = value {
247 match self.text_color {
249 Color::Gray(g) => content.push_str(&format!("{g} g\n")),
250 Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
251 Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
252 }
253
254 content.push_str("BT\n");
256 content.push_str(&format!(
257 "/{} {} Tf\n",
258 self.font.pdf_name(),
259 self.font_size
260 ));
261
262 let padding = 2.0;
264 let text_y = (height - self.font_size) / 2.0 + self.font_size * 0.3;
265
266 let text_x = match self.justification {
267 1 => width / 2.0, 2 => width - padding, _ => padding, };
271
272 content.push_str(&format!("{text_x} {text_y} Td\n"));
273
274 let escaped_text = text
276 .replace('\\', "\\\\")
277 .replace('(', "\\(")
278 .replace(')', "\\)");
279 content.push_str(&format!("({escaped_text}) Tj\n"));
280
281 content.push_str("ET\n");
283 }
284
285 content.push_str("Q\n");
287
288 let mut resources = Dictionary::new();
290
291 let mut font_dict = Dictionary::new();
293 let mut font_res = Dictionary::new();
294 font_res.set("Type", Object::Name("Font".to_string()));
295 font_res.set("Subtype", Object::Name("Type1".to_string()));
296 font_res.set("BaseFont", Object::Name(self.font.pdf_name()));
297 font_dict.set(self.font.pdf_name(), Object::Dictionary(font_res));
298 resources.set("Font", Object::Dictionary(font_dict));
299
300 let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height])
301 .with_resources(resources);
302
303 Ok(stream)
304 }
305}
306
307pub struct CheckBoxAppearance {
309 pub check_style: CheckStyle,
311 pub check_color: Color,
313}
314
315#[derive(Debug, Clone, Copy)]
317pub enum CheckStyle {
318 Check,
320 Cross,
322 Square,
324 Circle,
326 Star,
328}
329
330impl Default for CheckBoxAppearance {
331 fn default() -> Self {
332 Self {
333 check_style: CheckStyle::Check,
334 check_color: Color::black(),
335 }
336 }
337}
338
339impl AppearanceGenerator for CheckBoxAppearance {
340 fn generate_appearance(
341 &self,
342 widget: &Widget,
343 value: Option<&str>,
344 _state: AppearanceState,
345 ) -> Result<AppearanceStream> {
346 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
347 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
348 let is_checked = value.is_some_and(|v| v == "Yes" || v == "On" || v == "true");
349
350 let mut content = String::new();
351
352 content.push_str("q\n");
354
355 if let Some(bg_color) = &widget.appearance.background_color {
357 match bg_color {
358 Color::Gray(g) => content.push_str(&format!("{g} g\n")),
359 Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
360 Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
361 }
362 content.push_str(&format!("0 0 {width} {height} re f\n"));
363 }
364
365 if let Some(border_color) = &widget.appearance.border_color {
367 match border_color {
368 Color::Gray(g) => content.push_str(&format!("{g} G\n")),
369 Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} RG\n")),
370 Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} K\n")),
371 }
372 content.push_str(&format!("{} w\n", widget.appearance.border_width));
373 content.push_str(&format!("0 0 {width} {height} re S\n"));
374 }
375
376 if is_checked {
378 match self.check_color {
380 Color::Gray(g) => content.push_str(&format!("{g} g\n")),
381 Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
382 Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
383 }
384
385 let inset = width * 0.2;
386
387 match self.check_style {
388 CheckStyle::Check => {
389 content.push_str(&format!("{} {} m\n", inset, height * 0.5));
391 content.push_str(&format!("{} {} l\n", width * 0.4, inset));
392 content.push_str(&format!("{} {} l\n", width - inset, height - inset));
393 content.push_str("3 w S\n");
394 }
395 CheckStyle::Cross => {
396 content.push_str(&format!("{inset} {inset} m\n"));
398 content.push_str(&format!("{} {} l\n", width - inset, height - inset));
399 content.push_str(&format!("{} {inset} m\n", width - inset));
400 content.push_str(&format!("{inset} {} l\n", height - inset));
401 content.push_str("2 w S\n");
402 }
403 CheckStyle::Square => {
404 content.push_str(&format!(
406 "{inset} {inset} {} {} re f\n",
407 width - 2.0 * inset,
408 height - 2.0 * inset
409 ));
410 }
411 CheckStyle::Circle => {
412 let cx = width / 2.0;
414 let cy = height / 2.0;
415 let r = (width.min(height) - 2.0 * inset) / 2.0;
416
417 let k = 0.552284749831;
419 content.push_str(&format!("{} {} m\n", cx + r, cy));
420 content.push_str(&format!(
421 "{} {} {} {} {} {} c\n",
422 cx + r,
423 cy + k * r,
424 cx + k * r,
425 cy + r,
426 cx,
427 cy + r
428 ));
429 content.push_str(&format!(
430 "{} {} {} {} {} {} c\n",
431 cx - k * r,
432 cy + r,
433 cx - r,
434 cy + k * r,
435 cx - r,
436 cy
437 ));
438 content.push_str(&format!(
439 "{} {} {} {} {} {} c\n",
440 cx - r,
441 cy - k * r,
442 cx - k * r,
443 cy - r,
444 cx,
445 cy - r
446 ));
447 content.push_str(&format!(
448 "{} {} {} {} {} {} c\n",
449 cx + k * r,
450 cy - r,
451 cx + r,
452 cy - k * r,
453 cx + r,
454 cy
455 ));
456 content.push_str("f\n");
457 }
458 CheckStyle::Star => {
459 let cx = width / 2.0;
461 let cy = height / 2.0;
462 let r = (width.min(height) - 2.0 * inset) / 2.0;
463
464 for i in 0..5 {
466 let angle = std::f64::consts::PI * 2.0 * i as f64 / 5.0
467 - std::f64::consts::PI / 2.0;
468 let x = cx + r * angle.cos();
469 let y = cy + r * angle.sin();
470
471 if i == 0 {
472 content.push_str(&format!("{x} {y} m\n"));
473 } else {
474 content.push_str(&format!("{x} {y} l\n"));
475 }
476 }
477 content.push_str("f\n");
478 }
479 }
480 }
481
482 content.push_str("Q\n");
484
485 let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height]);
486
487 Ok(stream)
488 }
489}
490
491pub struct RadioButtonAppearance {
493 pub selected_color: Color,
495}
496
497impl Default for RadioButtonAppearance {
498 fn default() -> Self {
499 Self {
500 selected_color: Color::black(),
501 }
502 }
503}
504
505impl AppearanceGenerator for RadioButtonAppearance {
506 fn generate_appearance(
507 &self,
508 widget: &Widget,
509 value: Option<&str>,
510 _state: AppearanceState,
511 ) -> Result<AppearanceStream> {
512 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
513 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
514 let is_selected = value.is_some_and(|v| v == "Yes" || v == "On" || v == "true");
515
516 let mut content = String::new();
517
518 content.push_str("q\n");
520
521 if let Some(bg_color) = &widget.appearance.background_color {
523 match bg_color {
524 Color::Gray(g) => content.push_str(&format!("{g} g\n")),
525 Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
526 Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
527 }
528 } else {
529 content.push_str("1 g\n"); }
531
532 let cx = width / 2.0;
533 let cy = height / 2.0;
534 let r = width.min(height) / 2.0 - widget.appearance.border_width;
535
536 let k = 0.552284749831;
538 content.push_str(&format!("{} {} m\n", cx + r, cy));
539 content.push_str(&format!(
540 "{} {} {} {} {} {} c\n",
541 cx + r,
542 cy + k * r,
543 cx + k * r,
544 cy + r,
545 cx,
546 cy + r
547 ));
548 content.push_str(&format!(
549 "{} {} {} {} {} {} c\n",
550 cx - k * r,
551 cy + r,
552 cx - r,
553 cy + k * r,
554 cx - r,
555 cy
556 ));
557 content.push_str(&format!(
558 "{} {} {} {} {} {} c\n",
559 cx - r,
560 cy - k * r,
561 cx - k * r,
562 cy - r,
563 cx,
564 cy - r
565 ));
566 content.push_str(&format!(
567 "{} {} {} {} {} {} c\n",
568 cx + k * r,
569 cy - r,
570 cx + r,
571 cy - k * r,
572 cx + r,
573 cy
574 ));
575 content.push_str("f\n");
576
577 if let Some(border_color) = &widget.appearance.border_color {
579 match border_color {
580 Color::Gray(g) => content.push_str(&format!("{g} G\n")),
581 Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} RG\n")),
582 Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} K\n")),
583 }
584 content.push_str(&format!("{} w\n", widget.appearance.border_width));
585
586 content.push_str(&format!("{} {} m\n", cx + r, cy));
587 content.push_str(&format!(
588 "{} {} {} {} {} {} c\n",
589 cx + r,
590 cy + k * r,
591 cx + k * r,
592 cy + r,
593 cx,
594 cy + r
595 ));
596 content.push_str(&format!(
597 "{} {} {} {} {} {} c\n",
598 cx - k * r,
599 cy + r,
600 cx - r,
601 cy + k * r,
602 cx - r,
603 cy
604 ));
605 content.push_str(&format!(
606 "{} {} {} {} {} {} c\n",
607 cx - r,
608 cy - k * r,
609 cx - k * r,
610 cy - r,
611 cx,
612 cy - r
613 ));
614 content.push_str(&format!(
615 "{} {} {} {} {} {} c\n",
616 cx + k * r,
617 cy - r,
618 cx + r,
619 cy - k * r,
620 cx + r,
621 cy
622 ));
623 content.push_str("S\n");
624 }
625
626 if is_selected {
628 match self.selected_color {
629 Color::Gray(g) => content.push_str(&format!("{g} g\n")),
630 Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
631 Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
632 }
633
634 let inner_r = r * 0.4;
635 content.push_str(&format!("{} {} m\n", cx + inner_r, cy));
636 content.push_str(&format!(
637 "{} {} {} {} {} {} c\n",
638 cx + inner_r,
639 cy + k * inner_r,
640 cx + k * inner_r,
641 cy + inner_r,
642 cx,
643 cy + inner_r
644 ));
645 content.push_str(&format!(
646 "{} {} {} {} {} {} c\n",
647 cx - k * inner_r,
648 cy + inner_r,
649 cx - inner_r,
650 cy + k * inner_r,
651 cx - inner_r,
652 cy
653 ));
654 content.push_str(&format!(
655 "{} {} {} {} {} {} c\n",
656 cx - inner_r,
657 cy - k * inner_r,
658 cx - k * inner_r,
659 cy - inner_r,
660 cx,
661 cy - inner_r
662 ));
663 content.push_str(&format!(
664 "{} {} {} {} {} {} c\n",
665 cx + k * inner_r,
666 cy - inner_r,
667 cx + inner_r,
668 cy - k * inner_r,
669 cx + inner_r,
670 cy
671 ));
672 content.push_str("f\n");
673 }
674
675 content.push_str("Q\n");
677
678 let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height]);
679
680 Ok(stream)
681 }
682}
683
684pub struct PushButtonAppearance {
686 pub label: String,
688 pub font: Font,
690 pub font_size: f64,
692 pub text_color: Color,
694}
695
696impl Default for PushButtonAppearance {
697 fn default() -> Self {
698 Self {
699 label: String::new(),
700 font: Font::Helvetica,
701 font_size: 12.0,
702 text_color: Color::black(),
703 }
704 }
705}
706
707impl AppearanceGenerator for PushButtonAppearance {
708 fn generate_appearance(
709 &self,
710 widget: &Widget,
711 _value: Option<&str>,
712 state: AppearanceState,
713 ) -> Result<AppearanceStream> {
714 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
715 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
716
717 let mut content = String::new();
718
719 content.push_str("q\n");
721
722 let bg_color = match state {
724 AppearanceState::Down => Color::gray(0.8),
725 AppearanceState::Rollover => Color::gray(0.95),
726 AppearanceState::Normal => widget
727 .appearance
728 .background_color
729 .unwrap_or(Color::gray(0.9)),
730 };
731
732 match bg_color {
733 Color::Gray(g) => content.push_str(&format!("{g} g\n")),
734 Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
735 Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
736 }
737 content.push_str(&format!("0 0 {width} {height} re f\n"));
738
739 if matches!(widget.appearance.border_style, BorderStyle::Beveled) {
741 content.push_str("0.9 G\n");
743 content.push_str("2 w\n");
744 content.push_str(&format!("0 {height} m {width} {height} l\n"));
745 content.push_str(&format!("{width} {height} l {width} 0 l S\n"));
746
747 content.push_str("0.3 G\n");
749 content.push_str(&format!("0 0 m {width} 0 l\n"));
750 content.push_str(&format!("0 0 l 0 {height} l S\n"));
751 } else {
752 if let Some(border_color) = &widget.appearance.border_color {
754 match border_color {
755 Color::Gray(g) => content.push_str(&format!("{g} G\n")),
756 Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} RG\n")),
757 Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} K\n")),
758 }
759 content.push_str(&format!("{} w\n", widget.appearance.border_width));
760 content.push_str(&format!("0 0 {width} {height} re S\n"));
761 }
762 }
763
764 if !self.label.is_empty() {
766 match self.text_color {
767 Color::Gray(g) => content.push_str(&format!("{g} g\n")),
768 Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
769 Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
770 }
771
772 content.push_str("BT\n");
773 content.push_str(&format!(
774 "/{} {} Tf\n",
775 self.font.pdf_name(),
776 self.font_size
777 ));
778
779 let text_x = width / 4.0; let text_y = (height - self.font_size) / 2.0 + self.font_size * 0.3;
782
783 content.push_str(&format!("{text_x} {text_y} Td\n"));
784
785 let escaped_label = self
786 .label
787 .replace('\\', "\\\\")
788 .replace('(', "\\(")
789 .replace(')', "\\)");
790 content.push_str(&format!("({escaped_label}) Tj\n"));
791
792 content.push_str("ET\n");
793 }
794
795 content.push_str("Q\n");
797
798 let mut resources = Dictionary::new();
800
801 let mut font_dict = Dictionary::new();
803 let mut font_res = Dictionary::new();
804 font_res.set("Type", Object::Name("Font".to_string()));
805 font_res.set("Subtype", Object::Name("Type1".to_string()));
806 font_res.set("BaseFont", Object::Name(self.font.pdf_name()));
807 font_dict.set(self.font.pdf_name(), Object::Dictionary(font_res));
808 resources.set("Font", Object::Dictionary(font_dict));
809
810 let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height])
811 .with_resources(resources);
812
813 Ok(stream)
814 }
815}
816
817#[derive(Debug, Clone)]
819pub struct ComboBoxAppearance {
820 pub font: Font,
822 pub font_size: f64,
824 pub text_color: Color,
826 pub selected_text: Option<String>,
828 pub show_arrow: bool,
830}
831
832impl Default for ComboBoxAppearance {
833 fn default() -> Self {
834 Self {
835 font: Font::Helvetica,
836 font_size: 12.0,
837 text_color: Color::black(),
838 selected_text: None,
839 show_arrow: true,
840 }
841 }
842}
843
844impl AppearanceGenerator for ComboBoxAppearance {
845 fn generate_appearance(
846 &self,
847 widget: &Widget,
848 value: Option<&str>,
849 _state: AppearanceState,
850 ) -> Result<AppearanceStream> {
851 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
852 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
853
854 let mut content = String::new();
855
856 content.push_str("1 1 1 rg\n"); content.push_str(&format!("0 0 {} {} re\n", width, height));
859 content.push_str("f\n");
860
861 if let Some(ref border_color) = widget.appearance.border_color {
863 match border_color {
864 Color::Gray(g) => content.push_str(&format!("{} G\n", g)),
865 Color::Rgb(r, g, b) => content.push_str(&format!("{} {} {} RG\n", r, g, b)),
866 Color::Cmyk(c, m, y, k) => {
867 content.push_str(&format!("{} {} {} {} K\n", c, m, y, k))
868 }
869 }
870 content.push_str(&format!("{} w\n", widget.appearance.border_width));
871 content.push_str(&format!("0 0 {} {} re\n", width, height));
872 content.push_str("S\n");
873 }
874
875 if self.show_arrow {
877 let arrow_x = width - 15.0;
878 let arrow_y = height / 2.0;
879 content.push_str("0.5 0.5 0.5 rg\n"); content.push_str(&format!("{} {} m\n", arrow_x, arrow_y + 3.0));
881 content.push_str(&format!("{} {} l\n", arrow_x + 8.0, arrow_y + 3.0));
882 content.push_str(&format!("{} {} l\n", arrow_x + 4.0, arrow_y - 3.0));
883 content.push_str("f\n");
884 }
885
886 let text_to_show = value.or(self.selected_text.as_deref());
888 if let Some(text) = text_to_show {
889 content.push_str("BT\n");
890 content.push_str(&format!(
891 "/{} {} Tf\n",
892 self.font.pdf_name(),
893 self.font_size
894 ));
895 match self.text_color {
896 Color::Gray(g) => content.push_str(&format!("{} g\n", g)),
897 Color::Rgb(r, g, b) => content.push_str(&format!("{} {} {} rg\n", r, g, b)),
898 Color::Cmyk(c, m, y, k) => {
899 content.push_str(&format!("{} {} {} {} k\n", c, m, y, k))
900 }
901 }
902 content.push_str(&format!("5 {} Td\n", (height - self.font_size) / 2.0));
903
904 let escaped = text
906 .replace('\\', "\\\\")
907 .replace('(', "\\(")
908 .replace(')', "\\)")
909 .replace('\n', "\\n")
910 .replace('\r', "\\r")
911 .replace('\t', "\\t");
912 content.push_str(&format!("({}) Tj\n", escaped));
913 content.push_str("ET\n");
914 }
915
916 let bbox = [0.0, 0.0, width, height];
917 Ok(AppearanceStream::new(content.into_bytes(), bbox))
918 }
919}
920
921#[derive(Debug, Clone)]
923pub struct ListBoxAppearance {
924 pub font: Font,
926 pub font_size: f64,
928 pub text_color: Color,
930 pub selection_color: Color,
932 pub options: Vec<String>,
934 pub selected: Vec<usize>,
936 pub item_height: f64,
938}
939
940impl Default for ListBoxAppearance {
941 fn default() -> Self {
942 Self {
943 font: Font::Helvetica,
944 font_size: 12.0,
945 text_color: Color::black(),
946 selection_color: Color::rgb(0.2, 0.4, 0.8),
947 options: Vec::new(),
948 selected: Vec::new(),
949 item_height: 16.0,
950 }
951 }
952}
953
954impl AppearanceGenerator for ListBoxAppearance {
955 fn generate_appearance(
956 &self,
957 widget: &Widget,
958 _value: Option<&str>,
959 _state: AppearanceState,
960 ) -> Result<AppearanceStream> {
961 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
962 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
963
964 let mut content = String::new();
965
966 content.push_str("1 1 1 rg\n"); content.push_str(&format!("0 0 {} {} re\n", width, height));
969 content.push_str("f\n");
970
971 if let Some(ref border_color) = widget.appearance.border_color {
973 match border_color {
974 Color::Gray(g) => content.push_str(&format!("{} G\n", g)),
975 Color::Rgb(r, g, b) => content.push_str(&format!("{} {} {} RG\n", r, g, b)),
976 Color::Cmyk(c, m, y, k) => {
977 content.push_str(&format!("{} {} {} {} K\n", c, m, y, k))
978 }
979 }
980 content.push_str(&format!("{} w\n", widget.appearance.border_width));
981 content.push_str(&format!("0 0 {} {} re\n", width, height));
982 content.push_str("S\n");
983 }
984
985 let mut y = height - self.item_height;
987 for (index, option) in self.options.iter().enumerate() {
988 if y < 0.0 {
989 break; }
991
992 if self.selected.contains(&index) {
994 match self.selection_color {
995 Color::Gray(g) => content.push_str(&format!("{} g\n", g)),
996 Color::Rgb(r, g, b) => content.push_str(&format!("{} {} {} rg\n", r, g, b)),
997 Color::Cmyk(c, m, y_val, k) => {
998 content.push_str(&format!("{} {} {} {} k\n", c, m, y_val, k))
999 }
1000 }
1001 content.push_str(&format!("0 {} {} {} re\n", y, width, self.item_height));
1002 content.push_str("f\n");
1003 }
1004
1005 content.push_str("BT\n");
1007 content.push_str(&format!(
1008 "/{} {} Tf\n",
1009 self.font.pdf_name(),
1010 self.font_size
1011 ));
1012
1013 if self.selected.contains(&index) {
1015 content.push_str("1 1 1 rg\n");
1016 } else {
1017 match self.text_color {
1018 Color::Gray(g) => content.push_str(&format!("{} g\n", g)),
1019 Color::Rgb(r, g, b) => content.push_str(&format!("{} {} {} rg\n", r, g, b)),
1020 Color::Cmyk(c, m, y_val, k) => {
1021 content.push_str(&format!("{} {} {} {} k\n", c, m, y_val, k))
1022 }
1023 }
1024 }
1025
1026 content.push_str(&format!("5 {} Td\n", y + 2.0));
1027
1028 let escaped = option
1030 .replace('\\', "\\\\")
1031 .replace('(', "\\(")
1032 .replace(')', "\\)")
1033 .replace('\n', "\\n")
1034 .replace('\r', "\\r")
1035 .replace('\t', "\\t");
1036 content.push_str(&format!("({}) Tj\n", escaped));
1037 content.push_str("ET\n");
1038
1039 y -= self.item_height;
1040 }
1041
1042 let bbox = [0.0, 0.0, width, height];
1043 Ok(AppearanceStream::new(content.into_bytes(), bbox))
1044 }
1045}
1046
1047pub fn generate_default_appearance(
1049 field_type: FieldType,
1050 widget: &Widget,
1051 value: Option<&str>,
1052) -> Result<AppearanceStream> {
1053 match field_type {
1054 FieldType::Text => {
1055 let generator = TextFieldAppearance::default();
1056 generator.generate_appearance(widget, value, AppearanceState::Normal)
1057 }
1058 FieldType::Button => {
1059 let generator = CheckBoxAppearance::default();
1062 generator.generate_appearance(widget, value, AppearanceState::Normal)
1063 }
1064 FieldType::Choice => {
1065 let generator = ComboBoxAppearance::default();
1067 generator.generate_appearance(widget, value, AppearanceState::Normal)
1068 }
1069 FieldType::Signature => {
1070 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
1072 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
1073 Ok(AppearanceStream::new(
1074 b"q\nQ\n".to_vec(),
1075 [0.0, 0.0, width, height],
1076 ))
1077 }
1078 }
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083 use super::*;
1084 use crate::geometry::{Point, Rectangle};
1085
1086 #[test]
1087 fn test_appearance_state_names() {
1088 assert_eq!(AppearanceState::Normal.pdf_name(), "N");
1089 assert_eq!(AppearanceState::Rollover.pdf_name(), "R");
1090 assert_eq!(AppearanceState::Down.pdf_name(), "D");
1091 }
1092
1093 #[test]
1094 fn test_appearance_stream_creation() {
1095 let content = b"q\n1 0 0 RG\n0 0 100 50 re S\nQ\n";
1096 let stream = AppearanceStream::new(content.to_vec(), [0.0, 0.0, 100.0, 50.0]);
1097
1098 assert_eq!(stream.content, content);
1099 assert_eq!(stream.bbox, [0.0, 0.0, 100.0, 50.0]);
1100 assert!(stream.resources.is_empty());
1101 }
1102
1103 #[test]
1104 fn test_appearance_stream_with_resources() {
1105 let mut resources = Dictionary::new();
1106 resources.set("Font", Object::Name("F1".to_string()));
1107
1108 let content = b"BT\n/F1 12 Tf\n(Test) Tj\nET\n";
1109 let stream = AppearanceStream::new(content.to_vec(), [0.0, 0.0, 100.0, 50.0])
1110 .with_resources(resources.clone());
1111
1112 assert_eq!(stream.resources, resources);
1113 }
1114
1115 #[test]
1116 fn test_appearance_dictionary() {
1117 let mut app_dict = AppearanceDictionary::new();
1118
1119 let normal_stream = AppearanceStream::new(b"normal".to_vec(), [0.0, 0.0, 10.0, 10.0]);
1120 let down_stream = AppearanceStream::new(b"down".to_vec(), [0.0, 0.0, 10.0, 10.0]);
1121
1122 app_dict.set_appearance(AppearanceState::Normal, normal_stream.clone());
1123 app_dict.set_appearance(AppearanceState::Down, down_stream);
1124
1125 assert!(app_dict.get_appearance(AppearanceState::Normal).is_some());
1126 assert!(app_dict.get_appearance(AppearanceState::Down).is_some());
1127 assert!(app_dict.get_appearance(AppearanceState::Rollover).is_none());
1128 }
1129
1130 #[test]
1131 fn test_text_field_appearance() {
1132 let widget = Widget::new(Rectangle {
1133 lower_left: Point { x: 0.0, y: 0.0 },
1134 upper_right: Point { x: 200.0, y: 30.0 },
1135 });
1136
1137 let generator = TextFieldAppearance::default();
1138 let result =
1139 generator.generate_appearance(&widget, Some("Test Text"), AppearanceState::Normal);
1140
1141 assert!(result.is_ok());
1142 let stream = result.unwrap();
1143 assert_eq!(stream.bbox, [0.0, 0.0, 200.0, 30.0]);
1144
1145 let content = String::from_utf8_lossy(&stream.content);
1146 assert!(content.contains("BT"));
1147 assert!(content.contains("(Test Text) Tj"));
1148 assert!(content.contains("ET"));
1149 }
1150
1151 #[test]
1152 fn test_checkbox_appearance_checked() {
1153 let widget = Widget::new(Rectangle {
1154 lower_left: Point { x: 0.0, y: 0.0 },
1155 upper_right: Point { x: 20.0, y: 20.0 },
1156 });
1157
1158 let generator = CheckBoxAppearance::default();
1159 let result = generator.generate_appearance(&widget, Some("Yes"), AppearanceState::Normal);
1160
1161 assert!(result.is_ok());
1162 let stream = result.unwrap();
1163 let content = String::from_utf8_lossy(&stream.content);
1164
1165 assert!(content.contains(" m"));
1167 assert!(content.contains(" l"));
1168 assert!(content.contains(" S"));
1169 }
1170
1171 #[test]
1172 fn test_checkbox_appearance_unchecked() {
1173 let widget = Widget::new(Rectangle {
1174 lower_left: Point { x: 0.0, y: 0.0 },
1175 upper_right: Point { x: 20.0, y: 20.0 },
1176 });
1177
1178 let generator = CheckBoxAppearance::default();
1179 let result = generator.generate_appearance(&widget, Some("No"), AppearanceState::Normal);
1180
1181 assert!(result.is_ok());
1182 let stream = result.unwrap();
1183 let content = String::from_utf8_lossy(&stream.content);
1184
1185 assert!(content.contains("q"));
1187 assert!(content.contains("Q"));
1188 }
1189
1190 #[test]
1191 fn test_radio_button_appearance() {
1192 let widget = Widget::new(Rectangle {
1193 lower_left: Point { x: 0.0, y: 0.0 },
1194 upper_right: Point { x: 20.0, y: 20.0 },
1195 });
1196
1197 let generator = RadioButtonAppearance::default();
1198 let result = generator.generate_appearance(&widget, Some("Yes"), AppearanceState::Normal);
1199
1200 assert!(result.is_ok());
1201 let stream = result.unwrap();
1202 let content = String::from_utf8_lossy(&stream.content);
1203
1204 assert!(
1206 content.contains(" c"),
1207 "Content should contain curve commands"
1208 );
1209 assert!(
1210 content.contains("f\n"),
1211 "Content should contain fill commands"
1212 );
1213 }
1214
1215 #[test]
1216 fn test_push_button_appearance() {
1217 let mut generator = PushButtonAppearance::default();
1218 generator.label = "Click Me".to_string();
1219
1220 let widget = Widget::new(Rectangle {
1221 lower_left: Point { x: 0.0, y: 0.0 },
1222 upper_right: Point { x: 100.0, y: 30.0 },
1223 });
1224
1225 let result = generator.generate_appearance(&widget, None, AppearanceState::Normal);
1226
1227 assert!(result.is_ok());
1228 let stream = result.unwrap();
1229 let content = String::from_utf8_lossy(&stream.content);
1230
1231 assert!(content.contains("(Click Me) Tj"));
1232 assert!(!stream.resources.is_empty());
1233 }
1234
1235 #[test]
1236 fn test_push_button_states() {
1237 let generator = PushButtonAppearance::default();
1238 let widget = Widget::new(Rectangle {
1239 lower_left: Point { x: 0.0, y: 0.0 },
1240 upper_right: Point { x: 100.0, y: 30.0 },
1241 });
1242
1243 let normal = generator
1245 .generate_appearance(&widget, None, AppearanceState::Normal)
1246 .unwrap();
1247 let down = generator
1248 .generate_appearance(&widget, None, AppearanceState::Down)
1249 .unwrap();
1250 let rollover = generator
1251 .generate_appearance(&widget, None, AppearanceState::Rollover)
1252 .unwrap();
1253
1254 assert_ne!(normal.content, down.content);
1256 assert_ne!(normal.content, rollover.content);
1257 assert_ne!(down.content, rollover.content);
1258 }
1259
1260 #[test]
1261 fn test_check_styles() {
1262 let widget = Widget::new(Rectangle {
1263 lower_left: Point { x: 0.0, y: 0.0 },
1264 upper_right: Point { x: 20.0, y: 20.0 },
1265 });
1266
1267 for style in [
1269 CheckStyle::Check,
1270 CheckStyle::Cross,
1271 CheckStyle::Square,
1272 CheckStyle::Circle,
1273 CheckStyle::Star,
1274 ] {
1275 let mut generator = CheckBoxAppearance::default();
1276 generator.check_style = style;
1277
1278 let result =
1279 generator.generate_appearance(&widget, Some("Yes"), AppearanceState::Normal);
1280
1281 assert!(result.is_ok(), "Failed for style {:?}", style);
1282 }
1283 }
1284
1285 #[test]
1286 fn test_appearance_state_pdf_names() {
1287 assert_eq!(AppearanceState::Normal.pdf_name(), "N");
1288 assert_eq!(AppearanceState::Rollover.pdf_name(), "R");
1289 assert_eq!(AppearanceState::Down.pdf_name(), "D");
1290 }
1291
1292 #[test]
1293 fn test_appearance_stream_creation_advanced() {
1294 let content = b"q 1 0 0 1 0 0 cm Q".to_vec();
1295 let bbox = [0.0, 0.0, 100.0, 50.0];
1296 let stream = AppearanceStream::new(content.clone(), bbox);
1297
1298 assert_eq!(stream.content, content);
1299 assert_eq!(stream.bbox, bbox);
1300 assert!(stream.resources.is_empty());
1301 }
1302
1303 #[test]
1304 fn test_appearance_stream_with_resources_advanced() {
1305 let mut resources = Dictionary::new();
1306 resources.set("Font", Object::Dictionary(Dictionary::new()));
1307
1308 let stream =
1309 AppearanceStream::new(vec![], [0.0, 0.0, 10.0, 10.0]).with_resources(resources.clone());
1310
1311 assert_eq!(stream.resources, resources);
1312 }
1313
1314 #[test]
1315 fn test_appearance_dictionary_new() {
1316 let dict = AppearanceDictionary::new();
1317 assert!(dict.appearances.is_empty());
1318 assert!(dict.down_appearances.is_empty());
1319 }
1320
1321 #[test]
1322 fn test_appearance_dictionary_set_get() {
1323 let mut dict = AppearanceDictionary::new();
1324 let stream = AppearanceStream::new(vec![1, 2, 3], [0.0, 0.0, 10.0, 10.0]);
1325
1326 dict.set_appearance(AppearanceState::Normal, stream.clone());
1327 assert!(dict.get_appearance(AppearanceState::Normal).is_some());
1328 assert!(dict.get_appearance(AppearanceState::Down).is_none());
1329 }
1330
1331 #[test]
1332 fn test_text_field_multiline() {
1333 let mut generator = TextFieldAppearance::default();
1334 generator.multiline = true;
1335
1336 let widget = Widget::new(Rectangle {
1337 lower_left: Point { x: 0.0, y: 0.0 },
1338 upper_right: Point { x: 200.0, y: 100.0 },
1339 });
1340
1341 let text = "Line 1\nLine 2\nLine 3";
1342 let result = generator.generate_appearance(&widget, Some(text), AppearanceState::Normal);
1343 assert!(result.is_ok());
1344 }
1345
1346 #[test]
1347 fn test_appearance_with_custom_colors() {
1348 let mut generator = TextFieldAppearance::default();
1349 generator.text_color = Color::rgb(1.0, 0.0, 0.0); generator.font_size = 14.0;
1351 generator.justification = 1; let widget = Widget::new(Rectangle {
1354 lower_left: Point { x: 0.0, y: 0.0 },
1355 upper_right: Point { x: 100.0, y: 30.0 },
1356 });
1357
1358 let result =
1359 generator.generate_appearance(&widget, Some("Colored"), AppearanceState::Normal);
1360 assert!(result.is_ok());
1361 }
1362}