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() && !self.font.is_custom() {
900 crate::graphics::color::write_fill_color(&mut content, self.text_color);
901
902 content.push_str("BT\n");
903 content.push_str(&format!(
904 "/{} {} Tf\n",
905 self.font.pdf_name(),
906 self.font_size
907 ));
908
909 let text_x = width / 4.0; let text_y = (height - self.font_size) / 2.0 + self.font_size * 0.3;
912
913 content.push_str(&format!("{text_x} {text_y} Td\n"));
914
915 emit_tj_for_builtin(&mut content, &self.label, &self.font)?;
919
920 content.push_str("ET\n");
921 }
922
923 content.push_str("Q\n");
925
926 let mut resources = Dictionary::new();
928
929 let mut font_dict = Dictionary::new();
936 if self.font.is_custom() {
937 let mut placeholder = Dictionary::new();
938 placeholder.set("Type", Object::Name("Font".to_string()));
939 placeholder.set("Subtype", Object::Name("Type0".to_string()));
940 placeholder.set("BaseFont", Object::Name(self.font.pdf_name()));
941 placeholder.set("Encoding", Object::Name("Identity-H".to_string()));
942 font_dict.set(self.font.pdf_name(), Object::Dictionary(placeholder));
943 } else {
944 let mut font_res = Dictionary::new();
945 font_res.set("Type", Object::Name("Font".to_string()));
946 font_res.set("Subtype", Object::Name("Type1".to_string()));
947 font_res.set("BaseFont", Object::Name(self.font.pdf_name()));
948 font_dict.set(self.font.pdf_name(), Object::Dictionary(font_res));
949 }
950 resources.set("Font", Object::Dictionary(font_dict));
951
952 let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height])
953 .with_resources(resources);
954
955 Ok(stream)
956 }
957}
958
959#[derive(Debug, Clone)]
961pub struct ComboBoxAppearance {
962 pub font: Font,
964 pub font_size: f64,
966 pub text_color: Color,
968 pub selected_text: Option<String>,
970 pub show_arrow: bool,
972}
973
974impl Default for ComboBoxAppearance {
975 fn default() -> Self {
976 Self {
977 font: Font::Helvetica,
978 font_size: 12.0,
979 text_color: Color::black(),
980 selected_text: None,
981 show_arrow: true,
982 }
983 }
984}
985
986impl AppearanceGenerator for ComboBoxAppearance {
987 fn generate_appearance(
988 &self,
989 widget: &Widget,
990 value: Option<&str>,
991 state: AppearanceState,
992 ) -> Result<AppearanceStream> {
993 let result = self.generate_appearance_with_font(widget, value, state, None)?;
994 Ok(result.stream)
995 }
996}
997
998impl ComboBoxAppearance {
999 pub fn generate_appearance_with_font(
1007 &self,
1008 widget: &Widget,
1009 value: Option<&str>,
1010 _state: AppearanceState,
1011 custom_font: Option<&crate::fonts::Font>,
1012 ) -> Result<FieldAppearanceResult> {
1013 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
1014 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
1015
1016 let mut content = String::new();
1017 let mut used_chars_per_font: HashMap<String, HashSet<char>> = HashMap::new();
1018
1019 crate::graphics::color::write_fill_color(&mut content, Color::white());
1021 content.push_str(&format!("0 0 {} {} re\n", width, height));
1022 content.push_str("f\n");
1023
1024 if let Some(ref border_color) = widget.appearance.border_color {
1026 crate::graphics::color::write_stroke_color(&mut content, *border_color);
1027 content.push_str(&format!("{} w\n", widget.appearance.border_width));
1028 content.push_str(&format!("0 0 {} {} re\n", width, height));
1029 content.push_str("S\n");
1030 }
1031
1032 if self.show_arrow {
1034 let arrow_x = width - 15.0;
1035 let arrow_y = height / 2.0;
1036 crate::graphics::color::write_fill_color(&mut content, Color::gray(0.5));
1037 content.push_str(&format!("{} {} m\n", arrow_x, arrow_y + 3.0));
1038 content.push_str(&format!("{} {} l\n", arrow_x + 8.0, arrow_y + 3.0));
1039 content.push_str(&format!("{} {} l\n", arrow_x + 4.0, arrow_y - 3.0));
1040 content.push_str("f\n");
1041 }
1042
1043 let text_to_show = value.or(self.selected_text.as_deref());
1045 if let Some(text) = text_to_show {
1046 content.push_str("BT\n");
1047 content.push_str(&format!(
1048 "/{} {} Tf\n",
1049 self.font.pdf_name(),
1050 self.font_size
1051 ));
1052 crate::graphics::color::write_fill_color(&mut content, self.text_color);
1053 content.push_str(&format!("5 {} Td\n", (height - self.font_size) / 2.0));
1054
1055 match (self.font.is_custom(), custom_font) {
1060 (true, Some(cf)) => {
1061 let font_name = self.font.pdf_name();
1062 let entry = used_chars_per_font.entry(font_name.clone()).or_default();
1063 emit_tj_for_custom(&mut content, text, &font_name, cf, entry)?;
1064 }
1065 (true, None) => {
1066 return Err(PdfError::EncodingError(format!(
1067 "Font {:?} is marked as Custom but was not found in the \
1068 document registry; call Document::add_font_from_bytes with \
1069 this name before fill_field/save. See issue #212.",
1070 self.font.pdf_name(),
1071 )));
1072 }
1073 (false, _) => emit_tj_for_builtin(&mut content, text, &self.font)?,
1074 }
1075 content.push_str("ET\n");
1076 }
1077
1078 let mut resources = Dictionary::new();
1081 let mut font_dict = Dictionary::new();
1082 if self.font.is_custom() {
1083 let mut placeholder = Dictionary::new();
1084 placeholder.set("Type", Object::Name("Font".to_string()));
1085 placeholder.set("Subtype", Object::Name("Type0".to_string()));
1086 placeholder.set("BaseFont", Object::Name(self.font.pdf_name()));
1087 placeholder.set("Encoding", Object::Name("Identity-H".to_string()));
1088 font_dict.set(self.font.pdf_name(), Object::Dictionary(placeholder));
1089 } else {
1090 let mut font_res = Dictionary::new();
1091 font_res.set("Type", Object::Name("Font".to_string()));
1092 font_res.set("Subtype", Object::Name("Type1".to_string()));
1093 font_res.set("BaseFont", Object::Name(self.font.pdf_name()));
1094 font_dict.set(self.font.pdf_name(), Object::Dictionary(font_res));
1095 }
1096 resources.set("Font", Object::Dictionary(font_dict));
1097
1098 let bbox = [0.0, 0.0, width, height];
1099 let stream = AppearanceStream::new(content.into_bytes(), bbox).with_resources(resources);
1100 Ok(FieldAppearanceResult {
1101 stream,
1102 used_chars_by_font: used_chars_per_font,
1103 })
1104 }
1105}
1106
1107#[derive(Debug, Clone)]
1109pub struct ListBoxAppearance {
1110 pub font: Font,
1112 pub font_size: f64,
1114 pub text_color: Color,
1116 pub selection_color: Color,
1118 pub options: Vec<String>,
1120 pub selected: Vec<usize>,
1122 pub item_height: f64,
1124}
1125
1126impl Default for ListBoxAppearance {
1127 fn default() -> Self {
1128 Self {
1129 font: Font::Helvetica,
1130 font_size: 12.0,
1131 text_color: Color::black(),
1132 selection_color: Color::rgb(0.2, 0.4, 0.8),
1133 options: Vec::new(),
1134 selected: Vec::new(),
1135 item_height: 16.0,
1136 }
1137 }
1138}
1139
1140impl AppearanceGenerator for ListBoxAppearance {
1141 fn generate_appearance(
1142 &self,
1143 widget: &Widget,
1144 value: Option<&str>,
1145 state: AppearanceState,
1146 ) -> Result<AppearanceStream> {
1147 Ok(self
1148 .generate_appearance_with_font(widget, value, state, None)?
1149 .stream)
1150 }
1151}
1152
1153impl ListBoxAppearance {
1154 pub fn generate_appearance_with_font(
1169 &self,
1170 widget: &Widget,
1171 _value: Option<&str>,
1172 _state: AppearanceState,
1173 custom_font: Option<&crate::fonts::Font>,
1174 ) -> Result<FieldAppearanceResult> {
1175 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
1176 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
1177
1178 let mut content = String::new();
1179 let mut used_chars_per_font: HashMap<String, HashSet<char>> = HashMap::new();
1180
1181 crate::graphics::color::write_fill_color(&mut content, Color::white());
1183 content.push_str(&format!("0 0 {} {} re\n", width, height));
1184 content.push_str("f\n");
1185
1186 if let Some(ref border_color) = widget.appearance.border_color {
1188 crate::graphics::color::write_stroke_color(&mut content, *border_color);
1189 content.push_str(&format!("{} w\n", widget.appearance.border_width));
1190 content.push_str(&format!("0 0 {} {} re\n", width, height));
1191 content.push_str("S\n");
1192 }
1193
1194 let mut y = height - self.item_height;
1196 for (index, option) in self.options.iter().enumerate() {
1197 if y < 0.0 {
1198 break; }
1200
1201 if self.selected.contains(&index) {
1203 crate::graphics::color::write_fill_color(&mut content, self.selection_color);
1204 content.push_str(&format!("0 {} {} {} re\n", y, width, self.item_height));
1205 content.push_str("f\n");
1206 }
1207
1208 content.push_str("BT\n");
1210 content.push_str(&format!(
1211 "/{} {} Tf\n",
1212 self.font.pdf_name(),
1213 self.font_size
1214 ));
1215
1216 if self.selected.contains(&index) {
1218 crate::graphics::color::write_fill_color(&mut content, Color::white());
1219 } else {
1220 crate::graphics::color::write_fill_color(&mut content, self.text_color);
1221 }
1222
1223 content.push_str(&format!("5 {} Td\n", y + 2.0));
1224
1225 match (self.font.is_custom(), custom_font) {
1227 (true, Some(cf)) => {
1228 let font_name = self.font.pdf_name();
1229 let entry = used_chars_per_font.entry(font_name.clone()).or_default();
1230 emit_tj_for_custom(&mut content, option, &font_name, cf, entry)?;
1231 }
1232 (true, None) => {
1233 return Err(PdfError::EncodingError(format!(
1234 "Font {:?} is marked as Custom but was not found in the \
1235 document registry; call Document::add_font_from_bytes with \
1236 this name before fill_field/save. See issue #212.",
1237 self.font.pdf_name(),
1238 )));
1239 }
1240 (false, _) => emit_tj_for_builtin(&mut content, option, &self.font)?,
1241 }
1242
1243 content.push_str("ET\n");
1244
1245 y -= self.item_height;
1246 }
1247
1248 let mut resources = Dictionary::new();
1250 let mut font_dict = Dictionary::new();
1251 if self.font.is_custom() {
1252 let mut placeholder = Dictionary::new();
1253 placeholder.set("Type", Object::Name("Font".to_string()));
1254 placeholder.set("Subtype", Object::Name("Type0".to_string()));
1255 placeholder.set("BaseFont", Object::Name(self.font.pdf_name()));
1256 placeholder.set("Encoding", Object::Name("Identity-H".to_string()));
1257 font_dict.set(self.font.pdf_name(), Object::Dictionary(placeholder));
1258 } else {
1259 let mut font_res = Dictionary::new();
1260 font_res.set("Type", Object::Name("Font".to_string()));
1261 font_res.set("Subtype", Object::Name("Type1".to_string()));
1262 font_res.set("BaseFont", Object::Name(self.font.pdf_name()));
1263 font_dict.set(self.font.pdf_name(), Object::Dictionary(font_res));
1264 }
1265 resources.set("Font", Object::Dictionary(font_dict));
1266
1267 let bbox = [0.0, 0.0, width, height];
1268 let stream = AppearanceStream::new(content.into_bytes(), bbox).with_resources(resources);
1269 Ok(FieldAppearanceResult {
1270 stream,
1271 used_chars_by_font: used_chars_per_font,
1272 })
1273 }
1274}
1275
1276pub fn generate_default_appearance(
1284 field_type: FieldType,
1285 widget: &Widget,
1286 value: Option<&str>,
1287) -> Result<AppearanceStream> {
1288 Ok(generate_field_appearance(field_type, widget, value, None, None)?.stream)
1289}
1290
1291pub fn generate_field_appearance(
1310 field_type: FieldType,
1311 widget: &Widget,
1312 value: Option<&str>,
1313 default_appearance: Option<&DefaultAppearance>,
1314 custom_font: Option<&crate::fonts::Font>,
1315) -> Result<FieldAppearanceResult> {
1316 match field_type {
1317 FieldType::Text => {
1318 let mut generator = TextFieldAppearance::default();
1319 if let Some(da) = default_appearance {
1320 generator.font = da.font.clone();
1321 generator.font_size = da.font_size;
1322 generator.text_color = da.color.clone();
1323 }
1324 generator.generate_appearance_with_font(
1325 widget,
1326 value,
1327 AppearanceState::Normal,
1328 custom_font,
1329 )
1330 }
1331 FieldType::Button => {
1332 let generator = CheckBoxAppearance::default();
1335 let stream = generator.generate_appearance(widget, value, AppearanceState::Normal)?;
1336 Ok(FieldAppearanceResult {
1337 stream,
1338 used_chars_by_font: HashMap::new(),
1339 })
1340 }
1341 FieldType::Choice => {
1342 let mut generator = ComboBoxAppearance::default();
1343 if let Some(da) = default_appearance {
1344 generator.font = da.font.clone();
1345 generator.font_size = da.font_size;
1346 generator.text_color = da.color.clone();
1347 }
1348 generator.generate_appearance_with_font(
1349 widget,
1350 value,
1351 AppearanceState::Normal,
1352 custom_font,
1353 )
1354 }
1355 FieldType::Signature => {
1356 let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
1357 let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
1358 Ok(FieldAppearanceResult {
1359 stream: AppearanceStream::new(b"q\nQ\n".to_vec(), [0.0, 0.0, width, height]),
1360 used_chars_by_font: HashMap::new(),
1361 })
1362 }
1363 }
1364}
1365
1366#[cfg(test)]
1367mod tests {
1368 use super::*;
1369 use crate::geometry::{Point, Rectangle};
1370
1371 #[test]
1372 fn test_appearance_state_names() {
1373 assert_eq!(AppearanceState::Normal.pdf_name(), "N");
1374 assert_eq!(AppearanceState::Rollover.pdf_name(), "R");
1375 assert_eq!(AppearanceState::Down.pdf_name(), "D");
1376 }
1377
1378 #[test]
1379 fn test_appearance_stream_creation() {
1380 let content = b"q\n1 0 0 RG\n0 0 100 50 re S\nQ\n";
1381 let stream = AppearanceStream::new(content.to_vec(), [0.0, 0.0, 100.0, 50.0]);
1382
1383 assert_eq!(stream.content, content);
1384 assert_eq!(stream.bbox, [0.0, 0.0, 100.0, 50.0]);
1385 assert!(stream.resources.is_empty());
1386 }
1387
1388 #[test]
1389 fn test_appearance_stream_with_resources() {
1390 let mut resources = Dictionary::new();
1391 resources.set("Font", Object::Name("F1".to_string()));
1392
1393 let content = b"BT\n/F1 12 Tf\n(Test) Tj\nET\n";
1394 let stream = AppearanceStream::new(content.to_vec(), [0.0, 0.0, 100.0, 50.0])
1395 .with_resources(resources.clone());
1396
1397 assert_eq!(stream.resources, resources);
1398 }
1399
1400 #[test]
1401 fn test_appearance_dictionary() {
1402 let mut app_dict = AppearanceDictionary::new();
1403
1404 let normal_stream = AppearanceStream::new(b"normal".to_vec(), [0.0, 0.0, 10.0, 10.0]);
1405 let down_stream = AppearanceStream::new(b"down".to_vec(), [0.0, 0.0, 10.0, 10.0]);
1406
1407 app_dict.set_appearance(AppearanceState::Normal, normal_stream);
1408 app_dict.set_appearance(AppearanceState::Down, down_stream);
1409
1410 assert!(app_dict.get_appearance(AppearanceState::Normal).is_some());
1411 assert!(app_dict.get_appearance(AppearanceState::Down).is_some());
1412 assert!(app_dict.get_appearance(AppearanceState::Rollover).is_none());
1413 }
1414
1415 #[test]
1416 fn test_text_field_appearance() {
1417 let widget = Widget::new(Rectangle {
1418 lower_left: Point { x: 0.0, y: 0.0 },
1419 upper_right: Point { x: 200.0, y: 30.0 },
1420 });
1421
1422 let generator = TextFieldAppearance::default();
1423 let result =
1424 generator.generate_appearance(&widget, Some("Test Text"), AppearanceState::Normal);
1425
1426 assert!(result.is_ok());
1427 let stream = result.unwrap();
1428 assert_eq!(stream.bbox, [0.0, 0.0, 200.0, 30.0]);
1429
1430 let content = String::from_utf8_lossy(&stream.content);
1431 assert!(content.contains("BT"));
1432 assert!(content.contains("(Test Text) Tj"));
1433 assert!(content.contains("ET"));
1434 }
1435
1436 #[test]
1437 fn test_checkbox_appearance_checked() {
1438 let widget = Widget::new(Rectangle {
1439 lower_left: Point { x: 0.0, y: 0.0 },
1440 upper_right: Point { x: 20.0, y: 20.0 },
1441 });
1442
1443 let generator = CheckBoxAppearance::default();
1444 let result = generator.generate_appearance(&widget, Some("Yes"), AppearanceState::Normal);
1445
1446 assert!(result.is_ok());
1447 let stream = result.unwrap();
1448 let content = String::from_utf8_lossy(&stream.content);
1449
1450 assert!(content.contains(" m"));
1452 assert!(content.contains(" l"));
1453 assert!(content.contains(" S"));
1454 }
1455
1456 #[test]
1457 fn test_checkbox_appearance_unchecked() {
1458 let widget = Widget::new(Rectangle {
1459 lower_left: Point { x: 0.0, y: 0.0 },
1460 upper_right: Point { x: 20.0, y: 20.0 },
1461 });
1462
1463 let generator = CheckBoxAppearance::default();
1464 let result = generator.generate_appearance(&widget, Some("No"), AppearanceState::Normal);
1465
1466 assert!(result.is_ok());
1467 let stream = result.unwrap();
1468 let content = String::from_utf8_lossy(&stream.content);
1469
1470 assert!(content.contains("q"));
1472 assert!(content.contains("Q"));
1473 }
1474
1475 #[test]
1476 fn test_radio_button_appearance() {
1477 let widget = Widget::new(Rectangle {
1478 lower_left: Point { x: 0.0, y: 0.0 },
1479 upper_right: Point { x: 20.0, y: 20.0 },
1480 });
1481
1482 let generator = RadioButtonAppearance::default();
1483 let result = generator.generate_appearance(&widget, Some("Yes"), AppearanceState::Normal);
1484
1485 assert!(result.is_ok());
1486 let stream = result.unwrap();
1487 let content = String::from_utf8_lossy(&stream.content);
1488
1489 assert!(
1491 content.contains(" c"),
1492 "Content should contain curve commands"
1493 );
1494 assert!(
1495 content.contains("f\n"),
1496 "Content should contain fill commands"
1497 );
1498 }
1499
1500 #[test]
1501 fn test_push_button_appearance() {
1502 let mut generator = PushButtonAppearance::default();
1503 generator.label = "Click Me".to_string();
1504
1505 let widget = Widget::new(Rectangle {
1506 lower_left: Point { x: 0.0, y: 0.0 },
1507 upper_right: Point { x: 100.0, y: 30.0 },
1508 });
1509
1510 let result = generator.generate_appearance(&widget, None, AppearanceState::Normal);
1511
1512 assert!(result.is_ok());
1513 let stream = result.unwrap();
1514 let content = String::from_utf8_lossy(&stream.content);
1515
1516 assert!(content.contains("(Click Me) Tj"));
1517 assert!(!stream.resources.is_empty());
1518 }
1519
1520 #[test]
1521 fn test_push_button_states() {
1522 let generator = PushButtonAppearance::default();
1523 let widget = Widget::new(Rectangle {
1524 lower_left: Point { x: 0.0, y: 0.0 },
1525 upper_right: Point { x: 100.0, y: 30.0 },
1526 });
1527
1528 let normal = generator
1530 .generate_appearance(&widget, None, AppearanceState::Normal)
1531 .unwrap();
1532 let down = generator
1533 .generate_appearance(&widget, None, AppearanceState::Down)
1534 .unwrap();
1535 let rollover = generator
1536 .generate_appearance(&widget, None, AppearanceState::Rollover)
1537 .unwrap();
1538
1539 assert_ne!(normal.content, down.content);
1541 assert_ne!(normal.content, rollover.content);
1542 assert_ne!(down.content, rollover.content);
1543 }
1544
1545 #[test]
1546 fn test_check_styles() {
1547 let widget = Widget::new(Rectangle {
1548 lower_left: Point { x: 0.0, y: 0.0 },
1549 upper_right: Point { x: 20.0, y: 20.0 },
1550 });
1551
1552 for style in [
1554 CheckStyle::Check,
1555 CheckStyle::Cross,
1556 CheckStyle::Square,
1557 CheckStyle::Circle,
1558 CheckStyle::Star,
1559 ] {
1560 let mut generator = CheckBoxAppearance::default();
1561 generator.check_style = style;
1562
1563 let result =
1564 generator.generate_appearance(&widget, Some("Yes"), AppearanceState::Normal);
1565
1566 assert!(result.is_ok(), "Failed for style {:?}", style);
1567 }
1568 }
1569
1570 #[test]
1571 fn test_appearance_state_pdf_names() {
1572 assert_eq!(AppearanceState::Normal.pdf_name(), "N");
1573 assert_eq!(AppearanceState::Rollover.pdf_name(), "R");
1574 assert_eq!(AppearanceState::Down.pdf_name(), "D");
1575 }
1576
1577 #[test]
1578 fn test_appearance_stream_creation_advanced() {
1579 let content = b"q 1 0 0 1 0 0 cm Q".to_vec();
1580 let bbox = [0.0, 0.0, 100.0, 50.0];
1581 let stream = AppearanceStream::new(content.clone(), bbox);
1582
1583 assert_eq!(stream.content, content);
1584 assert_eq!(stream.bbox, bbox);
1585 assert!(stream.resources.is_empty());
1586 }
1587
1588 #[test]
1589 fn test_appearance_stream_with_resources_advanced() {
1590 let mut resources = Dictionary::new();
1591 resources.set("Font", Object::Dictionary(Dictionary::new()));
1592
1593 let stream =
1594 AppearanceStream::new(vec![], [0.0, 0.0, 10.0, 10.0]).with_resources(resources.clone());
1595
1596 assert_eq!(stream.resources, resources);
1597 }
1598
1599 #[test]
1600 fn test_appearance_dictionary_new() {
1601 let dict = AppearanceDictionary::new();
1602 assert!(dict.appearances.is_empty());
1603 assert!(dict.down_appearances.is_empty());
1604 }
1605
1606 #[test]
1607 fn test_appearance_dictionary_set_get() {
1608 let mut dict = AppearanceDictionary::new();
1609 let stream = AppearanceStream::new(vec![1, 2, 3], [0.0, 0.0, 10.0, 10.0]);
1610
1611 dict.set_appearance(AppearanceState::Normal, stream);
1612 assert!(dict.get_appearance(AppearanceState::Normal).is_some());
1613 assert!(dict.get_appearance(AppearanceState::Down).is_none());
1614 }
1615
1616 #[test]
1617 fn test_text_field_multiline() {
1618 let mut generator = TextFieldAppearance::default();
1619 generator.multiline = true;
1620
1621 let widget = Widget::new(Rectangle {
1622 lower_left: Point { x: 0.0, y: 0.0 },
1623 upper_right: Point { x: 200.0, y: 100.0 },
1624 });
1625
1626 let text = "Line 1\nLine 2\nLine 3";
1627 let result = generator.generate_appearance(&widget, Some(text), AppearanceState::Normal);
1628 assert!(result.is_ok());
1629 }
1630
1631 #[test]
1632 fn test_appearance_with_custom_colors() {
1633 let mut generator = TextFieldAppearance::default();
1634 generator.text_color = Color::rgb(1.0, 0.0, 0.0); generator.font_size = 14.0;
1636 generator.justification = 1; let widget = Widget::new(Rectangle {
1639 lower_left: Point { x: 0.0, y: 0.0 },
1640 upper_right: Point { x: 100.0, y: 30.0 },
1641 });
1642
1643 let result =
1644 generator.generate_appearance(&widget, Some("Colored"), AppearanceState::Normal);
1645 assert!(result.is_ok());
1646 }
1647}