Skip to main content

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(crate::graphics::color::fill_color_op(*bg_color));
261            ops.push(format!("0 0 {} {} re", width, height));
262            ops.push("f".to_string());
263        }
264
265        // Draw border if specified
266        if let Some(border_color) = &self.border_color {
267            if self.border_width > 0.0 {
268                ops.push(format!("{} w", self.border_width));
269                ops.push(crate::graphics::color::stroke_color_op(*border_color));
270                ops.push(format!(
271                    "{} {} {} {} re",
272                    self.border_width / 2.0,
273                    self.border_width / 2.0,
274                    width - self.border_width,
275                    height - self.border_width
276                ));
277                ops.push("S".to_string());
278            }
279        }
280
281        // Begin text
282        ops.push("BT".to_string());
283
284        // Set font and size
285        ops.push(format!("/{} {} Tf", self.font, self.font_size));
286
287        // Set text color
288        ops.push(crate::graphics::color::fill_color_op(self.text_color));
289
290        // Calculate text position
291        let padding = 2.0;
292        let text_y = height / 2.0 - self.font_size / 2.0;
293
294        if self.comb {
295            if let Some(max_len) = self.max_length {
296                // Comb field - evenly space characters
297                let char_width = (width - 2.0 * padding) / max_len as f64;
298
299                for (i, ch) in self.value.chars().take(max_len).enumerate() {
300                    let x = padding + (i as f64 + 0.5) * char_width;
301                    ops.push(format!("{} {} Td", x, text_y));
302                    ops.push(format!("({}) Tj", escape_string(&ch.to_string())));
303                    if i < self.value.len() - 1 {
304                        ops.push(format!("{} 0 Td", -x));
305                    }
306                }
307            }
308        } else if self.multiline {
309            // Multi-line text field
310            let lines = self.value.lines();
311            let line_height = self.font_size * 1.2;
312            let mut y = height - padding - self.font_size;
313
314            for line in lines {
315                let x = match self.alignment {
316                    TextAlignment::Left => padding,
317                    TextAlignment::Center => width / 2.0,
318                    TextAlignment::Right => width - padding,
319                };
320
321                ops.push(format!("{} {} Td", x, y));
322                ops.push(format!("({}) Tj", escape_string(line)));
323
324                y -= line_height;
325                if y < padding {
326                    break;
327                }
328            }
329        } else {
330            // Single line text field
331            let x = match self.alignment {
332                TextAlignment::Left => padding,
333                TextAlignment::Center => width / 2.0,
334                TextAlignment::Right => width - padding,
335            };
336
337            ops.push(format!("{} {} Td", x, text_y));
338            ops.push(format!("({}) Tj", escape_string(&self.value)));
339        }
340
341        // End text
342        ops.push("ET".to_string());
343
344        // Restore graphics state
345        ops.push("Q".to_string());
346
347        let content = ops.join("\n");
348
349        let mut stream = Stream::new(content.into_bytes());
350        stream
351            .dictionary_mut()
352            .set("Type", Object::Name("XObject".to_string()));
353        stream
354            .dictionary_mut()
355            .set("Subtype", Object::Name("Form".to_string()));
356        stream.dictionary_mut().set(
357            "BBox",
358            Object::Array(vec![
359                Object::Real(0.0),
360                Object::Real(0.0),
361                Object::Real(width),
362                Object::Real(height),
363            ]),
364        );
365
366        Ok(stream)
367    }
368}
369
370/// Checkbox/Radio button appearance generator
371pub struct ButtonAppearanceGenerator {
372    /// Button style
373    pub style: ButtonStyle,
374    /// Size of the button
375    pub size: f64,
376    /// Border color
377    pub border_color: Color,
378    /// Background color
379    pub background_color: Color,
380    /// Check/dot color
381    pub check_color: Color,
382    /// Border width
383    pub border_width: f64,
384}
385
386/// Button visual style
387#[derive(Debug, Clone, Copy, PartialEq)]
388pub enum ButtonStyle {
389    /// Checkbox with checkmark
390    Check,
391    /// Checkbox with cross
392    Cross,
393    /// Checkbox with diamond
394    Diamond,
395    /// Checkbox with circle
396    Circle,
397    /// Checkbox with star
398    Star,
399    /// Checkbox with square
400    Square,
401    /// Radio button (circle with dot)
402    Radio,
403}
404
405impl ButtonAppearanceGenerator {
406    /// Generate appearance stream for checked state
407    pub fn generate_checked(&self) -> Result<Stream> {
408        let mut ops = Vec::new();
409
410        // Save graphics state
411        ops.push("q".to_string());
412
413        // Draw background
414        ops.push(crate::graphics::color::fill_color_op(self.background_color));
415
416        match self.style {
417            ButtonStyle::Radio => {
418                // Circle background
419                self.draw_circle(&mut ops, self.size / 2.0, self.size / 2.0, self.size / 2.0);
420                ops.push("f".to_string());
421
422                // Draw border
423                if self.border_width > 0.0 {
424                    ops.push(format!("{} w", self.border_width));
425                    ops.push(crate::graphics::color::stroke_color_op(self.border_color));
426                    ops.push("s".to_string());
427                }
428
429                // Draw dot
430                let dot_size = self.size * 0.3;
431                ops.push(crate::graphics::color::fill_color_op(self.check_color));
432                self.draw_circle(&mut ops, self.size / 2.0, self.size / 2.0, dot_size);
433                ops.push("f".to_string());
434            }
435            _ => {
436                // Rectangle background
437                ops.push(format!("0 0 {} {} re", self.size, self.size));
438                ops.push("f".to_string());
439
440                // Draw border
441                if self.border_width > 0.0 {
442                    ops.push(format!("{} w", self.border_width));
443                    ops.push(crate::graphics::color::stroke_color_op(self.border_color));
444                    ops.push(format!(
445                        "{} {} {} {} re",
446                        self.border_width / 2.0,
447                        self.border_width / 2.0,
448                        self.size - self.border_width,
449                        self.size - self.border_width
450                    ));
451                    ops.push("S".to_string());
452                }
453
454                // Draw check mark based on style
455                ops.push(crate::graphics::color::fill_color_op(self.check_color));
456
457                self.draw_check_style(&mut ops);
458            }
459        }
460
461        // Restore graphics state
462        ops.push("Q".to_string());
463
464        let content = ops.join("\n");
465
466        let mut stream = Stream::new(content.into_bytes());
467        stream
468            .dictionary_mut()
469            .set("Type", Object::Name("XObject".to_string()));
470        stream
471            .dictionary_mut()
472            .set("Subtype", Object::Name("Form".to_string()));
473        stream.dictionary_mut().set(
474            "BBox",
475            Object::Array(vec![
476                Object::Real(0.0),
477                Object::Real(0.0),
478                Object::Real(self.size),
479                Object::Real(self.size),
480            ]),
481        );
482
483        Ok(stream)
484    }
485
486    /// Generate appearance stream for unchecked state
487    pub fn generate_unchecked(&self) -> Result<Stream> {
488        let mut ops = Vec::new();
489
490        // Save graphics state
491        ops.push("q".to_string());
492
493        // Draw background
494        ops.push(crate::graphics::color::fill_color_op(self.background_color));
495
496        if self.style == ButtonStyle::Radio {
497            // Circle background
498            self.draw_circle(&mut ops, self.size / 2.0, self.size / 2.0, self.size / 2.0);
499            ops.push("f".to_string());
500
501            // Draw border
502            if self.border_width > 0.0 {
503                ops.push(format!("{} w", self.border_width));
504                ops.push(crate::graphics::color::stroke_color_op(self.border_color));
505                ops.push("s".to_string());
506            }
507        } else {
508            // Rectangle background
509            ops.push(format!("0 0 {} {} re", self.size, self.size));
510            ops.push("f".to_string());
511
512            // Draw border
513            if self.border_width > 0.0 {
514                ops.push(format!("{} w", self.border_width));
515                ops.push(crate::graphics::color::stroke_color_op(self.border_color));
516                ops.push(format!(
517                    "{} {} {} {} re",
518                    self.border_width / 2.0,
519                    self.border_width / 2.0,
520                    self.size - self.border_width,
521                    self.size - self.border_width
522                ));
523                ops.push("S".to_string());
524            }
525        }
526
527        // Restore graphics state
528        ops.push("Q".to_string());
529
530        let content = ops.join("\n");
531
532        let mut stream = Stream::new(content.into_bytes());
533        stream
534            .dictionary_mut()
535            .set("Type", Object::Name("XObject".to_string()));
536        stream
537            .dictionary_mut()
538            .set("Subtype", Object::Name("Form".to_string()));
539        stream.dictionary_mut().set(
540            "BBox",
541            Object::Array(vec![
542                Object::Real(0.0),
543                Object::Real(0.0),
544                Object::Real(self.size),
545                Object::Real(self.size),
546            ]),
547        );
548
549        Ok(stream)
550    }
551
552    fn draw_circle(&self, ops: &mut Vec<String>, cx: f64, cy: f64, r: f64) {
553        // Draw circle using Bezier curves
554        let k = 0.552284749831; // Magic constant for circle approximation
555
556        ops.push(format!("{} {} m", cx + r, cy));
557        ops.push(format!(
558            "{} {} {} {} {} {} c",
559            cx + r,
560            cy + r * k,
561            cx + r * k,
562            cy + r,
563            cx,
564            cy + r
565        ));
566        ops.push(format!(
567            "{} {} {} {} {} {} c",
568            cx - r * k,
569            cy + r,
570            cx - r,
571            cy + r * k,
572            cx - r,
573            cy
574        ));
575        ops.push(format!(
576            "{} {} {} {} {} {} c",
577            cx - r,
578            cy - r * k,
579            cx - r * k,
580            cy - r,
581            cx,
582            cy - r
583        ));
584        ops.push(format!(
585            "{} {} {} {} {} {} c",
586            cx + r * k,
587            cy - r,
588            cx + r,
589            cy - r * k,
590            cx + r,
591            cy
592        ));
593    }
594
595    fn draw_check_style(&self, ops: &mut Vec<String>) {
596        match self.style {
597            ButtonStyle::Check => {
598                // Draw checkmark
599                ops.push(format!("{} w", self.size * 0.1));
600                ops.push(format!("{} {} m", self.size * 0.2, self.size * 0.5));
601                ops.push(format!("{} {} l", self.size * 0.4, self.size * 0.3));
602                ops.push(format!("{} {} l", self.size * 0.8, self.size * 0.7));
603                ops.push("S".to_string());
604            }
605            ButtonStyle::Cross => {
606                // Draw X
607                ops.push(format!("{} w", self.size * 0.1));
608                ops.push(format!("{} {} m", self.size * 0.2, self.size * 0.2));
609                ops.push(format!("{} {} l", self.size * 0.8, self.size * 0.8));
610                ops.push(format!("{} {} m", self.size * 0.2, self.size * 0.8));
611                ops.push(format!("{} {} l", self.size * 0.8, self.size * 0.2));
612                ops.push("S".to_string());
613            }
614            ButtonStyle::Diamond => {
615                // Draw diamond
616                ops.push(format!("{} {} m", self.size * 0.5, self.size * 0.8));
617                ops.push(format!("{} {} l", self.size * 0.8, self.size * 0.5));
618                ops.push(format!("{} {} l", self.size * 0.5, self.size * 0.2));
619                ops.push(format!("{} {} l", self.size * 0.2, self.size * 0.5));
620                ops.push("f".to_string());
621            }
622            ButtonStyle::Circle => {
623                // Draw filled circle
624                self.draw_circle(ops, self.size / 2.0, self.size / 2.0, self.size * 0.3);
625                ops.push("f".to_string());
626            }
627            ButtonStyle::Star => {
628                // Draw star
629                let cx = self.size / 2.0;
630                let cy = self.size / 2.0;
631                let r = self.size * 0.4;
632
633                ops.push(format!("{} {} m", cx, cy + r));
634                for i in 1..10 {
635                    let angle = i as f64 * 36.0 * std::f64::consts::PI / 180.0;
636                    let radius = if i % 2 == 0 { r } else { r * 0.5 };
637                    let x = cx + radius * angle.sin();
638                    let y = cy + radius * angle.cos();
639                    ops.push(format!("{} {} l", x, y));
640                }
641                ops.push("f".to_string());
642            }
643            ButtonStyle::Square => {
644                // Draw filled square
645                let inset = self.size * 0.25;
646                ops.push(format!(
647                    "{} {} {} {} re",
648                    inset,
649                    inset,
650                    self.size - 2.0 * inset,
651                    self.size - 2.0 * inset
652                ));
653                ops.push("f".to_string());
654            }
655            _ => {}
656        }
657    }
658}
659
660/// Escape special characters in PDF strings
661fn escape_string(s: &str) -> String {
662    s.chars()
663        .map(|c| match c {
664            '(' => "\\(".to_string(),
665            ')' => "\\)".to_string(),
666            '\\' => "\\\\".to_string(),
667            '\n' => "\\n".to_string(),
668            '\r' => "\\r".to_string(),
669            '\t' => "\\t".to_string(),
670            c => c.to_string(),
671        })
672        .collect()
673}
674
675/// Push button appearance generator
676pub struct PushButtonAppearanceGenerator {
677    /// Button caption
678    pub caption: String,
679    /// Font to use
680    pub font: String,
681    /// Font size
682    pub font_size: f64,
683    /// Text color
684    pub text_color: Color,
685    /// Background color
686    pub background_color: Color,
687    /// Border color
688    pub border_color: Color,
689    /// Border width
690    pub border_width: f64,
691    /// Button rectangle [width, height]
692    pub size: [f64; 2],
693    /// Border style
694    pub border_style: ButtonBorderStyle,
695}
696
697/// Border style for buttons
698#[derive(Debug, Clone, Copy, PartialEq)]
699pub enum ButtonBorderStyle {
700    /// Solid border
701    Solid,
702    /// Dashed border
703    Dashed,
704    /// Beveled (3D raised)
705    Beveled,
706    /// Inset (3D pressed)
707    Inset,
708    /// Underline only
709    Underline,
710}
711
712impl PushButtonAppearanceGenerator {
713    /// Generate normal appearance
714    pub fn generate_normal(&self) -> Result<Stream> {
715        self.generate_appearance(false)
716    }
717
718    /// Generate rollover appearance
719    pub fn generate_rollover(&self) -> Result<Stream> {
720        // Make slightly lighter for rollover
721        let mut appearance = self.clone();
722        appearance.background_color = appearance.background_color.lighten(0.1);
723        appearance.generate_appearance(false)
724    }
725
726    /// Generate down (pressed) appearance
727    pub fn generate_down(&self) -> Result<Stream> {
728        self.generate_appearance(true)
729    }
730
731    fn generate_appearance(&self, pressed: bool) -> Result<Stream> {
732        let mut ops = Vec::new();
733        let [width, height] = self.size;
734
735        // Save graphics state
736        ops.push("q".to_string());
737
738        // Draw background
739        let bg_color = if pressed {
740            self.background_color.darken(0.1)
741        } else {
742            self.background_color
743        };
744
745        ops.push(crate::graphics::color::fill_color_op(bg_color));
746        ops.push(format!("0 0 {} {} re", width, height));
747        ops.push("f".to_string());
748
749        // Draw border based on style
750        self.draw_border(&mut ops, width, height, pressed);
751
752        // Draw caption text
753        if !self.caption.is_empty() {
754            ops.push("BT".to_string());
755            ops.push(format!("/{} {} Tf", self.font, self.font_size));
756            ops.push(crate::graphics::color::fill_color_op(self.text_color));
757
758            // Center text
759            let text_x = width / 2.0;
760            let text_y = height / 2.0 - self.font_size / 2.0;
761
762            ops.push(format!("{} {} Td", text_x, text_y));
763            ops.push(format!("({}) Tj", escape_string(&self.caption)));
764            ops.push("ET".to_string());
765        }
766
767        // Restore graphics state
768        ops.push("Q".to_string());
769
770        let content = ops.join("\n");
771
772        let mut stream = Stream::new(content.into_bytes());
773        stream
774            .dictionary_mut()
775            .set("Type", Object::Name("XObject".to_string()));
776        stream
777            .dictionary_mut()
778            .set("Subtype", Object::Name("Form".to_string()));
779        stream.dictionary_mut().set(
780            "BBox",
781            Object::Array(vec![
782                Object::Real(0.0),
783                Object::Real(0.0),
784                Object::Real(width),
785                Object::Real(height),
786            ]),
787        );
788
789        Ok(stream)
790    }
791
792    fn draw_border(&self, ops: &mut Vec<String>, width: f64, height: f64, pressed: bool) {
793        match self.border_style {
794            ButtonBorderStyle::Solid => {
795                if self.border_width > 0.0 {
796                    ops.push(format!("{} w", self.border_width));
797                    ops.push(crate::graphics::color::stroke_color_op(self.border_color));
798                    ops.push(format!(
799                        "{} {} {} {} re",
800                        self.border_width / 2.0,
801                        self.border_width / 2.0,
802                        width - self.border_width,
803                        height - self.border_width
804                    ));
805                    ops.push("S".to_string());
806                }
807            }
808            ButtonBorderStyle::Dashed => {
809                if self.border_width > 0.0 {
810                    ops.push(format!("{} w", self.border_width));
811                    ops.push("[3 3] 0 d".to_string()); // Dash pattern
812                    ops.push(crate::graphics::color::stroke_color_op(self.border_color));
813                    ops.push(format!(
814                        "{} {} {} {} re",
815                        self.border_width / 2.0,
816                        self.border_width / 2.0,
817                        width - self.border_width,
818                        height - self.border_width
819                    ));
820                    ops.push("S".to_string());
821                }
822            }
823            ButtonBorderStyle::Beveled | ButtonBorderStyle::Inset => {
824                let is_inset = self.border_style == ButtonBorderStyle::Inset || pressed;
825                let light_color = if is_inset {
826                    self.border_color.darken(0.3)
827                } else {
828                    self.border_color.lighten(0.3)
829                };
830                let dark_color = if is_inset {
831                    self.border_color.lighten(0.3)
832                } else {
833                    self.border_color.darken(0.3)
834                };
835
836                // Top and left edges (light)
837                ops.push(format!("{} w", self.border_width));
838                ops.push(crate::graphics::color::stroke_color_op(light_color));
839                ops.push(format!("{} {} m", 0.0, 0.0));
840                ops.push(format!("{} {} l", 0.0, height));
841                ops.push(format!("{} {} l", width, height));
842                ops.push("S".to_string());
843
844                // Bottom and right edges (dark)
845                ops.push(crate::graphics::color::stroke_color_op(dark_color));
846                ops.push(format!("{} {} m", width, height));
847                ops.push(format!("{} {} l", width, 0.0));
848                ops.push(format!("{} {} l", 0.0, 0.0));
849                ops.push("S".to_string());
850            }
851            ButtonBorderStyle::Underline => {
852                if self.border_width > 0.0 {
853                    ops.push(format!("{} w", self.border_width));
854                    ops.push(crate::graphics::color::stroke_color_op(self.border_color));
855                    ops.push(format!("{} {} m", 0.0, self.border_width / 2.0));
856                    ops.push(format!("{} {} l", width, self.border_width / 2.0));
857                    ops.push("S".to_string());
858                }
859            }
860        }
861    }
862}
863
864impl Clone for PushButtonAppearanceGenerator {
865    fn clone(&self) -> Self {
866        Self {
867            caption: self.caption.clone(),
868            font: self.font.clone(),
869            font_size: self.font_size,
870            text_color: self.text_color,
871            background_color: self.background_color,
872            border_color: self.border_color,
873            border_width: self.border_width,
874            size: self.size,
875            border_style: self.border_style,
876        }
877    }
878}
879
880impl Color {
881    pub fn lighten(&self, amount: f64) -> Color {
882        Color::rgb(
883            (self.r() + amount).min(1.0),
884            (self.g() + amount).min(1.0),
885            (self.b() + amount).min(1.0),
886        )
887    }
888
889    pub fn darken(&self, amount: f64) -> Color {
890        Color::rgb(
891            (self.r() - amount).max(0.0),
892            (self.g() - amount).max(0.0),
893            (self.b() - amount).max(0.0),
894        )
895    }
896
897    pub fn to_array(&self) -> Object {
898        Object::Array(vec![
899            Object::Real(self.r()),
900            Object::Real(self.g()),
901            Object::Real(self.b()),
902        ])
903    }
904}