oxidize_pdf/forms/
appearance.rs

1//! Appearance streams for form fields according to ISO 32000-1 Section 12.7.3.3
2//!
3//! This module provides appearance stream generation for interactive form fields,
4//! ensuring visual representation of field content and states.
5
6use 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/// Appearance states for form fields
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum AppearanceState {
16    /// Normal appearance (default state)
17    Normal,
18    /// Rollover appearance (mouse hover)
19    Rollover,
20    /// Down appearance (mouse pressed)
21    Down,
22}
23
24impl AppearanceState {
25    /// Get the PDF name for this state
26    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/// Appearance stream for a form field
36#[derive(Debug, Clone)]
37pub struct AppearanceStream {
38    /// The content stream data
39    pub content: Vec<u8>,
40    /// Resources dictionary (fonts, colors, etc.)
41    pub resources: Dictionary,
42    /// Bounding box for the appearance
43    pub bbox: [f64; 4],
44}
45
46impl AppearanceStream {
47    /// Create a new appearance stream
48    pub fn new(content: Vec<u8>, bbox: [f64; 4]) -> Self {
49        Self {
50            content,
51            resources: Dictionary::new(),
52            bbox,
53        }
54    }
55
56    /// Set resources dictionary
57    pub fn with_resources(mut self, resources: Dictionary) -> Self {
58        self.resources = resources;
59        self
60    }
61
62    /// Convert to a Stream object
63    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        // Set bounding box
69        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        // Set resources
78        if !self.resources.is_empty() {
79            dict.set("Resources", Object::Dictionary(self.resources.clone()));
80        }
81
82        // Create stream with dictionary
83        Stream::with_dictionary(dict, self.content.clone())
84    }
85}
86
87/// Appearance dictionary for a form field
88#[derive(Debug, Clone)]
89pub struct AppearanceDictionary {
90    /// Appearance streams by state
91    appearances: HashMap<AppearanceState, AppearanceStream>,
92    /// Down appearances for different values (checkboxes, radio buttons)
93    down_appearances: HashMap<String, AppearanceStream>,
94}
95
96impl AppearanceDictionary {
97    /// Create a new appearance dictionary
98    pub fn new() -> Self {
99        Self {
100            appearances: HashMap::new(),
101            down_appearances: HashMap::new(),
102        }
103    }
104
105    /// Set appearance for a specific state
106    pub fn set_appearance(&mut self, state: AppearanceState, stream: AppearanceStream) {
107        self.appearances.insert(state, stream);
108    }
109
110    /// Set down appearance for a specific value
111    pub fn set_down_appearance(&mut self, value: String, stream: AppearanceStream) {
112        self.down_appearances.insert(value, stream);
113    }
114
115    /// Get appearance for a state
116    pub fn get_appearance(&self, state: AppearanceState) -> Option<&AppearanceStream> {
117        self.appearances.get(&state)
118    }
119
120    /// Convert to PDF dictionary
121    pub fn to_dict(&self) -> Dictionary {
122        let mut dict = Dictionary::new();
123
124        // Add appearances by state
125        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        // Add down appearances if any
134        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
156/// Trait for generating appearance streams for different field types
157pub trait AppearanceGenerator {
158    /// Generate appearance stream for the field
159    fn generate_appearance(
160        &self,
161        widget: &Widget,
162        value: Option<&str>,
163        state: AppearanceState,
164    ) -> Result<AppearanceStream>;
165}
166
167/// Text field appearance generator
168pub struct TextFieldAppearance {
169    /// Font to use
170    pub font: Font,
171    /// Font size
172    pub font_size: f64,
173    /// Text color
174    pub text_color: Color,
175    /// Justification (0=left, 1=center, 2=right)
176    pub justification: i32,
177    /// Multiline text
178    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        // Save graphics state
206        content.push_str("q\n");
207
208        // Draw background if specified
209        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        // Draw border
219        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                    // Simplified beveled/inset border
237                    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        // Draw text if value is provided
246        if let Some(text) = value {
247            // Set text color
248            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            // Begin text
255            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            // Calculate text position
263            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,     // Center (would need text width calculation)
268                2 => width - padding, // Right
269                _ => padding,         // Left
270            };
271
272            content.push_str(&format!("{text_x} {text_y} Td\n"));
273
274            // Show text (escape special characters)
275            let escaped_text = text
276                .replace('\\', "\\\\")
277                .replace('(', "\\(")
278                .replace(')', "\\)");
279            content.push_str(&format!("({escaped_text}) Tj\n"));
280
281            // End text
282            content.push_str("ET\n");
283        }
284
285        // Restore graphics state
286        content.push_str("Q\n");
287
288        // Create resources dictionary
289        let mut resources = Dictionary::new();
290
291        // Add font resource
292        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
307/// Checkbox appearance generator
308pub struct CheckBoxAppearance {
309    /// Check mark style
310    pub check_style: CheckStyle,
311    /// Check color
312    pub check_color: Color,
313}
314
315/// Style of check mark
316#[derive(Debug, Clone, Copy)]
317pub enum CheckStyle {
318    /// Check mark (✓)
319    Check,
320    /// Cross (✗)
321    Cross,
322    /// Square (■)
323    Square,
324    /// Circle (●)
325    Circle,
326    /// Star (★)
327    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        // Save graphics state
353        content.push_str("q\n");
354
355        // Draw background
356        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        // Draw border
366        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        // Draw check mark if checked
377        if is_checked {
378            // Set check color
379            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                    // Draw check mark path
390                    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                    // Draw X
397                    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                    // Draw filled square
405                    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                    // Draw filled circle (simplified)
413                    let cx = width / 2.0;
414                    let cy = height / 2.0;
415                    let r = (width.min(height) - 2.0 * inset) / 2.0;
416
417                    // Use Bézier curves to approximate circle
418                    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                    // Draw 5-pointed star (simplified)
460                    let cx = width / 2.0;
461                    let cy = height / 2.0;
462                    let r = (width.min(height) - 2.0 * inset) / 2.0;
463
464                    // Star points (simplified)
465                    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        // Restore graphics state
483        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
491/// Radio button appearance generator
492pub struct RadioButtonAppearance {
493    /// Button color when selected
494    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        // Save graphics state
519        content.push_str("q\n");
520
521        // Draw background circle
522        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"); // White background
530        }
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        // Draw outer circle
537        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        // Draw border
578        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        // Draw inner dot if selected
627        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        // Restore graphics state
676        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
684/// Push button appearance generator
685pub struct PushButtonAppearance {
686    /// Button label
687    pub label: String,
688    /// Label font
689    pub font: Font,
690    /// Font size
691    pub font_size: f64,
692    /// Text color
693    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        // Save graphics state
720        content.push_str("q\n");
721
722        // Draw background with different colors for different states
723        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        // Draw beveled border for button appearance
740        if matches!(widget.appearance.border_style, BorderStyle::Beveled) {
741            // Light edge (top and left)
742            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            // Dark edge (bottom and right)
748            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            // Regular border
753            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        // Draw label text
765        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            // Center text (simplified - would need actual text width calculation)
780            let text_x = width / 4.0; // Approximate centering
781            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        // Restore graphics state
796        content.push_str("Q\n");
797
798        // Create resources dictionary
799        let mut resources = Dictionary::new();
800
801        // Add font resource
802        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/// Appearance generator for ComboBox fields
818#[derive(Debug, Clone)]
819pub struct ComboBoxAppearance {
820    /// Font for text
821    pub font: Font,
822    /// Font size
823    pub font_size: f64,
824    /// Text color
825    pub text_color: Color,
826    /// Selected option
827    pub selected_text: Option<String>,
828    /// Show dropdown arrow
829    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        // Draw background
857        content.push_str("1 1 1 rg\n"); // White background
858        content.push_str(&format!("0 0 {} {} re\n", width, height));
859        content.push_str("f\n");
860
861        // Draw border
862        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        // Draw dropdown arrow if enabled
876        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"); // Gray arrow
880            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        // Draw selected text
887        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            // Escape special characters in PDF strings
905            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/// Appearance generator for ListBox fields
922#[derive(Debug, Clone)]
923pub struct ListBoxAppearance {
924    /// Font for text
925    pub font: Font,
926    /// Font size
927    pub font_size: f64,
928    /// Text color
929    pub text_color: Color,
930    /// Background color for selected items
931    pub selection_color: Color,
932    /// Options to display
933    pub options: Vec<String>,
934    /// Selected indices
935    pub selected: Vec<usize>,
936    /// Item height
937    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        // Draw background
967        content.push_str("1 1 1 rg\n"); // White background
968        content.push_str(&format!("0 0 {} {} re\n", width, height));
969        content.push_str("f\n");
970
971        // Draw border
972        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        // Draw list items
986        let mut y = height - self.item_height;
987        for (index, option) in self.options.iter().enumerate() {
988            if y < 0.0 {
989                break; // Stop if we've filled the visible area
990            }
991
992            // Draw selection background if selected
993            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            // Draw text
1006            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            // Use white text for selected items, black for others
1014            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            // Escape special characters in PDF strings
1029            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
1047/// Generate default appearance stream for a field type
1048pub 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            // For now, default to checkbox appearance
1060            // In a real implementation, we'd need additional context to determine button type
1061            let generator = CheckBoxAppearance::default();
1062            generator.generate_appearance(widget, value, AppearanceState::Normal)
1063        }
1064        FieldType::Choice => {
1065            // Default to ComboBox appearance for choice fields
1066            let generator = ComboBoxAppearance::default();
1067            generator.generate_appearance(widget, value, AppearanceState::Normal)
1068        }
1069        FieldType::Signature => {
1070            // Use empty appearance for signature fields
1071            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        // Should contain check mark drawing commands
1166        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        // Should not contain complex drawing for check mark
1186        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        // Should contain circle drawing commands (Bézier curves)
1205        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        // Test different states produce different appearances
1244        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        // Content should be different for different states (different background colors)
1255        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        // Test different check styles
1268        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); // Red text
1350        generator.font_size = 14.0;
1351        generator.justification = 1; // center
1352
1353        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}