oxidize_pdf/forms/
field_appearance.rs

1//! Form field appearance streams per ISO 32000-1 ยง12.7.3.3
2//!
3//! This module handles the visual representation of form fields,
4//! including text fields, checkboxes, radio buttons, and buttons.
5
6use crate::error::Result;
7use crate::graphics::Color;
8use crate::objects::{Dictionary, Object, Stream};
9
10/// Appearance characteristics for form fields
11#[derive(Debug, Clone)]
12pub struct AppearanceCharacteristics {
13    /// Rotation of annotation (0, 90, 180, 270)
14    pub rotation: i32,
15    /// Border color (RGB)
16    pub border_color: Option<Color>,
17    /// Background color (RGB)
18    pub background_color: Option<Color>,
19    /// Normal caption
20    pub normal_caption: Option<String>,
21    /// Rollover caption
22    pub rollover_caption: Option<String>,
23    /// Down (pressed) caption
24    pub down_caption: Option<String>,
25    /// Normal icon
26    pub normal_icon: Option<Stream>,
27    /// Rollover icon
28    pub rollover_icon: Option<Stream>,
29    /// Down icon
30    pub down_icon: Option<Stream>,
31    /// Icon fit parameters
32    pub icon_fit: Option<IconFit>,
33    /// Text position relative to icon
34    pub text_position: TextPosition,
35}
36
37impl Default for AppearanceCharacteristics {
38    fn default() -> Self {
39        Self {
40            rotation: 0,
41            border_color: None,
42            background_color: None,
43            normal_caption: None,
44            rollover_caption: None,
45            down_caption: None,
46            normal_icon: None,
47            rollover_icon: None,
48            down_icon: None,
49            icon_fit: None,
50            text_position: TextPosition::CaptionOnly,
51        }
52    }
53}
54
55/// Icon fit parameters
56#[derive(Debug, Clone)]
57pub struct IconFit {
58    /// Scale type
59    pub scale_type: IconScaleType,
60    /// Scale when type
61    pub scale_when: IconScaleWhen,
62    /// Horizontal alignment (0.0 = left, 1.0 = right)
63    pub align_x: f64,
64    /// Vertical alignment (0.0 = bottom, 1.0 = top)
65    pub align_y: f64,
66    /// Fit to bounds ignoring aspect ratio
67    pub fit_bounds: bool,
68}
69
70/// How to scale icon
71#[derive(Debug, Clone, PartialEq)]
72pub enum IconScaleType {
73    /// Always scale
74    Always,
75    /// Scale only when icon is bigger
76    Bigger,
77    /// Scale only when icon is smaller
78    Smaller,
79    /// Never scale
80    Never,
81}
82
83/// When to scale icon
84#[derive(Debug, Clone, PartialEq)]
85pub enum IconScaleWhen {
86    /// Always scale
87    Always,
88    /// Scale only when icon is bigger than bounds
89    IconBigger,
90    /// Scale only when icon is smaller than bounds
91    IconSmaller,
92    /// Never scale
93    Never,
94}
95
96/// Text position relative to icon
97#[derive(Debug, Clone, PartialEq)]
98pub enum TextPosition {
99    /// No icon, caption only
100    CaptionOnly,
101    /// No caption, icon only
102    IconOnly,
103    /// Caption below icon
104    CaptionBelowIcon,
105    /// Caption above icon
106    CaptionAboveIcon,
107    /// Caption to the right of icon
108    CaptionRightIcon,
109    /// Caption to the left of icon
110    CaptionLeftIcon,
111    /// Caption overlaid on icon
112    CaptionOverlayIcon,
113}
114
115impl AppearanceCharacteristics {
116    /// Convert to PDF dictionary
117    pub fn to_dict(&self) -> Dictionary {
118        let mut dict = Dictionary::new();
119
120        if self.rotation != 0 {
121            dict.set("R", Object::Integer(self.rotation as i64));
122        }
123
124        if let Some(color) = &self.border_color {
125            dict.set("BC", color.to_array());
126        }
127
128        if let Some(color) = &self.background_color {
129            dict.set("BG", color.to_array());
130        }
131
132        if let Some(caption) = &self.normal_caption {
133            dict.set("CA", Object::String(caption.clone()));
134        }
135
136        if let Some(caption) = &self.rollover_caption {
137            dict.set("RC", Object::String(caption.clone()));
138        }
139
140        if let Some(caption) = &self.down_caption {
141            dict.set("AC", Object::String(caption.clone()));
142        }
143
144        if let Some(fit) = &self.icon_fit {
145            dict.set("IF", fit.to_dict());
146        }
147
148        dict.set("TP", Object::Integer(self.text_position.to_int()));
149
150        dict
151    }
152}
153
154impl IconFit {
155    /// Convert to PDF dictionary
156    pub fn to_dict(&self) -> Object {
157        let mut dict = Dictionary::new();
158
159        dict.set(
160            "SW",
161            Object::Name(
162                match self.scale_when {
163                    IconScaleWhen::Always => "A",
164                    IconScaleWhen::IconBigger => "B",
165                    IconScaleWhen::IconSmaller => "S",
166                    IconScaleWhen::Never => "N",
167                }
168                .to_string(),
169            ),
170        );
171
172        dict.set(
173            "S",
174            Object::Name(
175                match self.scale_type {
176                    IconScaleType::Always => "A",
177                    IconScaleType::Bigger => "B",
178                    IconScaleType::Smaller => "S",
179                    IconScaleType::Never => "N",
180                }
181                .to_string(),
182            ),
183        );
184
185        dict.set(
186            "A",
187            Object::Array(vec![Object::Real(self.align_x), Object::Real(self.align_y)]),
188        );
189
190        if self.fit_bounds {
191            dict.set("FB", Object::Boolean(true));
192        }
193
194        Object::Dictionary(dict)
195    }
196}
197
198impl TextPosition {
199    pub fn to_int(&self) -> i64 {
200        match self {
201            TextPosition::CaptionOnly => 0,
202            TextPosition::IconOnly => 1,
203            TextPosition::CaptionBelowIcon => 2,
204            TextPosition::CaptionAboveIcon => 3,
205            TextPosition::CaptionRightIcon => 4,
206            TextPosition::CaptionLeftIcon => 5,
207            TextPosition::CaptionOverlayIcon => 6,
208        }
209    }
210}
211
212/// Text field appearance generator
213pub struct FieldAppearanceGenerator {
214    /// Field value
215    pub value: String,
216    /// Font to use
217    pub font: String,
218    /// Font size
219    pub font_size: f64,
220    /// Text color
221    pub text_color: Color,
222    /// Background color
223    pub background_color: Option<Color>,
224    /// Border color
225    pub border_color: Option<Color>,
226    /// Border width
227    pub border_width: f64,
228    /// Field rectangle [x1, y1, x2, y2]
229    pub rect: [f64; 4],
230    /// Text alignment
231    pub alignment: TextAlignment,
232    /// Multi-line field
233    pub multiline: bool,
234    /// Max length (for comb fields)
235    pub max_length: Option<usize>,
236    /// Comb field (evenly spaced characters)
237    pub comb: bool,
238}
239
240/// Text alignment in fields
241#[derive(Debug, Clone, Copy, PartialEq)]
242pub enum TextAlignment {
243    Left,
244    Center,
245    Right,
246}
247
248impl FieldAppearanceGenerator {
249    /// Generate appearance stream for text field
250    pub fn generate_text_field(&self) -> Result<Stream> {
251        let mut ops = Vec::new();
252        let width = self.rect[2] - self.rect[0];
253        let height = self.rect[3] - self.rect[1];
254
255        // Save graphics state
256        ops.push("q".to_string());
257
258        // Draw background if specified
259        if let Some(bg_color) = &self.background_color {
260            ops.push(format!(
261                "{} {} {} rg",
262                bg_color.r(),
263                bg_color.g(),
264                bg_color.b()
265            ));
266            ops.push(format!("0 0 {} {} re", width, height));
267            ops.push("f".to_string());
268        }
269
270        // Draw border if specified
271        if let Some(border_color) = &self.border_color {
272            if self.border_width > 0.0 {
273                ops.push(format!("{} w", self.border_width));
274                ops.push(format!(
275                    "{} {} {} RG",
276                    border_color.r(),
277                    border_color.g(),
278                    border_color.b()
279                ));
280                ops.push(format!(
281                    "{} {} {} {} re",
282                    self.border_width / 2.0,
283                    self.border_width / 2.0,
284                    width - self.border_width,
285                    height - self.border_width
286                ));
287                ops.push("S".to_string());
288            }
289        }
290
291        // Begin text
292        ops.push("BT".to_string());
293
294        // Set font and size
295        ops.push(format!("/{} {} Tf", self.font, self.font_size));
296
297        // Set text color
298        ops.push(format!(
299            "{} {} {} rg",
300            self.text_color.r(),
301            self.text_color.g(),
302            self.text_color.b()
303        ));
304
305        // Calculate text position
306        let padding = 2.0;
307        let text_y = height / 2.0 - self.font_size / 2.0;
308
309        if self.comb {
310            if let Some(max_len) = self.max_length {
311                // Comb field - evenly space characters
312                let char_width = (width - 2.0 * padding) / max_len as f64;
313
314                for (i, ch) in self.value.chars().take(max_len).enumerate() {
315                    let x = padding + (i as f64 + 0.5) * char_width;
316                    ops.push(format!("{} {} Td", x, text_y));
317                    ops.push(format!("({}) Tj", escape_string(&ch.to_string())));
318                    if i < self.value.len() - 1 {
319                        ops.push(format!("{} 0 Td", -x));
320                    }
321                }
322            }
323        } else if self.multiline {
324            // Multi-line text field
325            let lines = self.value.lines();
326            let line_height = self.font_size * 1.2;
327            let mut y = height - padding - self.font_size;
328
329            for line in lines {
330                let x = match self.alignment {
331                    TextAlignment::Left => padding,
332                    TextAlignment::Center => width / 2.0,
333                    TextAlignment::Right => width - padding,
334                };
335
336                ops.push(format!("{} {} Td", x, y));
337                ops.push(format!("({}) Tj", escape_string(line)));
338
339                y -= line_height;
340                if y < padding {
341                    break;
342                }
343            }
344        } else {
345            // Single line text field
346            let x = match self.alignment {
347                TextAlignment::Left => padding,
348                TextAlignment::Center => width / 2.0,
349                TextAlignment::Right => width - padding,
350            };
351
352            ops.push(format!("{} {} Td", x, text_y));
353            ops.push(format!("({}) Tj", escape_string(&self.value)));
354        }
355
356        // End text
357        ops.push("ET".to_string());
358
359        // Restore graphics state
360        ops.push("Q".to_string());
361
362        let content = ops.join("\n");
363
364        let mut stream = Stream::new(content.into_bytes());
365        stream
366            .dictionary_mut()
367            .set("Type", Object::Name("XObject".to_string()));
368        stream
369            .dictionary_mut()
370            .set("Subtype", Object::Name("Form".to_string()));
371        stream.dictionary_mut().set(
372            "BBox",
373            Object::Array(vec![
374                Object::Real(0.0),
375                Object::Real(0.0),
376                Object::Real(width),
377                Object::Real(height),
378            ]),
379        );
380
381        Ok(stream)
382    }
383}
384
385/// Checkbox/Radio button appearance generator
386pub struct ButtonAppearanceGenerator {
387    /// Button style
388    pub style: ButtonStyle,
389    /// Size of the button
390    pub size: f64,
391    /// Border color
392    pub border_color: Color,
393    /// Background color
394    pub background_color: Color,
395    /// Check/dot color
396    pub check_color: Color,
397    /// Border width
398    pub border_width: f64,
399}
400
401/// Button visual style
402#[derive(Debug, Clone, Copy, PartialEq)]
403pub enum ButtonStyle {
404    /// Checkbox with checkmark
405    Check,
406    /// Checkbox with cross
407    Cross,
408    /// Checkbox with diamond
409    Diamond,
410    /// Checkbox with circle
411    Circle,
412    /// Checkbox with star
413    Star,
414    /// Checkbox with square
415    Square,
416    /// Radio button (circle with dot)
417    Radio,
418}
419
420impl ButtonAppearanceGenerator {
421    /// Generate appearance stream for checked state
422    pub fn generate_checked(&self) -> Result<Stream> {
423        let mut ops = Vec::new();
424
425        // Save graphics state
426        ops.push("q".to_string());
427
428        // Draw background
429        ops.push(format!(
430            "{} {} {} rg",
431            self.background_color.r(),
432            self.background_color.g(),
433            self.background_color.b()
434        ));
435
436        match self.style {
437            ButtonStyle::Radio => {
438                // Circle background
439                self.draw_circle(&mut ops, self.size / 2.0, self.size / 2.0, self.size / 2.0);
440                ops.push("f".to_string());
441
442                // Draw border
443                if self.border_width > 0.0 {
444                    ops.push(format!("{} w", self.border_width));
445                    ops.push(format!(
446                        "{} {} {} RG",
447                        self.border_color.r(),
448                        self.border_color.g(),
449                        self.border_color.b()
450                    ));
451                    ops.push("s".to_string());
452                }
453
454                // Draw dot
455                let dot_size = self.size * 0.3;
456                ops.push(format!(
457                    "{} {} {} rg",
458                    self.check_color.r(),
459                    self.check_color.g(),
460                    self.check_color.b()
461                ));
462                self.draw_circle(&mut ops, self.size / 2.0, self.size / 2.0, dot_size);
463                ops.push("f".to_string());
464            }
465            _ => {
466                // Rectangle background
467                ops.push(format!("0 0 {} {} re", self.size, self.size));
468                ops.push("f".to_string());
469
470                // Draw border
471                if self.border_width > 0.0 {
472                    ops.push(format!("{} w", self.border_width));
473                    ops.push(format!(
474                        "{} {} {} RG",
475                        self.border_color.r(),
476                        self.border_color.g(),
477                        self.border_color.b()
478                    ));
479                    ops.push(format!(
480                        "{} {} {} {} re",
481                        self.border_width / 2.0,
482                        self.border_width / 2.0,
483                        self.size - self.border_width,
484                        self.size - self.border_width
485                    ));
486                    ops.push("S".to_string());
487                }
488
489                // Draw check mark based on style
490                ops.push(format!(
491                    "{} {} {} rg",
492                    self.check_color.r(),
493                    self.check_color.g(),
494                    self.check_color.b()
495                ));
496
497                self.draw_check_style(&mut ops);
498            }
499        }
500
501        // Restore graphics state
502        ops.push("Q".to_string());
503
504        let content = ops.join("\n");
505
506        let mut stream = Stream::new(content.into_bytes());
507        stream
508            .dictionary_mut()
509            .set("Type", Object::Name("XObject".to_string()));
510        stream
511            .dictionary_mut()
512            .set("Subtype", Object::Name("Form".to_string()));
513        stream.dictionary_mut().set(
514            "BBox",
515            Object::Array(vec![
516                Object::Real(0.0),
517                Object::Real(0.0),
518                Object::Real(self.size),
519                Object::Real(self.size),
520            ]),
521        );
522
523        Ok(stream)
524    }
525
526    /// Generate appearance stream for unchecked state
527    pub fn generate_unchecked(&self) -> Result<Stream> {
528        let mut ops = Vec::new();
529
530        // Save graphics state
531        ops.push("q".to_string());
532
533        // Draw background
534        ops.push(format!(
535            "{} {} {} rg",
536            self.background_color.r(),
537            self.background_color.g(),
538            self.background_color.b()
539        ));
540
541        if self.style == ButtonStyle::Radio {
542            // Circle background
543            self.draw_circle(&mut ops, self.size / 2.0, self.size / 2.0, self.size / 2.0);
544            ops.push("f".to_string());
545
546            // Draw border
547            if self.border_width > 0.0 {
548                ops.push(format!("{} w", self.border_width));
549                ops.push(format!(
550                    "{} {} {} RG",
551                    self.border_color.r(),
552                    self.border_color.g(),
553                    self.border_color.b()
554                ));
555                ops.push("s".to_string());
556            }
557        } else {
558            // Rectangle background
559            ops.push(format!("0 0 {} {} re", self.size, self.size));
560            ops.push("f".to_string());
561
562            // Draw border
563            if self.border_width > 0.0 {
564                ops.push(format!("{} w", self.border_width));
565                ops.push(format!(
566                    "{} {} {} RG",
567                    self.border_color.r(),
568                    self.border_color.g(),
569                    self.border_color.b()
570                ));
571                ops.push(format!(
572                    "{} {} {} {} re",
573                    self.border_width / 2.0,
574                    self.border_width / 2.0,
575                    self.size - self.border_width,
576                    self.size - self.border_width
577                ));
578                ops.push("S".to_string());
579            }
580        }
581
582        // Restore graphics state
583        ops.push("Q".to_string());
584
585        let content = ops.join("\n");
586
587        let mut stream = Stream::new(content.into_bytes());
588        stream
589            .dictionary_mut()
590            .set("Type", Object::Name("XObject".to_string()));
591        stream
592            .dictionary_mut()
593            .set("Subtype", Object::Name("Form".to_string()));
594        stream.dictionary_mut().set(
595            "BBox",
596            Object::Array(vec![
597                Object::Real(0.0),
598                Object::Real(0.0),
599                Object::Real(self.size),
600                Object::Real(self.size),
601            ]),
602        );
603
604        Ok(stream)
605    }
606
607    fn draw_circle(&self, ops: &mut Vec<String>, cx: f64, cy: f64, r: f64) {
608        // Draw circle using Bezier curves
609        let k = 0.552284749831; // Magic constant for circle approximation
610
611        ops.push(format!("{} {} m", cx + r, cy));
612        ops.push(format!(
613            "{} {} {} {} {} {} c",
614            cx + r,
615            cy + r * k,
616            cx + r * k,
617            cy + r,
618            cx,
619            cy + r
620        ));
621        ops.push(format!(
622            "{} {} {} {} {} {} c",
623            cx - r * k,
624            cy + r,
625            cx - r,
626            cy + r * k,
627            cx - r,
628            cy
629        ));
630        ops.push(format!(
631            "{} {} {} {} {} {} c",
632            cx - r,
633            cy - r * k,
634            cx - r * k,
635            cy - r,
636            cx,
637            cy - r
638        ));
639        ops.push(format!(
640            "{} {} {} {} {} {} c",
641            cx + r * k,
642            cy - r,
643            cx + r,
644            cy - r * k,
645            cx + r,
646            cy
647        ));
648    }
649
650    fn draw_check_style(&self, ops: &mut Vec<String>) {
651        match self.style {
652            ButtonStyle::Check => {
653                // Draw checkmark
654                ops.push(format!("{} w", self.size * 0.1));
655                ops.push(format!("{} {} m", self.size * 0.2, self.size * 0.5));
656                ops.push(format!("{} {} l", self.size * 0.4, self.size * 0.3));
657                ops.push(format!("{} {} l", self.size * 0.8, self.size * 0.7));
658                ops.push("S".to_string());
659            }
660            ButtonStyle::Cross => {
661                // Draw X
662                ops.push(format!("{} w", self.size * 0.1));
663                ops.push(format!("{} {} m", self.size * 0.2, self.size * 0.2));
664                ops.push(format!("{} {} l", self.size * 0.8, self.size * 0.8));
665                ops.push(format!("{} {} m", self.size * 0.2, self.size * 0.8));
666                ops.push(format!("{} {} l", self.size * 0.8, self.size * 0.2));
667                ops.push("S".to_string());
668            }
669            ButtonStyle::Diamond => {
670                // Draw diamond
671                ops.push(format!("{} {} m", self.size * 0.5, self.size * 0.8));
672                ops.push(format!("{} {} l", self.size * 0.8, self.size * 0.5));
673                ops.push(format!("{} {} l", self.size * 0.5, self.size * 0.2));
674                ops.push(format!("{} {} l", self.size * 0.2, self.size * 0.5));
675                ops.push("f".to_string());
676            }
677            ButtonStyle::Circle => {
678                // Draw filled circle
679                self.draw_circle(ops, self.size / 2.0, self.size / 2.0, self.size * 0.3);
680                ops.push("f".to_string());
681            }
682            ButtonStyle::Star => {
683                // Draw star
684                let cx = self.size / 2.0;
685                let cy = self.size / 2.0;
686                let r = self.size * 0.4;
687
688                ops.push(format!("{} {} m", cx, cy + r));
689                for i in 1..10 {
690                    let angle = i as f64 * 36.0 * std::f64::consts::PI / 180.0;
691                    let radius = if i % 2 == 0 { r } else { r * 0.5 };
692                    let x = cx + radius * angle.sin();
693                    let y = cy + radius * angle.cos();
694                    ops.push(format!("{} {} l", x, y));
695                }
696                ops.push("f".to_string());
697            }
698            ButtonStyle::Square => {
699                // Draw filled square
700                let inset = self.size * 0.25;
701                ops.push(format!(
702                    "{} {} {} {} re",
703                    inset,
704                    inset,
705                    self.size - 2.0 * inset,
706                    self.size - 2.0 * inset
707                ));
708                ops.push("f".to_string());
709            }
710            _ => {}
711        }
712    }
713}
714
715/// Escape special characters in PDF strings
716fn escape_string(s: &str) -> String {
717    s.chars()
718        .map(|c| match c {
719            '(' => "\\(".to_string(),
720            ')' => "\\)".to_string(),
721            '\\' => "\\\\".to_string(),
722            '\n' => "\\n".to_string(),
723            '\r' => "\\r".to_string(),
724            '\t' => "\\t".to_string(),
725            c => c.to_string(),
726        })
727        .collect()
728}
729
730/// Push button appearance generator
731pub struct PushButtonAppearanceGenerator {
732    /// Button caption
733    pub caption: String,
734    /// Font to use
735    pub font: String,
736    /// Font size
737    pub font_size: f64,
738    /// Text color
739    pub text_color: Color,
740    /// Background color
741    pub background_color: Color,
742    /// Border color
743    pub border_color: Color,
744    /// Border width
745    pub border_width: f64,
746    /// Button rectangle [width, height]
747    pub size: [f64; 2],
748    /// Border style
749    pub border_style: ButtonBorderStyle,
750}
751
752/// Border style for buttons
753#[derive(Debug, Clone, Copy, PartialEq)]
754pub enum ButtonBorderStyle {
755    /// Solid border
756    Solid,
757    /// Dashed border
758    Dashed,
759    /// Beveled (3D raised)
760    Beveled,
761    /// Inset (3D pressed)
762    Inset,
763    /// Underline only
764    Underline,
765}
766
767impl PushButtonAppearanceGenerator {
768    /// Generate normal appearance
769    pub fn generate_normal(&self) -> Result<Stream> {
770        self.generate_appearance(false)
771    }
772
773    /// Generate rollover appearance
774    pub fn generate_rollover(&self) -> Result<Stream> {
775        // Make slightly lighter for rollover
776        let mut appearance = self.clone();
777        appearance.background_color = appearance.background_color.lighten(0.1);
778        appearance.generate_appearance(false)
779    }
780
781    /// Generate down (pressed) appearance
782    pub fn generate_down(&self) -> Result<Stream> {
783        self.generate_appearance(true)
784    }
785
786    fn generate_appearance(&self, pressed: bool) -> Result<Stream> {
787        let mut ops = Vec::new();
788        let [width, height] = self.size;
789
790        // Save graphics state
791        ops.push("q".to_string());
792
793        // Draw background
794        let bg_color = if pressed {
795            self.background_color.darken(0.1)
796        } else {
797            self.background_color
798        };
799
800        ops.push(format!(
801            "{} {} {} rg",
802            bg_color.r(),
803            bg_color.g(),
804            bg_color.b()
805        ));
806        ops.push(format!("0 0 {} {} re", width, height));
807        ops.push("f".to_string());
808
809        // Draw border based on style
810        self.draw_border(&mut ops, width, height, pressed);
811
812        // Draw caption text
813        if !self.caption.is_empty() {
814            ops.push("BT".to_string());
815            ops.push(format!("/{} {} Tf", self.font, self.font_size));
816            ops.push(format!(
817                "{} {} {} rg",
818                self.text_color.r(),
819                self.text_color.g(),
820                self.text_color.b()
821            ));
822
823            // Center text
824            let text_x = width / 2.0;
825            let text_y = height / 2.0 - self.font_size / 2.0;
826
827            ops.push(format!("{} {} Td", text_x, text_y));
828            ops.push(format!("({}) Tj", escape_string(&self.caption)));
829            ops.push("ET".to_string());
830        }
831
832        // Restore graphics state
833        ops.push("Q".to_string());
834
835        let content = ops.join("\n");
836
837        let mut stream = Stream::new(content.into_bytes());
838        stream
839            .dictionary_mut()
840            .set("Type", Object::Name("XObject".to_string()));
841        stream
842            .dictionary_mut()
843            .set("Subtype", Object::Name("Form".to_string()));
844        stream.dictionary_mut().set(
845            "BBox",
846            Object::Array(vec![
847                Object::Real(0.0),
848                Object::Real(0.0),
849                Object::Real(width),
850                Object::Real(height),
851            ]),
852        );
853
854        Ok(stream)
855    }
856
857    fn draw_border(&self, ops: &mut Vec<String>, width: f64, height: f64, pressed: bool) {
858        match self.border_style {
859            ButtonBorderStyle::Solid => {
860                if self.border_width > 0.0 {
861                    ops.push(format!("{} w", self.border_width));
862                    ops.push(format!(
863                        "{} {} {} RG",
864                        self.border_color.r(),
865                        self.border_color.g(),
866                        self.border_color.b()
867                    ));
868                    ops.push(format!(
869                        "{} {} {} {} re",
870                        self.border_width / 2.0,
871                        self.border_width / 2.0,
872                        width - self.border_width,
873                        height - self.border_width
874                    ));
875                    ops.push("S".to_string());
876                }
877            }
878            ButtonBorderStyle::Dashed => {
879                if self.border_width > 0.0 {
880                    ops.push(format!("{} w", self.border_width));
881                    ops.push("[3 3] 0 d".to_string()); // Dash pattern
882                    ops.push(format!(
883                        "{} {} {} RG",
884                        self.border_color.r(),
885                        self.border_color.g(),
886                        self.border_color.b()
887                    ));
888                    ops.push(format!(
889                        "{} {} {} {} re",
890                        self.border_width / 2.0,
891                        self.border_width / 2.0,
892                        width - self.border_width,
893                        height - self.border_width
894                    ));
895                    ops.push("S".to_string());
896                }
897            }
898            ButtonBorderStyle::Beveled | ButtonBorderStyle::Inset => {
899                let is_inset = self.border_style == ButtonBorderStyle::Inset || pressed;
900                let light_color = if is_inset {
901                    self.border_color.darken(0.3)
902                } else {
903                    self.border_color.lighten(0.3)
904                };
905                let dark_color = if is_inset {
906                    self.border_color.lighten(0.3)
907                } else {
908                    self.border_color.darken(0.3)
909                };
910
911                // Top and left edges (light)
912                ops.push(format!("{} w", self.border_width));
913                ops.push(format!(
914                    "{} {} {} RG",
915                    light_color.r(),
916                    light_color.g(),
917                    light_color.b()
918                ));
919                ops.push(format!("{} {} m", 0.0, 0.0));
920                ops.push(format!("{} {} l", 0.0, height));
921                ops.push(format!("{} {} l", width, height));
922                ops.push("S".to_string());
923
924                // Bottom and right edges (dark)
925                ops.push(format!(
926                    "{} {} {} RG",
927                    dark_color.r(),
928                    dark_color.g(),
929                    dark_color.b()
930                ));
931                ops.push(format!("{} {} m", width, height));
932                ops.push(format!("{} {} l", width, 0.0));
933                ops.push(format!("{} {} l", 0.0, 0.0));
934                ops.push("S".to_string());
935            }
936            ButtonBorderStyle::Underline => {
937                if self.border_width > 0.0 {
938                    ops.push(format!("{} w", self.border_width));
939                    ops.push(format!(
940                        "{} {} {} RG",
941                        self.border_color.r(),
942                        self.border_color.g(),
943                        self.border_color.b()
944                    ));
945                    ops.push(format!("{} {} m", 0.0, self.border_width / 2.0));
946                    ops.push(format!("{} {} l", width, self.border_width / 2.0));
947                    ops.push("S".to_string());
948                }
949            }
950        }
951    }
952}
953
954impl Clone for PushButtonAppearanceGenerator {
955    fn clone(&self) -> Self {
956        Self {
957            caption: self.caption.clone(),
958            font: self.font.clone(),
959            font_size: self.font_size,
960            text_color: self.text_color,
961            background_color: self.background_color,
962            border_color: self.border_color,
963            border_width: self.border_width,
964            size: self.size,
965            border_style: self.border_style,
966        }
967    }
968}
969
970impl Color {
971    pub fn lighten(&self, amount: f64) -> Color {
972        Color::rgb(
973            (self.r() + amount).min(1.0),
974            (self.g() + amount).min(1.0),
975            (self.b() + amount).min(1.0),
976        )
977    }
978
979    pub fn darken(&self, amount: f64) -> Color {
980        Color::rgb(
981            (self.r() - amount).max(0.0),
982            (self.g() - amount).max(0.0),
983            (self.b() - amount).max(0.0),
984        )
985    }
986
987    pub fn to_array(&self) -> Object {
988        Object::Array(vec![
989            Object::Real(self.r()),
990            Object::Real(self.g()),
991            Object::Real(self.b()),
992        ])
993    }
994}