Skip to main content

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::{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
13/// Emit a `(text) Tj` operator for a built-in PDF base-14 font.
14///
15/// ISO 32000-1 §9.6.2.2 specifies that the standard 14 Type1 fonts use
16/// StandardEncoding by default; when a /DA string references one, viewers
17/// apply WinAnsi encoding for the subset of base-14 fonts that name Latin
18/// glyphs (Helvetica, Times, Courier). The string-literal bytes in the `Tj`
19/// operator must therefore be *WinAnsi-encoded*, not UTF-8.
20///
21/// Fails explicitly (`PdfError::EncodingError`) when `text` contains any
22/// codepoint WinAnsi cannot represent — the caller (typically
23/// `Document::fill_field`) must propagate this so the user sees the real
24/// cause instead of receiving a silently-garbled `/AP` stream.
25///
26/// Custom (Type0/CID) fonts and symbolic fonts (Symbol, ZapfDingbats) are
27/// rejected here — they follow separate content-stream encodings that live
28/// outside the WinAnsi path.
29fn 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    // Built-in non-symbolic → WinAnsi strict. Any codepoint WinAnsi can't
46    // represent fails the operation instead of emitting `?` or raw UTF-8.
47    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
65/// Emit a `<HHHH...> Tj` operator for a custom Type0/CID font.
66///
67/// Each Unicode codepoint is resolved to its glyph index via the font's cmap
68/// (`Font::glyph_mapping.char_to_glyph`) and written as a 4-hex-digit code —
69/// exactly what a `/Encoding /Identity-H` font expects on the content-stream
70/// side. The chars are recorded into `used_chars` so the caller can merge
71/// them into `Document::used_characters_by_font` for the subsetter (matches
72/// the infrastructure introduced for issue #204).
73///
74/// Fails with `PdfError::EncodingError` when the font has no glyph for a
75/// codepoint — silent fallback to `.notdef` would leave the user with
76/// invisible-or-blank glyphs.
77fn 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/// Appearance states for form fields
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103pub enum AppearanceState {
104    /// Normal appearance (default state)
105    Normal,
106    /// Rollover appearance (mouse hover)
107    Rollover,
108    /// Down appearance (mouse pressed)
109    Down,
110}
111
112impl AppearanceState {
113    /// Get the PDF name for this state
114    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/// Appearance stream for a form field
124#[derive(Debug, Clone)]
125pub struct AppearanceStream {
126    /// The content stream data
127    pub content: Vec<u8>,
128    /// Resources dictionary (fonts, colors, etc.)
129    pub resources: Dictionary,
130    /// Bounding box for the appearance
131    pub bbox: [f64; 4],
132}
133
134impl AppearanceStream {
135    /// Create a new appearance stream
136    pub fn new(content: Vec<u8>, bbox: [f64; 4]) -> Self {
137        Self {
138            content,
139            resources: Dictionary::new(),
140            bbox,
141        }
142    }
143
144    /// Set resources dictionary
145    pub fn with_resources(mut self, resources: Dictionary) -> Self {
146        self.resources = resources;
147        self
148    }
149
150    /// Convert to a Stream object
151    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        // Set bounding box
157        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        // Set resources
166        if !self.resources.is_empty() {
167            dict.set("Resources", Object::Dictionary(self.resources.clone()));
168        }
169
170        // Create stream with dictionary
171        Stream::with_dictionary(dict, self.content.clone())
172    }
173}
174
175/// Outcome of an appearance-stream build — carries the stream plus the
176/// per-font character obligations the generator recorded. Callers
177/// (typically `Document::fill_field`) merge `used_chars_by_font` into
178/// `Document::used_characters_by_font` so the writer's font subsetter
179/// covers every codepoint referenced from any `/AP` stream (preserves
180/// the #204 invariant).
181///
182/// Named struct instead of a tuple so adding further outputs (e.g.
183/// computed bbox overrides, resource hints) doesn't become a breaking
184/// API change. The map is empty for built-in fonts that don't need
185/// subsetting.
186#[derive(Debug, Clone)]
187pub struct FieldAppearanceResult {
188    /// The rendered appearance stream ready to attach to a widget.
189    pub stream: AppearanceStream,
190    /// Characters consumed from each custom Type0 font, keyed by font
191    /// name. Empty when every emission went through the built-in path.
192    pub used_chars_by_font: HashMap<String, HashSet<char>>,
193}
194
195/// Appearance dictionary for a form field
196#[derive(Debug, Clone)]
197pub struct AppearanceDictionary {
198    /// Appearance streams by state
199    appearances: HashMap<AppearanceState, AppearanceStream>,
200    /// Down appearances for different values (checkboxes, radio buttons)
201    down_appearances: HashMap<String, AppearanceStream>,
202}
203
204impl AppearanceDictionary {
205    /// Create a new appearance dictionary
206    pub fn new() -> Self {
207        Self {
208            appearances: HashMap::new(),
209            down_appearances: HashMap::new(),
210        }
211    }
212
213    /// Set appearance for a specific state
214    pub fn set_appearance(&mut self, state: AppearanceState, stream: AppearanceStream) {
215        self.appearances.insert(state, stream);
216    }
217
218    /// Set down appearance for a specific value
219    pub fn set_down_appearance(&mut self, value: String, stream: AppearanceStream) {
220        self.down_appearances.insert(value, stream);
221    }
222
223    /// Get appearance for a state
224    pub fn get_appearance(&self, state: AppearanceState) -> Option<&AppearanceStream> {
225        self.appearances.get(&state)
226    }
227
228    /// Convert to PDF dictionary
229    pub fn to_dict(&self) -> Dictionary {
230        let mut dict = Dictionary::new();
231
232        // Add appearances by state
233        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        // Add down appearances if any
242        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
264/// Trait for generating appearance streams for different field types
265pub trait AppearanceGenerator {
266    /// Generate appearance stream for the field
267    fn generate_appearance(
268        &self,
269        widget: &Widget,
270        value: Option<&str>,
271        state: AppearanceState,
272    ) -> Result<AppearanceStream>;
273}
274
275/// Text field appearance generator
276pub struct TextFieldAppearance {
277    /// Font to use
278    pub font: Font,
279    /// Font size
280    pub font_size: f64,
281    /// Text color
282    pub text_color: Color,
283    /// Justification (0=left, 1=center, 2=right)
284    pub justification: i32,
285    /// Multiline text
286    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    /// Generate the appearance honouring an optional pre-resolved custom
315    /// (Type0/CID) font. Returns a [`FieldAppearanceResult`] carrying both
316    /// the stream and any characters that the Type0 path consumed from the
317    /// font (empty for the built-in path).
318    ///
319    /// The caller is responsible for supplying `custom_font` when
320    /// `self.font == Font::Custom(_)`. If `self.font` is custom but
321    /// `custom_font` is `None`, an explicit `PdfError::EncodingError`
322    /// signals "font not registered on the Document" (a common user
323    /// mistake distinct from the generator not supporting custom fonts).
324    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        // Save graphics state
338        content.push_str("q\n");
339
340        // Draw background if specified
341        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        // Draw border
347        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                    // Simplified beveled/inset border
361                    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        // Draw text if value is provided
370        if let Some(text) = value {
371            // Set text color
372            crate::graphics::color::write_fill_color(&mut content, self.text_color);
373
374            // Begin text
375            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            // Calculate text position
383            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,     // Center (would need text width calculation)
388                2 => width - padding, // Right
389                _ => padding,         // Left
390            };
391
392            content.push_str(&format!("{text_x} {text_y} Td\n"));
393
394            // Dispatch on the font kind.
395            //
396            // `(true, Some)`  → Custom Type0/CID font with a resolved font
397            //                   handle. Emits hex-CID Tj and records used
398            //                   chars (#204 subsetter invariant).
399            // `(true, None)`  → `/DA` names a custom font the Document does
400            //                   not have registered. Fail fast with a
401            //                   diagnostic that points the caller at the
402            //                   likely mistake; silently falling through to
403            //                   `emit_tj_for_builtin` would produce an
404            //                   opaque "Custom fonts not supported" error
405            //                   from a different code path, confusing
406            //                   anyone who mistyped a font name.
407            // `(false, _)`    → Built-in Type1 path, WinAnsi strict.
408            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            // End text
428            content.push_str("ET\n");
429        }
430
431        // Restore graphics state
432        content.push_str("Q\n");
433
434        // Create resources dictionary — the /Font entry differs by path:
435        // - Built-in: emit the current Type1 inline dict (correct as-is).
436        // - Custom Type0: emit a placeholder entry whose key matches the
437        //   font name; the writer's /AP externalisation pass rewrites it
438        //   to an indirect Reference to the document-level Type0 object
439        //   (see issue #212 Phase 3).
440        let mut resources = Dictionary::new();
441        let mut font_dict = Dictionary::new();
442        if self.font.is_custom() {
443            // Placeholder — real reference is wired in at write-time.
444            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
469/// Checkbox appearance generator
470pub struct CheckBoxAppearance {
471    /// Check mark style
472    pub check_style: CheckStyle,
473    /// Check color
474    pub check_color: Color,
475}
476
477/// Style of check mark
478#[derive(Debug, Clone, Copy)]
479pub enum CheckStyle {
480    /// Check mark (✓)
481    Check,
482    /// Cross (✗)
483    Cross,
484    /// Square (■)
485    Square,
486    /// Circle (●)
487    Circle,
488    /// Star (★)
489    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        // Save graphics state
515        content.push_str("q\n");
516
517        // Draw background
518        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        // Draw border
524        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        // Draw check mark if checked
531        if is_checked {
532            // Set check color
533            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                    // Draw check mark path
540                    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                    // Draw X
547                    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                    // Draw filled square
555                    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                    // Draw filled circle (simplified)
563                    let cx = width / 2.0;
564                    let cy = height / 2.0;
565                    let r = (width.min(height) - 2.0 * inset) / 2.0;
566
567                    // Use Bézier curves to approximate circle
568                    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                    // Draw 5-pointed star (simplified)
610                    let cx = width / 2.0;
611                    let cy = height / 2.0;
612                    let r = (width.min(height) - 2.0 * inset) / 2.0;
613
614                    // Star points (simplified)
615                    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        // Restore graphics state
633        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
641/// Radio button appearance generator
642pub struct RadioButtonAppearance {
643    /// Button color when selected
644    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        // Save graphics state
669        content.push_str("q\n");
670
671        // Draw background circle
672        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        // Draw outer circle
683        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        // Draw border
724        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        // Draw inner dot if selected
769        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        // Restore graphics state
814        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
822/// Push button appearance generator
823pub struct PushButtonAppearance {
824    /// Button label
825    pub label: String,
826    /// Label font
827    pub font: Font,
828    /// Font size
829    pub font_size: f64,
830    /// Text color
831    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        // Save graphics state
858        content.push_str("q\n");
859
860        // Draw background with different colors for different states
861        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        // Draw beveled border for button appearance
874        if matches!(widget.appearance.border_style, BorderStyle::Beveled) {
875            // Light edge (top and left)
876            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            // Dark edge (bottom and right)
882            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            // Regular border
887            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        // Draw label text — skipped for custom Type0 fonts because the
895        // hex-CID Tj path requires the font's glyph mapping, which is not
896        // yet a parameter of this generator (see issue #212 follow-up).
897        // The resource dict below still emits the Type0 placeholder so the
898        // writer can rewrite the indirect reference correctly.
899        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            // Center text (simplified - would need actual text width calculation)
910            let text_x = width / 4.0; // Approximate centering
911            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            // Same contract as TextFieldAppearance: built-in Type1 → WinAnsi
916            // strict encoding, explicit error for anything outside the
917            // repertoire. Applies to the button label.
918            emit_tj_for_builtin(&mut content, &self.label, &self.font)?;
919
920            content.push_str("ET\n");
921        }
922
923        // Restore graphics state
924        content.push_str("Q\n");
925
926        // Create resources dictionary
927        let mut resources = Dictionary::new();
928
929        // Font resource — Type0 placeholder for custom fonts so the writer's
930        // rewrite_ap_stream_font_resources can substitute an indirect
931        // reference to the document-level CIDFontType0 object. Built-in
932        // Type1 fonts continue to embed the inline Type1 dict (the writer
933        // does not rewrite Type1 inline dicts; viewers resolve base-14 by
934        // name).
935        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/// Appearance generator for ComboBox fields
960#[derive(Debug, Clone)]
961pub struct ComboBoxAppearance {
962    /// Font for text
963    pub font: Font,
964    /// Font size
965    pub font_size: f64,
966    /// Text color
967    pub text_color: Color,
968    /// Selected option
969    pub selected_text: Option<String>,
970    /// Show dropdown arrow
971    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    /// Parallel to [`TextFieldAppearance::generate_appearance_with_font`].
1000    /// When `self.font == Font::Custom(name)` AND `custom_font` is supplied,
1001    /// emits a hex-CID `<HHHH...> Tj` with a Type0 placeholder resource; the
1002    /// writer rewrites the placeholder into an indirect Reference to the
1003    /// document-level font object. Same contract on the
1004    /// `Custom + None` case: fails fast with a "font not registered"
1005    /// `PdfError::EncodingError` instead of silently falling back.
1006    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        // Draw background
1020        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        // Draw border
1025        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        // Draw dropdown arrow if enabled
1033        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        // Draw selected text
1044        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            // Same dispatch as `TextFieldAppearance::generate_appearance_with_font`
1056            // — see that function for the rationale on the explicit
1057            // `(true, None)` arm. Combo boxes reach this path when the field's
1058            // `/DA` picks a custom font and the user renders the selected value.
1059            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        // Resources dict parallels the TextField path: custom → Type0
1079        // placeholder (writer-rewritten), builtin → Type1 inline.
1080        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/// Appearance generator for ListBox fields
1108#[derive(Debug, Clone)]
1109pub struct ListBoxAppearance {
1110    /// Font for text
1111    pub font: Font,
1112    /// Font size
1113    pub font_size: f64,
1114    /// Text color
1115    pub text_color: Color,
1116    /// Background color for selected items
1117    pub selection_color: Color,
1118    /// Options to display
1119    pub options: Vec<String>,
1120    /// Selected indices
1121    pub selected: Vec<usize>,
1122    /// Item height
1123    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    /// Type0/CID-aware appearance generator for ListBox fields.
1155    ///
1156    /// Mirrors [`TextFieldAppearance::generate_appearance_with_font`] and
1157    /// [`ComboBoxAppearance::generate_appearance_with_font`]: dispatches on
1158    /// `self.font.is_custom()` × the optional `custom_font` parameter to emit
1159    /// hex-CID Tj for Type0/CID fonts, an explicit error when the font name
1160    /// is custom but the resolved font object is missing, and the legacy
1161    /// WinAnsi-strict path for built-in Type1 fonts.
1162    ///
1163    /// The `/Resources/Font/<name>` entry follows the same pattern: a Type0
1164    /// placeholder when `is_custom()` (rewritten by the writer to an indirect
1165    /// reference at serialisation time, see
1166    /// `writer::pdf_writer::rewrite_ap_stream_font_resources`), and an inline
1167    /// Type1 dict otherwise. Used in support of issue #212.
1168    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        // Draw background
1182        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        // Draw border
1187        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        // Draw list items
1195        let mut y = height - self.item_height;
1196        for (index, option) in self.options.iter().enumerate() {
1197            if y < 0.0 {
1198                break; // Stop if we've filled the visible area
1199            }
1200
1201            // Draw selection background if selected
1202            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            // Draw text
1209            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            // Use white text for selected items, regular colour for others
1217            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            // Same dispatch as ComboBoxAppearance / TextFieldAppearance.
1226            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        // Resources dict — Type0 placeholder for custom, inline Type1 for built-in.
1249        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
1276/// Generate default appearance stream for a field type.
1277///
1278/// Kept for API stability — delegates to [`generate_field_appearance`] with
1279/// no `/DA` override and no custom-font context, which is the only behaviour
1280/// this function could ever offer. Prefer the richer signature of
1281/// [`generate_field_appearance`] when you need non-WinAnsi values or a
1282/// per-field font.
1283pub 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
1291/// Generate an appearance stream honouring an optional typed `/DA` and an
1292/// optional resolved custom font.
1293///
1294/// Returns both the stream and the set of characters consumed from the
1295/// custom font (empty for built-in fonts) — callers (typically
1296/// `Document::fill_field`) merge the latter into
1297/// `Document::used_characters_by_font` so the font subsetter emits a subset
1298/// that covers the appearance content (same invariant as issue #204).
1299///
1300/// Dispatch:
1301/// - `default_appearance.font == Font::Custom(name)` AND `custom_font` is
1302///   `Some(...)` → **Type0/CID path**. Content stream uses hex glyph-index
1303///   Tj, resources dict carries a placeholder `/Type0` entry that the
1304///   writer rewrites to an indirect Reference to the document-level font
1305///   object (see [`writer::pdf_writer`]).
1306/// - Anything else → **built-in / WinAnsi path** (existing behaviour, now
1307///   with strict encoding so non-WinAnsi values fail explicitly instead of
1308///   being silently corrupted).
1309pub 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            // Default button appearance (checkbox-style) does not consume a
1333            // user-supplied `/DA` today — the button glyphs are synthesised.
1334            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        // Should contain check mark drawing commands
1451        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        // Should not contain complex drawing for check mark
1471        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        // Should contain circle drawing commands (Bézier curves)
1490        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        // Test different states produce different appearances
1529        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        // Content should be different for different states (different background colors)
1540        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        // Test different check styles
1553        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); // Red text
1635        generator.font_size = 14.0;
1636        generator.justification = 1; // center
1637
1638        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}