1use crate::error::{PdfError, Result};
7use crate::forms::{BorderStyle, DefaultAppearance, FieldType, Widget};
8use crate::graphics::Color;
9use crate::objects::{Dictionary, Object, Stream};
10use crate::text::{escape_pdf_string_literal, Font, TextEncoding};
11use std::collections::{HashMap, HashSet};
12
13fn emit_tj_for_builtin(content: &mut String, text: &str, font: &Font) -> Result<()> {
30 if font.is_custom() {
31 return Err(PdfError::EncodingError(format!(
32 "Custom Type0/CID fonts are not yet supported in form-field appearance \
33 streams (font: {:?}). Track: https://github.com/bzsanti/oxidizePdf/issues/212",
34 font.pdf_name(),
35 )));
36 }
37 if font.is_symbolic() {
38 return Err(PdfError::EncodingError(format!(
39 "Symbolic fonts ({:?}) are not supported for form-field text — their \
40 encoding depends on glyph names, not Unicode codepoints",
41 font.pdf_name(),
42 )));
43 }
44
45 let bytes = TextEncoding::WinAnsiEncoding
48 .encode_strict(text)
49 .map_err(|ch| {
50 PdfError::EncodingError(format!(
51 "Value contains character {:?} (U+{:04X}) which cannot be encoded \
52 in WinAnsiEncoding used by built-in PDF font {}. Register a Type0 \
53 font via `Document::add_font_from_bytes` and attach it to the field; \
54 see https://github.com/bzsanti/oxidizePdf/issues/212",
55 ch,
56 ch as u32,
57 font.pdf_name(),
58 ))
59 })?;
60
61 content.push_str(&format!("({}) Tj\n", escape_pdf_string_literal(&bytes)));
62 Ok(())
63}
64
65fn emit_tj_for_custom(
78 content: &mut String,
79 text: &str,
80 font_name: &str,
81 custom_font: &crate::fonts::Font,
82 used_chars: &mut HashSet<char>,
83) -> Result<()> {
84 use std::fmt::Write;
85 let mut hex = String::with_capacity(text.len() * 4);
86 for ch in text.chars() {
87 let gid = custom_font.glyph_mapping.char_to_glyph(ch).ok_or_else(|| {
88 PdfError::EncodingError(format!(
89 "Custom font {:?} has no glyph for character {:?} (U+{:04X}); \
90 the font's cmap does not cover this codepoint",
91 font_name, ch, ch as u32,
92 ))
93 })?;
94 write!(&mut hex, "{:04X}", gid).expect("writing to String cannot fail");
95 used_chars.insert(ch);
96 }
97 content.push_str(&format!("<{}> Tj\n", hex));
98 Ok(())
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103pub enum AppearanceState {
104 Normal,
106 Rollover,
108 Down,
110}
111
112impl AppearanceState {
113 pub fn pdf_name(&self) -> &'static str {
115 match self {
116 AppearanceState::Normal => "N",
117 AppearanceState::Rollover => "R",
118 AppearanceState::Down => "D",
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct AppearanceStream {
126 pub content: Vec<u8>,
128 pub resources: Dictionary,
130 pub bbox: [f64; 4],
132}
133
134impl AppearanceStream {
135 pub fn new(content: Vec<u8>, bbox: [f64; 4]) -> Self {
137 Self {
138 content,
139 resources: Dictionary::new(),
140 bbox,
141 }
142 }
143
144 pub fn with_resources(mut self, resources: Dictionary) -> Self {
146 self.resources = resources;
147 self
148 }
149
150 pub fn to_stream(&self) -> Stream {
152 let mut dict = Dictionary::new();
153 dict.set("Type", Object::Name("XObject".to_string()));
154 dict.set("Subtype", Object::Name("Form".to_string()));
155
156 let bbox_array = vec![
158 Object::Real(self.bbox[0]),
159 Object::Real(self.bbox[1]),
160 Object::Real(self.bbox[2]),
161 Object::Real(self.bbox[3]),
162 ];
163 dict.set("BBox", Object::Array(bbox_array));
164
165 if !self.resources.is_empty() {
167 dict.set("Resources", Object::Dictionary(self.resources.clone()));
168 }
169
170 Stream::with_dictionary(dict, self.content.clone())
172 }
173}
174
175#[derive(Debug, Clone)]
187pub struct FieldAppearanceResult {
188 pub stream: AppearanceStream,
190 pub used_chars_by_font: HashMap<String, HashSet<char>>,
193}
194
195#[derive(Debug, Clone)]
197pub struct AppearanceDictionary {
198 appearances: HashMap<AppearanceState, AppearanceStream>,
200 down_appearances: HashMap<String, AppearanceStream>,
202}
203
204impl AppearanceDictionary {
205 pub fn new() -> Self {
207 Self {
208 appearances: HashMap::new(),
209 down_appearances: HashMap::new(),
210 }
211 }
212
213 pub fn set_appearance(&mut self, state: AppearanceState, stream: AppearanceStream) {
215 self.appearances.insert(state, stream);
216 }
217
218 pub fn set_down_appearance(&mut self, value: String, stream: AppearanceStream) {
220 self.down_appearances.insert(value, stream);
221 }
222
223 pub fn get_appearance(&self, state: AppearanceState) -> Option<&AppearanceStream> {
225 self.appearances.get(&state)
226 }
227
228 pub fn to_dict(&self) -> Dictionary {
230 let mut dict = Dictionary::new();
231
232 for (state, stream) in &self.appearances {
234 let stream_obj = stream.to_stream();
235 dict.set(
236 state.pdf_name(),
237 Object::Stream(stream_obj.dictionary().clone(), stream_obj.data().to_vec()),
238 );
239 }
240
241 if !self.down_appearances.is_empty() {
243 let mut down_dict = Dictionary::new();
244 for (value, stream) in &self.down_appearances {
245 let stream_obj = stream.to_stream();
246 down_dict.set(
247 value,
248 Object::Stream(stream_obj.dictionary().clone(), stream_obj.data().to_vec()),
249 );
250 }
251 dict.set("D", Object::Dictionary(down_dict));
252 }
253
254 dict
255 }
256}
257
258impl Default for AppearanceDictionary {
259 fn default() -> Self {
260 Self::new()
261 }
262}
263
264pub trait AppearanceGenerator {
266 fn generate_appearance(
268 &self,
269 widget: &Widget,
270 value: Option<&str>,
271 state: AppearanceState,
272 ) -> Result<AppearanceStream>;
273}
274
275pub struct TextFieldAppearance {
277 pub font: Font,
279 pub font_size: f64,
281 pub text_color: Color,
283 pub justification: i32,
285 pub multiline: bool,
287}
288
289impl Default for TextFieldAppearance {
290 fn default() -> Self {
291 Self {
292 font: Font::Helvetica,
293 font_size: 12.0,
294 text_color: Color::black(),
295 justification: 0,
296 multiline: false,
297 }
298 }
299}
300
301impl AppearanceGenerator for TextFieldAppearance {
302 fn generate_appearance(
303 &self,
304 widget: &Widget,
305 value: Option<&str>,
306 state: AppearanceState,
307 ) -> Result<AppearanceStream> {
308 let result = self.generate_appearance_with_font(widget, value, state, None)?;
309 Ok(result.stream)
310 }
311}
312
313impl TextFieldAppearance {
314 pub fn generate_appearance_with_font(
325 &self,
326 widget: &Widget,
327 value: Option<&str>,
328 _state: AppearanceState,
329 custom_font: Option<&crate::fonts::Font>,
330 ) -> Result<FieldAppearanceResult> {
331 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
332 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
333
334 let mut content = String::new();
335 let mut used_chars_per_font: HashMap<String, HashSet<char>> = HashMap::new();
336
337 content.push_str("q\n");
339
340 if let Some(bg_color) = &widget.appearance.background_color {
342 crate::graphics::color::write_fill_color(&mut content, *bg_color);
343 content.push_str(&format!("0 0 {width} {height} re f\n"));
344 }
345
346 if let Some(border_color) = &widget.appearance.border_color {
348 crate::graphics::color::write_stroke_color(&mut content, *border_color);
349 content.push_str(&format!("{} w\n", widget.appearance.border_width));
350
351 match widget.appearance.border_style {
352 BorderStyle::Solid => {
353 content.push_str(&format!("0 0 {width} {height} re S\n"));
354 }
355 BorderStyle::Dashed => {
356 content.push_str("[3 2] 0 d\n");
357 content.push_str(&format!("0 0 {width} {height} re S\n"));
358 }
359 BorderStyle::Beveled | BorderStyle::Inset => {
360 content.push_str(&format!("0 0 {width} {height} re S\n"));
362 }
363 BorderStyle::Underline => {
364 content.push_str(&format!("0 0 m {width} 0 l S\n"));
365 }
366 }
367 }
368
369 if let Some(text) = value {
371 crate::graphics::color::write_fill_color(&mut content, self.text_color);
373
374 content.push_str("BT\n");
376 content.push_str(&format!(
377 "/{} {} Tf\n",
378 self.font.pdf_name(),
379 self.font_size
380 ));
381
382 let padding = 2.0;
384 let text_y = (height - self.font_size) / 2.0 + self.font_size * 0.3;
385
386 let text_x = match self.justification {
387 1 => width / 2.0, 2 => width - padding, _ => padding, };
391
392 content.push_str(&format!("{text_x} {text_y} Td\n"));
393
394 match (self.font.is_custom(), custom_font) {
409 (true, Some(cf)) => {
410 let font_name = self.font.pdf_name();
411 let entry = used_chars_per_font.entry(font_name.clone()).or_default();
412 emit_tj_for_custom(&mut content, text, &font_name, cf, entry)?;
413 }
414 (true, None) => {
415 return Err(PdfError::EncodingError(format!(
416 "Font {:?} is marked as Custom but was not found in the \
417 document registry; call Document::add_font_from_bytes with \
418 this name before fill_field/save. See issue #212.",
419 self.font.pdf_name(),
420 )));
421 }
422 (false, _) => {
423 emit_tj_for_builtin(&mut content, text, &self.font)?;
424 }
425 }
426
427 content.push_str("ET\n");
429 }
430
431 content.push_str("Q\n");
433
434 let mut resources = Dictionary::new();
441 let mut font_dict = Dictionary::new();
442 if self.font.is_custom() {
443 let mut placeholder = Dictionary::new();
445 placeholder.set("Type", Object::Name("Font".to_string()));
446 placeholder.set("Subtype", Object::Name("Type0".to_string()));
447 placeholder.set("BaseFont", Object::Name(self.font.pdf_name()));
448 placeholder.set("Encoding", Object::Name("Identity-H".to_string()));
449 font_dict.set(self.font.pdf_name(), Object::Dictionary(placeholder));
450 } else {
451 let mut font_res = Dictionary::new();
452 font_res.set("Type", Object::Name("Font".to_string()));
453 font_res.set("Subtype", Object::Name("Type1".to_string()));
454 font_res.set("BaseFont", Object::Name(self.font.pdf_name()));
455 font_dict.set(self.font.pdf_name(), Object::Dictionary(font_res));
456 }
457 resources.set("Font", Object::Dictionary(font_dict));
458
459 let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height])
460 .with_resources(resources);
461
462 Ok(FieldAppearanceResult {
463 stream,
464 used_chars_by_font: used_chars_per_font,
465 })
466 }
467}
468
469pub struct CheckBoxAppearance {
471 pub check_style: CheckStyle,
473 pub check_color: Color,
475}
476
477#[derive(Debug, Clone, Copy)]
479pub enum CheckStyle {
480 Check,
482 Cross,
484 Square,
486 Circle,
488 Star,
490}
491
492impl Default for CheckBoxAppearance {
493 fn default() -> Self {
494 Self {
495 check_style: CheckStyle::Check,
496 check_color: Color::black(),
497 }
498 }
499}
500
501impl AppearanceGenerator for CheckBoxAppearance {
502 fn generate_appearance(
503 &self,
504 widget: &Widget,
505 value: Option<&str>,
506 _state: AppearanceState,
507 ) -> Result<AppearanceStream> {
508 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
509 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
510 let is_checked = value.is_some_and(|v| v == "Yes" || v == "On" || v == "true");
511
512 let mut content = String::new();
513
514 content.push_str("q\n");
516
517 if let Some(bg_color) = &widget.appearance.background_color {
519 crate::graphics::color::write_fill_color(&mut content, *bg_color);
520 content.push_str(&format!("0 0 {width} {height} re f\n"));
521 }
522
523 if let Some(border_color) = &widget.appearance.border_color {
525 crate::graphics::color::write_stroke_color(&mut content, *border_color);
526 content.push_str(&format!("{} w\n", widget.appearance.border_width));
527 content.push_str(&format!("0 0 {width} {height} re S\n"));
528 }
529
530 if is_checked {
532 crate::graphics::color::write_fill_color(&mut content, self.check_color);
534
535 let inset = width * 0.2;
536
537 match self.check_style {
538 CheckStyle::Check => {
539 content.push_str(&format!("{} {} m\n", inset, height * 0.5));
541 content.push_str(&format!("{} {} l\n", width * 0.4, inset));
542 content.push_str(&format!("{} {} l\n", width - inset, height - inset));
543 content.push_str("3 w S\n");
544 }
545 CheckStyle::Cross => {
546 content.push_str(&format!("{inset} {inset} m\n"));
548 content.push_str(&format!("{} {} l\n", width - inset, height - inset));
549 content.push_str(&format!("{} {inset} m\n", width - inset));
550 content.push_str(&format!("{inset} {} l\n", height - inset));
551 content.push_str("2 w S\n");
552 }
553 CheckStyle::Square => {
554 content.push_str(&format!(
556 "{inset} {inset} {} {} re f\n",
557 width - 2.0 * inset,
558 height - 2.0 * inset
559 ));
560 }
561 CheckStyle::Circle => {
562 let cx = width / 2.0;
564 let cy = height / 2.0;
565 let r = (width.min(height) - 2.0 * inset) / 2.0;
566
567 let k = 0.552284749831;
569 content.push_str(&format!("{} {} m\n", cx + r, cy));
570 content.push_str(&format!(
571 "{} {} {} {} {} {} c\n",
572 cx + r,
573 cy + k * r,
574 cx + k * r,
575 cy + r,
576 cx,
577 cy + r
578 ));
579 content.push_str(&format!(
580 "{} {} {} {} {} {} c\n",
581 cx - k * r,
582 cy + r,
583 cx - r,
584 cy + k * r,
585 cx - r,
586 cy
587 ));
588 content.push_str(&format!(
589 "{} {} {} {} {} {} c\n",
590 cx - r,
591 cy - k * r,
592 cx - k * r,
593 cy - r,
594 cx,
595 cy - r
596 ));
597 content.push_str(&format!(
598 "{} {} {} {} {} {} c\n",
599 cx + k * r,
600 cy - r,
601 cx + r,
602 cy - k * r,
603 cx + r,
604 cy
605 ));
606 content.push_str("f\n");
607 }
608 CheckStyle::Star => {
609 let cx = width / 2.0;
611 let cy = height / 2.0;
612 let r = (width.min(height) - 2.0 * inset) / 2.0;
613
614 for i in 0..5 {
616 let angle = std::f64::consts::PI * 2.0 * i as f64 / 5.0
617 - std::f64::consts::PI / 2.0;
618 let x = cx + r * angle.cos();
619 let y = cy + r * angle.sin();
620
621 if i == 0 {
622 content.push_str(&format!("{x} {y} m\n"));
623 } else {
624 content.push_str(&format!("{x} {y} l\n"));
625 }
626 }
627 content.push_str("f\n");
628 }
629 }
630 }
631
632 content.push_str("Q\n");
634
635 let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height]);
636
637 Ok(stream)
638 }
639}
640
641pub struct RadioButtonAppearance {
643 pub selected_color: Color,
645}
646
647impl Default for RadioButtonAppearance {
648 fn default() -> Self {
649 Self {
650 selected_color: Color::black(),
651 }
652 }
653}
654
655impl AppearanceGenerator for RadioButtonAppearance {
656 fn generate_appearance(
657 &self,
658 widget: &Widget,
659 value: Option<&str>,
660 _state: AppearanceState,
661 ) -> Result<AppearanceStream> {
662 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
663 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
664 let is_selected = value.is_some_and(|v| v == "Yes" || v == "On" || v == "true");
665
666 let mut content = String::new();
667
668 content.push_str("q\n");
670
671 if let Some(bg_color) = &widget.appearance.background_color {
673 crate::graphics::color::write_fill_color(&mut content, *bg_color);
674 } else {
675 crate::graphics::color::write_fill_color(&mut content, Color::Gray(1.0));
676 }
677
678 let cx = width / 2.0;
679 let cy = height / 2.0;
680 let r = width.min(height) / 2.0 - widget.appearance.border_width;
681
682 let k = 0.552284749831;
684 content.push_str(&format!("{} {} m\n", cx + r, cy));
685 content.push_str(&format!(
686 "{} {} {} {} {} {} c\n",
687 cx + r,
688 cy + k * r,
689 cx + k * r,
690 cy + r,
691 cx,
692 cy + r
693 ));
694 content.push_str(&format!(
695 "{} {} {} {} {} {} c\n",
696 cx - k * r,
697 cy + r,
698 cx - r,
699 cy + k * r,
700 cx - r,
701 cy
702 ));
703 content.push_str(&format!(
704 "{} {} {} {} {} {} c\n",
705 cx - r,
706 cy - k * r,
707 cx - k * r,
708 cy - r,
709 cx,
710 cy - r
711 ));
712 content.push_str(&format!(
713 "{} {} {} {} {} {} c\n",
714 cx + k * r,
715 cy - r,
716 cx + r,
717 cy - k * r,
718 cx + r,
719 cy
720 ));
721 content.push_str("f\n");
722
723 if let Some(border_color) = &widget.appearance.border_color {
725 crate::graphics::color::write_stroke_color(&mut content, *border_color);
726 content.push_str(&format!("{} w\n", widget.appearance.border_width));
727
728 content.push_str(&format!("{} {} m\n", cx + r, cy));
729 content.push_str(&format!(
730 "{} {} {} {} {} {} c\n",
731 cx + r,
732 cy + k * r,
733 cx + k * r,
734 cy + r,
735 cx,
736 cy + r
737 ));
738 content.push_str(&format!(
739 "{} {} {} {} {} {} c\n",
740 cx - k * r,
741 cy + r,
742 cx - r,
743 cy + k * r,
744 cx - r,
745 cy
746 ));
747 content.push_str(&format!(
748 "{} {} {} {} {} {} c\n",
749 cx - r,
750 cy - k * r,
751 cx - k * r,
752 cy - r,
753 cx,
754 cy - r
755 ));
756 content.push_str(&format!(
757 "{} {} {} {} {} {} c\n",
758 cx + k * r,
759 cy - r,
760 cx + r,
761 cy - k * r,
762 cx + r,
763 cy
764 ));
765 content.push_str("S\n");
766 }
767
768 if is_selected {
770 crate::graphics::color::write_fill_color(&mut content, self.selected_color);
771
772 let inner_r = r * 0.4;
773 content.push_str(&format!("{} {} m\n", cx + inner_r, cy));
774 content.push_str(&format!(
775 "{} {} {} {} {} {} c\n",
776 cx + inner_r,
777 cy + k * inner_r,
778 cx + k * inner_r,
779 cy + inner_r,
780 cx,
781 cy + inner_r
782 ));
783 content.push_str(&format!(
784 "{} {} {} {} {} {} c\n",
785 cx - k * inner_r,
786 cy + inner_r,
787 cx - inner_r,
788 cy + k * inner_r,
789 cx - inner_r,
790 cy
791 ));
792 content.push_str(&format!(
793 "{} {} {} {} {} {} c\n",
794 cx - inner_r,
795 cy - k * inner_r,
796 cx - k * inner_r,
797 cy - inner_r,
798 cx,
799 cy - inner_r
800 ));
801 content.push_str(&format!(
802 "{} {} {} {} {} {} c\n",
803 cx + k * inner_r,
804 cy - inner_r,
805 cx + inner_r,
806 cy - k * inner_r,
807 cx + inner_r,
808 cy
809 ));
810 content.push_str("f\n");
811 }
812
813 content.push_str("Q\n");
815
816 let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height]);
817
818 Ok(stream)
819 }
820}
821
822pub struct PushButtonAppearance {
824 pub label: String,
826 pub font: Font,
828 pub font_size: f64,
830 pub text_color: Color,
832}
833
834impl Default for PushButtonAppearance {
835 fn default() -> Self {
836 Self {
837 label: String::new(),
838 font: Font::Helvetica,
839 font_size: 12.0,
840 text_color: Color::black(),
841 }
842 }
843}
844
845impl AppearanceGenerator for PushButtonAppearance {
846 fn generate_appearance(
847 &self,
848 widget: &Widget,
849 _value: Option<&str>,
850 state: AppearanceState,
851 ) -> Result<AppearanceStream> {
852 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
853 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
854
855 let mut content = String::new();
856
857 content.push_str("q\n");
859
860 let bg_color = match state {
862 AppearanceState::Down => Color::gray(0.8),
863 AppearanceState::Rollover => Color::gray(0.95),
864 AppearanceState::Normal => widget
865 .appearance
866 .background_color
867 .unwrap_or(Color::gray(0.9)),
868 };
869
870 crate::graphics::color::write_fill_color(&mut content, bg_color);
871 content.push_str(&format!("0 0 {width} {height} re f\n"));
872
873 if matches!(widget.appearance.border_style, BorderStyle::Beveled) {
875 content.push_str("0.9 G\n");
877 content.push_str("2 w\n");
878 content.push_str(&format!("0 {height} m {width} {height} l\n"));
879 content.push_str(&format!("{width} {height} l {width} 0 l S\n"));
880
881 content.push_str("0.3 G\n");
883 content.push_str(&format!("0 0 m {width} 0 l\n"));
884 content.push_str(&format!("0 0 l 0 {height} l S\n"));
885 } else {
886 if let Some(border_color) = &widget.appearance.border_color {
888 crate::graphics::color::write_stroke_color(&mut content, *border_color);
889 content.push_str(&format!("{} w\n", widget.appearance.border_width));
890 content.push_str(&format!("0 0 {width} {height} re S\n"));
891 }
892 }
893
894 if !self.label.is_empty() {
896 crate::graphics::color::write_fill_color(&mut content, self.text_color);
897
898 content.push_str("BT\n");
899 content.push_str(&format!(
900 "/{} {} Tf\n",
901 self.font.pdf_name(),
902 self.font_size
903 ));
904
905 let text_x = width / 4.0; let text_y = (height - self.font_size) / 2.0 + self.font_size * 0.3;
908
909 content.push_str(&format!("{text_x} {text_y} Td\n"));
910
911 emit_tj_for_builtin(&mut content, &self.label, &self.font)?;
915
916 content.push_str("ET\n");
917 }
918
919 content.push_str("Q\n");
921
922 let mut resources = Dictionary::new();
924
925 let mut font_dict = Dictionary::new();
927 let mut font_res = Dictionary::new();
928 font_res.set("Type", Object::Name("Font".to_string()));
929 font_res.set("Subtype", Object::Name("Type1".to_string()));
930 font_res.set("BaseFont", Object::Name(self.font.pdf_name()));
931 font_dict.set(self.font.pdf_name(), Object::Dictionary(font_res));
932 resources.set("Font", Object::Dictionary(font_dict));
933
934 let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height])
935 .with_resources(resources);
936
937 Ok(stream)
938 }
939}
940
941#[derive(Debug, Clone)]
943pub struct ComboBoxAppearance {
944 pub font: Font,
946 pub font_size: f64,
948 pub text_color: Color,
950 pub selected_text: Option<String>,
952 pub show_arrow: bool,
954}
955
956impl Default for ComboBoxAppearance {
957 fn default() -> Self {
958 Self {
959 font: Font::Helvetica,
960 font_size: 12.0,
961 text_color: Color::black(),
962 selected_text: None,
963 show_arrow: true,
964 }
965 }
966}
967
968impl AppearanceGenerator for ComboBoxAppearance {
969 fn generate_appearance(
970 &self,
971 widget: &Widget,
972 value: Option<&str>,
973 state: AppearanceState,
974 ) -> Result<AppearanceStream> {
975 let result = self.generate_appearance_with_font(widget, value, state, None)?;
976 Ok(result.stream)
977 }
978}
979
980impl ComboBoxAppearance {
981 pub fn generate_appearance_with_font(
989 &self,
990 widget: &Widget,
991 value: Option<&str>,
992 _state: AppearanceState,
993 custom_font: Option<&crate::fonts::Font>,
994 ) -> Result<FieldAppearanceResult> {
995 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
996 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
997
998 let mut content = String::new();
999 let mut used_chars_per_font: HashMap<String, HashSet<char>> = HashMap::new();
1000
1001 crate::graphics::color::write_fill_color(&mut content, Color::white());
1003 content.push_str(&format!("0 0 {} {} re\n", width, height));
1004 content.push_str("f\n");
1005
1006 if let Some(ref border_color) = widget.appearance.border_color {
1008 crate::graphics::color::write_stroke_color(&mut content, *border_color);
1009 content.push_str(&format!("{} w\n", widget.appearance.border_width));
1010 content.push_str(&format!("0 0 {} {} re\n", width, height));
1011 content.push_str("S\n");
1012 }
1013
1014 if self.show_arrow {
1016 let arrow_x = width - 15.0;
1017 let arrow_y = height / 2.0;
1018 crate::graphics::color::write_fill_color(&mut content, Color::gray(0.5));
1019 content.push_str(&format!("{} {} m\n", arrow_x, arrow_y + 3.0));
1020 content.push_str(&format!("{} {} l\n", arrow_x + 8.0, arrow_y + 3.0));
1021 content.push_str(&format!("{} {} l\n", arrow_x + 4.0, arrow_y - 3.0));
1022 content.push_str("f\n");
1023 }
1024
1025 let text_to_show = value.or(self.selected_text.as_deref());
1027 if let Some(text) = text_to_show {
1028 content.push_str("BT\n");
1029 content.push_str(&format!(
1030 "/{} {} Tf\n",
1031 self.font.pdf_name(),
1032 self.font_size
1033 ));
1034 crate::graphics::color::write_fill_color(&mut content, self.text_color);
1035 content.push_str(&format!("5 {} Td\n", (height - self.font_size) / 2.0));
1036
1037 match (self.font.is_custom(), custom_font) {
1042 (true, Some(cf)) => {
1043 let font_name = self.font.pdf_name();
1044 let entry = used_chars_per_font.entry(font_name.clone()).or_default();
1045 emit_tj_for_custom(&mut content, text, &font_name, cf, entry)?;
1046 }
1047 (true, None) => {
1048 return Err(PdfError::EncodingError(format!(
1049 "Font {:?} is marked as Custom but was not found in the \
1050 document registry; call Document::add_font_from_bytes with \
1051 this name before fill_field/save. See issue #212.",
1052 self.font.pdf_name(),
1053 )));
1054 }
1055 (false, _) => emit_tj_for_builtin(&mut content, text, &self.font)?,
1056 }
1057 content.push_str("ET\n");
1058 }
1059
1060 let mut resources = Dictionary::new();
1063 let mut font_dict = Dictionary::new();
1064 if self.font.is_custom() {
1065 let mut placeholder = Dictionary::new();
1066 placeholder.set("Type", Object::Name("Font".to_string()));
1067 placeholder.set("Subtype", Object::Name("Type0".to_string()));
1068 placeholder.set("BaseFont", Object::Name(self.font.pdf_name()));
1069 placeholder.set("Encoding", Object::Name("Identity-H".to_string()));
1070 font_dict.set(self.font.pdf_name(), Object::Dictionary(placeholder));
1071 } else {
1072 let mut font_res = Dictionary::new();
1073 font_res.set("Type", Object::Name("Font".to_string()));
1074 font_res.set("Subtype", Object::Name("Type1".to_string()));
1075 font_res.set("BaseFont", Object::Name(self.font.pdf_name()));
1076 font_dict.set(self.font.pdf_name(), Object::Dictionary(font_res));
1077 }
1078 resources.set("Font", Object::Dictionary(font_dict));
1079
1080 let bbox = [0.0, 0.0, width, height];
1081 let stream = AppearanceStream::new(content.into_bytes(), bbox).with_resources(resources);
1082 Ok(FieldAppearanceResult {
1083 stream,
1084 used_chars_by_font: used_chars_per_font,
1085 })
1086 }
1087}
1088
1089#[derive(Debug, Clone)]
1091pub struct ListBoxAppearance {
1092 pub font: Font,
1094 pub font_size: f64,
1096 pub text_color: Color,
1098 pub selection_color: Color,
1100 pub options: Vec<String>,
1102 pub selected: Vec<usize>,
1104 pub item_height: f64,
1106}
1107
1108impl Default for ListBoxAppearance {
1109 fn default() -> Self {
1110 Self {
1111 font: Font::Helvetica,
1112 font_size: 12.0,
1113 text_color: Color::black(),
1114 selection_color: Color::rgb(0.2, 0.4, 0.8),
1115 options: Vec::new(),
1116 selected: Vec::new(),
1117 item_height: 16.0,
1118 }
1119 }
1120}
1121
1122impl AppearanceGenerator for ListBoxAppearance {
1123 fn generate_appearance(
1124 &self,
1125 widget: &Widget,
1126 _value: Option<&str>,
1127 _state: AppearanceState,
1128 ) -> Result<AppearanceStream> {
1129 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
1130 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
1131
1132 let mut content = String::new();
1133
1134 crate::graphics::color::write_fill_color(&mut content, Color::white());
1136 content.push_str(&format!("0 0 {} {} re\n", width, height));
1137 content.push_str("f\n");
1138
1139 if let Some(ref border_color) = widget.appearance.border_color {
1141 crate::graphics::color::write_stroke_color(&mut content, *border_color);
1142 content.push_str(&format!("{} w\n", widget.appearance.border_width));
1143 content.push_str(&format!("0 0 {} {} re\n", width, height));
1144 content.push_str("S\n");
1145 }
1146
1147 let mut y = height - self.item_height;
1149 for (index, option) in self.options.iter().enumerate() {
1150 if y < 0.0 {
1151 break; }
1153
1154 if self.selected.contains(&index) {
1156 crate::graphics::color::write_fill_color(&mut content, self.selection_color);
1157 content.push_str(&format!("0 {} {} {} re\n", y, width, self.item_height));
1158 content.push_str("f\n");
1159 }
1160
1161 content.push_str("BT\n");
1163 content.push_str(&format!(
1164 "/{} {} Tf\n",
1165 self.font.pdf_name(),
1166 self.font_size
1167 ));
1168
1169 if self.selected.contains(&index) {
1171 crate::graphics::color::write_fill_color(&mut content, Color::white());
1172 } else {
1173 crate::graphics::color::write_fill_color(&mut content, self.text_color);
1174 }
1175
1176 content.push_str(&format!("5 {} Td\n", y + 2.0));
1177
1178 emit_tj_for_builtin(&mut content, option, &self.font)?;
1179 content.push_str("ET\n");
1180
1181 y -= self.item_height;
1182 }
1183
1184 let bbox = [0.0, 0.0, width, height];
1185 Ok(AppearanceStream::new(content.into_bytes(), bbox))
1186 }
1187}
1188
1189pub fn generate_default_appearance(
1197 field_type: FieldType,
1198 widget: &Widget,
1199 value: Option<&str>,
1200) -> Result<AppearanceStream> {
1201 Ok(generate_field_appearance(field_type, widget, value, None, None)?.stream)
1202}
1203
1204pub fn generate_field_appearance(
1223 field_type: FieldType,
1224 widget: &Widget,
1225 value: Option<&str>,
1226 default_appearance: Option<&DefaultAppearance>,
1227 custom_font: Option<&crate::fonts::Font>,
1228) -> Result<FieldAppearanceResult> {
1229 match field_type {
1230 FieldType::Text => {
1231 let mut generator = TextFieldAppearance::default();
1232 if let Some(da) = default_appearance {
1233 generator.font = da.font.clone();
1234 generator.font_size = da.font_size;
1235 generator.text_color = da.color.clone();
1236 }
1237 generator.generate_appearance_with_font(
1238 widget,
1239 value,
1240 AppearanceState::Normal,
1241 custom_font,
1242 )
1243 }
1244 FieldType::Button => {
1245 let generator = CheckBoxAppearance::default();
1248 let stream = generator.generate_appearance(widget, value, AppearanceState::Normal)?;
1249 Ok(FieldAppearanceResult {
1250 stream,
1251 used_chars_by_font: HashMap::new(),
1252 })
1253 }
1254 FieldType::Choice => {
1255 let mut generator = ComboBoxAppearance::default();
1256 if let Some(da) = default_appearance {
1257 generator.font = da.font.clone();
1258 generator.font_size = da.font_size;
1259 generator.text_color = da.color.clone();
1260 }
1261 generator.generate_appearance_with_font(
1262 widget,
1263 value,
1264 AppearanceState::Normal,
1265 custom_font,
1266 )
1267 }
1268 FieldType::Signature => {
1269 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
1270 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
1271 Ok(FieldAppearanceResult {
1272 stream: AppearanceStream::new(b"q\nQ\n".to_vec(), [0.0, 0.0, width, height]),
1273 used_chars_by_font: HashMap::new(),
1274 })
1275 }
1276 }
1277}
1278
1279#[cfg(test)]
1280mod tests {
1281 use super::*;
1282 use crate::geometry::{Point, Rectangle};
1283
1284 #[test]
1285 fn test_appearance_state_names() {
1286 assert_eq!(AppearanceState::Normal.pdf_name(), "N");
1287 assert_eq!(AppearanceState::Rollover.pdf_name(), "R");
1288 assert_eq!(AppearanceState::Down.pdf_name(), "D");
1289 }
1290
1291 #[test]
1292 fn test_appearance_stream_creation() {
1293 let content = b"q\n1 0 0 RG\n0 0 100 50 re S\nQ\n";
1294 let stream = AppearanceStream::new(content.to_vec(), [0.0, 0.0, 100.0, 50.0]);
1295
1296 assert_eq!(stream.content, content);
1297 assert_eq!(stream.bbox, [0.0, 0.0, 100.0, 50.0]);
1298 assert!(stream.resources.is_empty());
1299 }
1300
1301 #[test]
1302 fn test_appearance_stream_with_resources() {
1303 let mut resources = Dictionary::new();
1304 resources.set("Font", Object::Name("F1".to_string()));
1305
1306 let content = b"BT\n/F1 12 Tf\n(Test) Tj\nET\n";
1307 let stream = AppearanceStream::new(content.to_vec(), [0.0, 0.0, 100.0, 50.0])
1308 .with_resources(resources.clone());
1309
1310 assert_eq!(stream.resources, resources);
1311 }
1312
1313 #[test]
1314 fn test_appearance_dictionary() {
1315 let mut app_dict = AppearanceDictionary::new();
1316
1317 let normal_stream = AppearanceStream::new(b"normal".to_vec(), [0.0, 0.0, 10.0, 10.0]);
1318 let down_stream = AppearanceStream::new(b"down".to_vec(), [0.0, 0.0, 10.0, 10.0]);
1319
1320 app_dict.set_appearance(AppearanceState::Normal, normal_stream);
1321 app_dict.set_appearance(AppearanceState::Down, down_stream);
1322
1323 assert!(app_dict.get_appearance(AppearanceState::Normal).is_some());
1324 assert!(app_dict.get_appearance(AppearanceState::Down).is_some());
1325 assert!(app_dict.get_appearance(AppearanceState::Rollover).is_none());
1326 }
1327
1328 #[test]
1329 fn test_text_field_appearance() {
1330 let widget = Widget::new(Rectangle {
1331 lower_left: Point { x: 0.0, y: 0.0 },
1332 upper_right: Point { x: 200.0, y: 30.0 },
1333 });
1334
1335 let generator = TextFieldAppearance::default();
1336 let result =
1337 generator.generate_appearance(&widget, Some("Test Text"), AppearanceState::Normal);
1338
1339 assert!(result.is_ok());
1340 let stream = result.unwrap();
1341 assert_eq!(stream.bbox, [0.0, 0.0, 200.0, 30.0]);
1342
1343 let content = String::from_utf8_lossy(&stream.content);
1344 assert!(content.contains("BT"));
1345 assert!(content.contains("(Test Text) Tj"));
1346 assert!(content.contains("ET"));
1347 }
1348
1349 #[test]
1350 fn test_checkbox_appearance_checked() {
1351 let widget = Widget::new(Rectangle {
1352 lower_left: Point { x: 0.0, y: 0.0 },
1353 upper_right: Point { x: 20.0, y: 20.0 },
1354 });
1355
1356 let generator = CheckBoxAppearance::default();
1357 let result = generator.generate_appearance(&widget, Some("Yes"), AppearanceState::Normal);
1358
1359 assert!(result.is_ok());
1360 let stream = result.unwrap();
1361 let content = String::from_utf8_lossy(&stream.content);
1362
1363 assert!(content.contains(" m"));
1365 assert!(content.contains(" l"));
1366 assert!(content.contains(" S"));
1367 }
1368
1369 #[test]
1370 fn test_checkbox_appearance_unchecked() {
1371 let widget = Widget::new(Rectangle {
1372 lower_left: Point { x: 0.0, y: 0.0 },
1373 upper_right: Point { x: 20.0, y: 20.0 },
1374 });
1375
1376 let generator = CheckBoxAppearance::default();
1377 let result = generator.generate_appearance(&widget, Some("No"), AppearanceState::Normal);
1378
1379 assert!(result.is_ok());
1380 let stream = result.unwrap();
1381 let content = String::from_utf8_lossy(&stream.content);
1382
1383 assert!(content.contains("q"));
1385 assert!(content.contains("Q"));
1386 }
1387
1388 #[test]
1389 fn test_radio_button_appearance() {
1390 let widget = Widget::new(Rectangle {
1391 lower_left: Point { x: 0.0, y: 0.0 },
1392 upper_right: Point { x: 20.0, y: 20.0 },
1393 });
1394
1395 let generator = RadioButtonAppearance::default();
1396 let result = generator.generate_appearance(&widget, Some("Yes"), AppearanceState::Normal);
1397
1398 assert!(result.is_ok());
1399 let stream = result.unwrap();
1400 let content = String::from_utf8_lossy(&stream.content);
1401
1402 assert!(
1404 content.contains(" c"),
1405 "Content should contain curve commands"
1406 );
1407 assert!(
1408 content.contains("f\n"),
1409 "Content should contain fill commands"
1410 );
1411 }
1412
1413 #[test]
1414 fn test_push_button_appearance() {
1415 let mut generator = PushButtonAppearance::default();
1416 generator.label = "Click Me".to_string();
1417
1418 let widget = Widget::new(Rectangle {
1419 lower_left: Point { x: 0.0, y: 0.0 },
1420 upper_right: Point { x: 100.0, y: 30.0 },
1421 });
1422
1423 let result = generator.generate_appearance(&widget, None, AppearanceState::Normal);
1424
1425 assert!(result.is_ok());
1426 let stream = result.unwrap();
1427 let content = String::from_utf8_lossy(&stream.content);
1428
1429 assert!(content.contains("(Click Me) Tj"));
1430 assert!(!stream.resources.is_empty());
1431 }
1432
1433 #[test]
1434 fn test_push_button_states() {
1435 let generator = PushButtonAppearance::default();
1436 let widget = Widget::new(Rectangle {
1437 lower_left: Point { x: 0.0, y: 0.0 },
1438 upper_right: Point { x: 100.0, y: 30.0 },
1439 });
1440
1441 let normal = generator
1443 .generate_appearance(&widget, None, AppearanceState::Normal)
1444 .unwrap();
1445 let down = generator
1446 .generate_appearance(&widget, None, AppearanceState::Down)
1447 .unwrap();
1448 let rollover = generator
1449 .generate_appearance(&widget, None, AppearanceState::Rollover)
1450 .unwrap();
1451
1452 assert_ne!(normal.content, down.content);
1454 assert_ne!(normal.content, rollover.content);
1455 assert_ne!(down.content, rollover.content);
1456 }
1457
1458 #[test]
1459 fn test_check_styles() {
1460 let widget = Widget::new(Rectangle {
1461 lower_left: Point { x: 0.0, y: 0.0 },
1462 upper_right: Point { x: 20.0, y: 20.0 },
1463 });
1464
1465 for style in [
1467 CheckStyle::Check,
1468 CheckStyle::Cross,
1469 CheckStyle::Square,
1470 CheckStyle::Circle,
1471 CheckStyle::Star,
1472 ] {
1473 let mut generator = CheckBoxAppearance::default();
1474 generator.check_style = style;
1475
1476 let result =
1477 generator.generate_appearance(&widget, Some("Yes"), AppearanceState::Normal);
1478
1479 assert!(result.is_ok(), "Failed for style {:?}", style);
1480 }
1481 }
1482
1483 #[test]
1484 fn test_appearance_state_pdf_names() {
1485 assert_eq!(AppearanceState::Normal.pdf_name(), "N");
1486 assert_eq!(AppearanceState::Rollover.pdf_name(), "R");
1487 assert_eq!(AppearanceState::Down.pdf_name(), "D");
1488 }
1489
1490 #[test]
1491 fn test_appearance_stream_creation_advanced() {
1492 let content = b"q 1 0 0 1 0 0 cm Q".to_vec();
1493 let bbox = [0.0, 0.0, 100.0, 50.0];
1494 let stream = AppearanceStream::new(content.clone(), bbox);
1495
1496 assert_eq!(stream.content, content);
1497 assert_eq!(stream.bbox, bbox);
1498 assert!(stream.resources.is_empty());
1499 }
1500
1501 #[test]
1502 fn test_appearance_stream_with_resources_advanced() {
1503 let mut resources = Dictionary::new();
1504 resources.set("Font", Object::Dictionary(Dictionary::new()));
1505
1506 let stream =
1507 AppearanceStream::new(vec![], [0.0, 0.0, 10.0, 10.0]).with_resources(resources.clone());
1508
1509 assert_eq!(stream.resources, resources);
1510 }
1511
1512 #[test]
1513 fn test_appearance_dictionary_new() {
1514 let dict = AppearanceDictionary::new();
1515 assert!(dict.appearances.is_empty());
1516 assert!(dict.down_appearances.is_empty());
1517 }
1518
1519 #[test]
1520 fn test_appearance_dictionary_set_get() {
1521 let mut dict = AppearanceDictionary::new();
1522 let stream = AppearanceStream::new(vec![1, 2, 3], [0.0, 0.0, 10.0, 10.0]);
1523
1524 dict.set_appearance(AppearanceState::Normal, stream);
1525 assert!(dict.get_appearance(AppearanceState::Normal).is_some());
1526 assert!(dict.get_appearance(AppearanceState::Down).is_none());
1527 }
1528
1529 #[test]
1530 fn test_text_field_multiline() {
1531 let mut generator = TextFieldAppearance::default();
1532 generator.multiline = true;
1533
1534 let widget = Widget::new(Rectangle {
1535 lower_left: Point { x: 0.0, y: 0.0 },
1536 upper_right: Point { x: 200.0, y: 100.0 },
1537 });
1538
1539 let text = "Line 1\nLine 2\nLine 3";
1540 let result = generator.generate_appearance(&widget, Some(text), AppearanceState::Normal);
1541 assert!(result.is_ok());
1542 }
1543
1544 #[test]
1545 fn test_appearance_with_custom_colors() {
1546 let mut generator = TextFieldAppearance::default();
1547 generator.text_color = Color::rgb(1.0, 0.0, 0.0); generator.font_size = 14.0;
1549 generator.justification = 1; let widget = Widget::new(Rectangle {
1552 lower_left: Point { x: 0.0, y: 0.0 },
1553 upper_right: Point { x: 100.0, y: 30.0 },
1554 });
1555
1556 let result =
1557 generator.generate_appearance(&widget, Some("Colored"), AppearanceState::Normal);
1558 assert!(result.is_ok());
1559 }
1560}