use crate::error::Result;
use crate::forms::{BorderStyle, FieldType, Widget};
use crate::graphics::Color;
use crate::objects::{Dictionary, Object, Stream};
use crate::text::Font;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AppearanceState {
Normal,
Rollover,
Down,
}
impl AppearanceState {
pub fn pdf_name(&self) -> &'static str {
match self {
AppearanceState::Normal => "N",
AppearanceState::Rollover => "R",
AppearanceState::Down => "D",
}
}
}
#[derive(Debug, Clone)]
pub struct AppearanceStream {
pub content: Vec<u8>,
pub resources: Dictionary,
pub bbox: [f64; 4],
}
impl AppearanceStream {
pub fn new(content: Vec<u8>, bbox: [f64; 4]) -> Self {
Self {
content,
resources: Dictionary::new(),
bbox,
}
}
pub fn with_resources(mut self, resources: Dictionary) -> Self {
self.resources = resources;
self
}
pub fn to_stream(&self) -> Stream {
let mut dict = Dictionary::new();
dict.set("Type", Object::Name("XObject".to_string()));
dict.set("Subtype", Object::Name("Form".to_string()));
let bbox_array = vec![
Object::Real(self.bbox[0]),
Object::Real(self.bbox[1]),
Object::Real(self.bbox[2]),
Object::Real(self.bbox[3]),
];
dict.set("BBox", Object::Array(bbox_array));
if !self.resources.is_empty() {
dict.set("Resources", Object::Dictionary(self.resources.clone()));
}
Stream::with_dictionary(dict, self.content.clone())
}
}
#[derive(Debug, Clone)]
pub struct AppearanceDictionary {
appearances: HashMap<AppearanceState, AppearanceStream>,
down_appearances: HashMap<String, AppearanceStream>,
}
impl AppearanceDictionary {
pub fn new() -> Self {
Self {
appearances: HashMap::new(),
down_appearances: HashMap::new(),
}
}
pub fn set_appearance(&mut self, state: AppearanceState, stream: AppearanceStream) {
self.appearances.insert(state, stream);
}
pub fn set_down_appearance(&mut self, value: String, stream: AppearanceStream) {
self.down_appearances.insert(value, stream);
}
pub fn get_appearance(&self, state: AppearanceState) -> Option<&AppearanceStream> {
self.appearances.get(&state)
}
pub fn to_dict(&self) -> Dictionary {
let mut dict = Dictionary::new();
for (state, stream) in &self.appearances {
let stream_obj = stream.to_stream();
dict.set(
state.pdf_name(),
Object::Stream(stream_obj.dictionary().clone(), stream_obj.data().to_vec()),
);
}
if !self.down_appearances.is_empty() {
let mut down_dict = Dictionary::new();
for (value, stream) in &self.down_appearances {
let stream_obj = stream.to_stream();
down_dict.set(
value,
Object::Stream(stream_obj.dictionary().clone(), stream_obj.data().to_vec()),
);
}
dict.set("D", Object::Dictionary(down_dict));
}
dict
}
}
impl Default for AppearanceDictionary {
fn default() -> Self {
Self::new()
}
}
pub trait AppearanceGenerator {
fn generate_appearance(
&self,
widget: &Widget,
value: Option<&str>,
state: AppearanceState,
) -> Result<AppearanceStream>;
}
pub struct TextFieldAppearance {
pub font: Font,
pub font_size: f64,
pub text_color: Color,
pub justification: i32,
pub multiline: bool,
}
impl Default for TextFieldAppearance {
fn default() -> Self {
Self {
font: Font::Helvetica,
font_size: 12.0,
text_color: Color::black(),
justification: 0,
multiline: false,
}
}
}
impl AppearanceGenerator for TextFieldAppearance {
fn generate_appearance(
&self,
widget: &Widget,
value: Option<&str>,
_state: AppearanceState,
) -> Result<AppearanceStream> {
let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
let mut content = String::new();
content.push_str("q\n");
if let Some(bg_color) = &widget.appearance.background_color {
match bg_color {
Color::Gray(g) => content.push_str(&format!("{g} g\n")),
Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
}
content.push_str(&format!("0 0 {width} {height} re f\n"));
}
if let Some(border_color) = &widget.appearance.border_color {
match border_color {
Color::Gray(g) => content.push_str(&format!("{g} G\n")),
Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} RG\n")),
Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} K\n")),
}
content.push_str(&format!("{} w\n", widget.appearance.border_width));
match widget.appearance.border_style {
BorderStyle::Solid => {
content.push_str(&format!("0 0 {width} {height} re S\n"));
}
BorderStyle::Dashed => {
content.push_str("[3 2] 0 d\n");
content.push_str(&format!("0 0 {width} {height} re S\n"));
}
BorderStyle::Beveled | BorderStyle::Inset => {
content.push_str(&format!("0 0 {width} {height} re S\n"));
}
BorderStyle::Underline => {
content.push_str(&format!("0 0 m {width} 0 l S\n"));
}
}
}
if let Some(text) = value {
match self.text_color {
Color::Gray(g) => content.push_str(&format!("{g} g\n")),
Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
}
content.push_str("BT\n");
content.push_str(&format!(
"/{} {} Tf\n",
self.font.pdf_name(),
self.font_size
));
let padding = 2.0;
let text_y = (height - self.font_size) / 2.0 + self.font_size * 0.3;
let text_x = match self.justification {
1 => width / 2.0, 2 => width - padding, _ => padding, };
content.push_str(&format!("{text_x} {text_y} Td\n"));
let escaped_text = text
.replace('\\', "\\\\")
.replace('(', "\\(")
.replace(')', "\\)");
content.push_str(&format!("({escaped_text}) Tj\n"));
content.push_str("ET\n");
}
content.push_str("Q\n");
let mut resources = Dictionary::new();
let mut font_dict = Dictionary::new();
let mut font_res = Dictionary::new();
font_res.set("Type", Object::Name("Font".to_string()));
font_res.set("Subtype", Object::Name("Type1".to_string()));
font_res.set("BaseFont", Object::Name(self.font.pdf_name()));
font_dict.set(self.font.pdf_name(), Object::Dictionary(font_res));
resources.set("Font", Object::Dictionary(font_dict));
let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height])
.with_resources(resources);
Ok(stream)
}
}
pub struct CheckBoxAppearance {
pub check_style: CheckStyle,
pub check_color: Color,
}
#[derive(Debug, Clone, Copy)]
pub enum CheckStyle {
Check,
Cross,
Square,
Circle,
Star,
}
impl Default for CheckBoxAppearance {
fn default() -> Self {
Self {
check_style: CheckStyle::Check,
check_color: Color::black(),
}
}
}
impl AppearanceGenerator for CheckBoxAppearance {
fn generate_appearance(
&self,
widget: &Widget,
value: Option<&str>,
_state: AppearanceState,
) -> Result<AppearanceStream> {
let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
let is_checked = value.is_some_and(|v| v == "Yes" || v == "On" || v == "true");
let mut content = String::new();
content.push_str("q\n");
if let Some(bg_color) = &widget.appearance.background_color {
match bg_color {
Color::Gray(g) => content.push_str(&format!("{g} g\n")),
Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
}
content.push_str(&format!("0 0 {width} {height} re f\n"));
}
if let Some(border_color) = &widget.appearance.border_color {
match border_color {
Color::Gray(g) => content.push_str(&format!("{g} G\n")),
Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} RG\n")),
Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} K\n")),
}
content.push_str(&format!("{} w\n", widget.appearance.border_width));
content.push_str(&format!("0 0 {width} {height} re S\n"));
}
if is_checked {
match self.check_color {
Color::Gray(g) => content.push_str(&format!("{g} g\n")),
Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
}
let inset = width * 0.2;
match self.check_style {
CheckStyle::Check => {
content.push_str(&format!("{} {} m\n", inset, height * 0.5));
content.push_str(&format!("{} {} l\n", width * 0.4, inset));
content.push_str(&format!("{} {} l\n", width - inset, height - inset));
content.push_str("3 w S\n");
}
CheckStyle::Cross => {
content.push_str(&format!("{inset} {inset} m\n"));
content.push_str(&format!("{} {} l\n", width - inset, height - inset));
content.push_str(&format!("{} {inset} m\n", width - inset));
content.push_str(&format!("{inset} {} l\n", height - inset));
content.push_str("2 w S\n");
}
CheckStyle::Square => {
content.push_str(&format!(
"{inset} {inset} {} {} re f\n",
width - 2.0 * inset,
height - 2.0 * inset
));
}
CheckStyle::Circle => {
let cx = width / 2.0;
let cy = height / 2.0;
let r = (width.min(height) - 2.0 * inset) / 2.0;
let k = 0.552284749831;
content.push_str(&format!("{} {} m\n", cx + r, cy));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx + r,
cy + k * r,
cx + k * r,
cy + r,
cx,
cy + r
));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx - k * r,
cy + r,
cx - r,
cy + k * r,
cx - r,
cy
));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx - r,
cy - k * r,
cx - k * r,
cy - r,
cx,
cy - r
));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx + k * r,
cy - r,
cx + r,
cy - k * r,
cx + r,
cy
));
content.push_str("f\n");
}
CheckStyle::Star => {
let cx = width / 2.0;
let cy = height / 2.0;
let r = (width.min(height) - 2.0 * inset) / 2.0;
for i in 0..5 {
let angle = std::f64::consts::PI * 2.0 * i as f64 / 5.0
- std::f64::consts::PI / 2.0;
let x = cx + r * angle.cos();
let y = cy + r * angle.sin();
if i == 0 {
content.push_str(&format!("{x} {y} m\n"));
} else {
content.push_str(&format!("{x} {y} l\n"));
}
}
content.push_str("f\n");
}
}
}
content.push_str("Q\n");
let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height]);
Ok(stream)
}
}
pub struct RadioButtonAppearance {
pub selected_color: Color,
}
impl Default for RadioButtonAppearance {
fn default() -> Self {
Self {
selected_color: Color::black(),
}
}
}
impl AppearanceGenerator for RadioButtonAppearance {
fn generate_appearance(
&self,
widget: &Widget,
value: Option<&str>,
_state: AppearanceState,
) -> Result<AppearanceStream> {
let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
let is_selected = value.is_some_and(|v| v == "Yes" || v == "On" || v == "true");
let mut content = String::new();
content.push_str("q\n");
if let Some(bg_color) = &widget.appearance.background_color {
match bg_color {
Color::Gray(g) => content.push_str(&format!("{g} g\n")),
Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
}
} else {
content.push_str("1 g\n"); }
let cx = width / 2.0;
let cy = height / 2.0;
let r = width.min(height) / 2.0 - widget.appearance.border_width;
let k = 0.552284749831;
content.push_str(&format!("{} {} m\n", cx + r, cy));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx + r,
cy + k * r,
cx + k * r,
cy + r,
cx,
cy + r
));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx - k * r,
cy + r,
cx - r,
cy + k * r,
cx - r,
cy
));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx - r,
cy - k * r,
cx - k * r,
cy - r,
cx,
cy - r
));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx + k * r,
cy - r,
cx + r,
cy - k * r,
cx + r,
cy
));
content.push_str("f\n");
if let Some(border_color) = &widget.appearance.border_color {
match border_color {
Color::Gray(g) => content.push_str(&format!("{g} G\n")),
Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} RG\n")),
Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} K\n")),
}
content.push_str(&format!("{} w\n", widget.appearance.border_width));
content.push_str(&format!("{} {} m\n", cx + r, cy));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx + r,
cy + k * r,
cx + k * r,
cy + r,
cx,
cy + r
));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx - k * r,
cy + r,
cx - r,
cy + k * r,
cx - r,
cy
));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx - r,
cy - k * r,
cx - k * r,
cy - r,
cx,
cy - r
));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx + k * r,
cy - r,
cx + r,
cy - k * r,
cx + r,
cy
));
content.push_str("S\n");
}
if is_selected {
match self.selected_color {
Color::Gray(g) => content.push_str(&format!("{g} g\n")),
Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
}
let inner_r = r * 0.4;
content.push_str(&format!("{} {} m\n", cx + inner_r, cy));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx + inner_r,
cy + k * inner_r,
cx + k * inner_r,
cy + inner_r,
cx,
cy + inner_r
));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx - k * inner_r,
cy + inner_r,
cx - inner_r,
cy + k * inner_r,
cx - inner_r,
cy
));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx - inner_r,
cy - k * inner_r,
cx - k * inner_r,
cy - inner_r,
cx,
cy - inner_r
));
content.push_str(&format!(
"{} {} {} {} {} {} c\n",
cx + k * inner_r,
cy - inner_r,
cx + inner_r,
cy - k * inner_r,
cx + inner_r,
cy
));
content.push_str("f\n");
}
content.push_str("Q\n");
let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height]);
Ok(stream)
}
}
pub struct PushButtonAppearance {
pub label: String,
pub font: Font,
pub font_size: f64,
pub text_color: Color,
}
impl Default for PushButtonAppearance {
fn default() -> Self {
Self {
label: String::new(),
font: Font::Helvetica,
font_size: 12.0,
text_color: Color::black(),
}
}
}
impl AppearanceGenerator for PushButtonAppearance {
fn generate_appearance(
&self,
widget: &Widget,
_value: Option<&str>,
state: AppearanceState,
) -> Result<AppearanceStream> {
let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
let mut content = String::new();
content.push_str("q\n");
let bg_color = match state {
AppearanceState::Down => Color::gray(0.8),
AppearanceState::Rollover => Color::gray(0.95),
AppearanceState::Normal => widget
.appearance
.background_color
.unwrap_or(Color::gray(0.9)),
};
match bg_color {
Color::Gray(g) => content.push_str(&format!("{g} g\n")),
Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
}
content.push_str(&format!("0 0 {width} {height} re f\n"));
if matches!(widget.appearance.border_style, BorderStyle::Beveled) {
content.push_str("0.9 G\n");
content.push_str("2 w\n");
content.push_str(&format!("0 {height} m {width} {height} l\n"));
content.push_str(&format!("{width} {height} l {width} 0 l S\n"));
content.push_str("0.3 G\n");
content.push_str(&format!("0 0 m {width} 0 l\n"));
content.push_str(&format!("0 0 l 0 {height} l S\n"));
} else {
if let Some(border_color) = &widget.appearance.border_color {
match border_color {
Color::Gray(g) => content.push_str(&format!("{g} G\n")),
Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} RG\n")),
Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} K\n")),
}
content.push_str(&format!("{} w\n", widget.appearance.border_width));
content.push_str(&format!("0 0 {width} {height} re S\n"));
}
}
if !self.label.is_empty() {
match self.text_color {
Color::Gray(g) => content.push_str(&format!("{g} g\n")),
Color::Rgb(r, g, b) => content.push_str(&format!("{r} {g} {b} rg\n")),
Color::Cmyk(c, m, y, k) => content.push_str(&format!("{c} {m} {y} {k} k\n")),
}
content.push_str("BT\n");
content.push_str(&format!(
"/{} {} Tf\n",
self.font.pdf_name(),
self.font_size
));
let text_x = width / 4.0; let text_y = (height - self.font_size) / 2.0 + self.font_size * 0.3;
content.push_str(&format!("{text_x} {text_y} Td\n"));
let escaped_label = self
.label
.replace('\\', "\\\\")
.replace('(', "\\(")
.replace(')', "\\)");
content.push_str(&format!("({escaped_label}) Tj\n"));
content.push_str("ET\n");
}
content.push_str("Q\n");
let mut resources = Dictionary::new();
let mut font_dict = Dictionary::new();
let mut font_res = Dictionary::new();
font_res.set("Type", Object::Name("Font".to_string()));
font_res.set("Subtype", Object::Name("Type1".to_string()));
font_res.set("BaseFont", Object::Name(self.font.pdf_name()));
font_dict.set(self.font.pdf_name(), Object::Dictionary(font_res));
resources.set("Font", Object::Dictionary(font_dict));
let stream = AppearanceStream::new(content.into_bytes(), [0.0, 0.0, width, height])
.with_resources(resources);
Ok(stream)
}
}
#[derive(Debug, Clone)]
pub struct ComboBoxAppearance {
pub font: Font,
pub font_size: f64,
pub text_color: Color,
pub selected_text: Option<String>,
pub show_arrow: bool,
}
impl Default for ComboBoxAppearance {
fn default() -> Self {
Self {
font: Font::Helvetica,
font_size: 12.0,
text_color: Color::black(),
selected_text: None,
show_arrow: true,
}
}
}
impl AppearanceGenerator for ComboBoxAppearance {
fn generate_appearance(
&self,
widget: &Widget,
value: Option<&str>,
_state: AppearanceState,
) -> Result<AppearanceStream> {
let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
let mut content = String::new();
content.push_str("1 1 1 rg\n"); content.push_str(&format!("0 0 {} {} re\n", width, height));
content.push_str("f\n");
if let Some(ref border_color) = widget.appearance.border_color {
match border_color {
Color::Gray(g) => content.push_str(&format!("{} G\n", g)),
Color::Rgb(r, g, b) => content.push_str(&format!("{} {} {} RG\n", r, g, b)),
Color::Cmyk(c, m, y, k) => {
content.push_str(&format!("{} {} {} {} K\n", c, m, y, k))
}
}
content.push_str(&format!("{} w\n", widget.appearance.border_width));
content.push_str(&format!("0 0 {} {} re\n", width, height));
content.push_str("S\n");
}
if self.show_arrow {
let arrow_x = width - 15.0;
let arrow_y = height / 2.0;
content.push_str("0.5 0.5 0.5 rg\n"); content.push_str(&format!("{} {} m\n", arrow_x, arrow_y + 3.0));
content.push_str(&format!("{} {} l\n", arrow_x + 8.0, arrow_y + 3.0));
content.push_str(&format!("{} {} l\n", arrow_x + 4.0, arrow_y - 3.0));
content.push_str("f\n");
}
let text_to_show = value.or(self.selected_text.as_deref());
if let Some(text) = text_to_show {
content.push_str("BT\n");
content.push_str(&format!(
"/{} {} Tf\n",
self.font.pdf_name(),
self.font_size
));
match self.text_color {
Color::Gray(g) => content.push_str(&format!("{} g\n", g)),
Color::Rgb(r, g, b) => content.push_str(&format!("{} {} {} rg\n", r, g, b)),
Color::Cmyk(c, m, y, k) => {
content.push_str(&format!("{} {} {} {} k\n", c, m, y, k))
}
}
content.push_str(&format!("5 {} Td\n", (height - self.font_size) / 2.0));
let escaped = text
.replace('\\', "\\\\")
.replace('(', "\\(")
.replace(')', "\\)")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
content.push_str(&format!("({}) Tj\n", escaped));
content.push_str("ET\n");
}
let bbox = [0.0, 0.0, width, height];
Ok(AppearanceStream::new(content.into_bytes(), bbox))
}
}
#[derive(Debug, Clone)]
pub struct ListBoxAppearance {
pub font: Font,
pub font_size: f64,
pub text_color: Color,
pub selection_color: Color,
pub options: Vec<String>,
pub selected: Vec<usize>,
pub item_height: f64,
}
impl Default for ListBoxAppearance {
fn default() -> Self {
Self {
font: Font::Helvetica,
font_size: 12.0,
text_color: Color::black(),
selection_color: Color::rgb(0.2, 0.4, 0.8),
options: Vec::new(),
selected: Vec::new(),
item_height: 16.0,
}
}
}
impl AppearanceGenerator for ListBoxAppearance {
fn generate_appearance(
&self,
widget: &Widget,
_value: Option<&str>,
_state: AppearanceState,
) -> Result<AppearanceStream> {
let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
let mut content = String::new();
content.push_str("1 1 1 rg\n"); content.push_str(&format!("0 0 {} {} re\n", width, height));
content.push_str("f\n");
if let Some(ref border_color) = widget.appearance.border_color {
match border_color {
Color::Gray(g) => content.push_str(&format!("{} G\n", g)),
Color::Rgb(r, g, b) => content.push_str(&format!("{} {} {} RG\n", r, g, b)),
Color::Cmyk(c, m, y, k) => {
content.push_str(&format!("{} {} {} {} K\n", c, m, y, k))
}
}
content.push_str(&format!("{} w\n", widget.appearance.border_width));
content.push_str(&format!("0 0 {} {} re\n", width, height));
content.push_str("S\n");
}
let mut y = height - self.item_height;
for (index, option) in self.options.iter().enumerate() {
if y < 0.0 {
break; }
if self.selected.contains(&index) {
match self.selection_color {
Color::Gray(g) => content.push_str(&format!("{} g\n", g)),
Color::Rgb(r, g, b) => content.push_str(&format!("{} {} {} rg\n", r, g, b)),
Color::Cmyk(c, m, y_val, k) => {
content.push_str(&format!("{} {} {} {} k\n", c, m, y_val, k))
}
}
content.push_str(&format!("0 {} {} {} re\n", y, width, self.item_height));
content.push_str("f\n");
}
content.push_str("BT\n");
content.push_str(&format!(
"/{} {} Tf\n",
self.font.pdf_name(),
self.font_size
));
if self.selected.contains(&index) {
content.push_str("1 1 1 rg\n");
} else {
match self.text_color {
Color::Gray(g) => content.push_str(&format!("{} g\n", g)),
Color::Rgb(r, g, b) => content.push_str(&format!("{} {} {} rg\n", r, g, b)),
Color::Cmyk(c, m, y_val, k) => {
content.push_str(&format!("{} {} {} {} k\n", c, m, y_val, k))
}
}
}
content.push_str(&format!("5 {} Td\n", y + 2.0));
let escaped = option
.replace('\\', "\\\\")
.replace('(', "\\(")
.replace(')', "\\)")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
content.push_str(&format!("({}) Tj\n", escaped));
content.push_str("ET\n");
y -= self.item_height;
}
let bbox = [0.0, 0.0, width, height];
Ok(AppearanceStream::new(content.into_bytes(), bbox))
}
}
pub fn generate_default_appearance(
field_type: FieldType,
widget: &Widget,
value: Option<&str>,
) -> Result<AppearanceStream> {
match field_type {
FieldType::Text => {
let generator = TextFieldAppearance::default();
generator.generate_appearance(widget, value, AppearanceState::Normal)
}
FieldType::Button => {
let generator = CheckBoxAppearance::default();
generator.generate_appearance(widget, value, AppearanceState::Normal)
}
FieldType::Choice => {
let generator = ComboBoxAppearance::default();
generator.generate_appearance(widget, value, AppearanceState::Normal)
}
FieldType::Signature => {
let width = widget.rect.upper_right.x - widget.rect.lower_left.x;
let height = widget.rect.upper_right.y - widget.rect.lower_left.y;
Ok(AppearanceStream::new(
b"q\nQ\n".to_vec(),
[0.0, 0.0, width, height],
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::{Point, Rectangle};
#[test]
fn test_appearance_state_names() {
assert_eq!(AppearanceState::Normal.pdf_name(), "N");
assert_eq!(AppearanceState::Rollover.pdf_name(), "R");
assert_eq!(AppearanceState::Down.pdf_name(), "D");
}
#[test]
fn test_appearance_stream_creation() {
let content = b"q\n1 0 0 RG\n0 0 100 50 re S\nQ\n";
let stream = AppearanceStream::new(content.to_vec(), [0.0, 0.0, 100.0, 50.0]);
assert_eq!(stream.content, content);
assert_eq!(stream.bbox, [0.0, 0.0, 100.0, 50.0]);
assert!(stream.resources.is_empty());
}
#[test]
fn test_appearance_stream_with_resources() {
let mut resources = Dictionary::new();
resources.set("Font", Object::Name("F1".to_string()));
let content = b"BT\n/F1 12 Tf\n(Test) Tj\nET\n";
let stream = AppearanceStream::new(content.to_vec(), [0.0, 0.0, 100.0, 50.0])
.with_resources(resources.clone());
assert_eq!(stream.resources, resources);
}
#[test]
fn test_appearance_dictionary() {
let mut app_dict = AppearanceDictionary::new();
let normal_stream = AppearanceStream::new(b"normal".to_vec(), [0.0, 0.0, 10.0, 10.0]);
let down_stream = AppearanceStream::new(b"down".to_vec(), [0.0, 0.0, 10.0, 10.0]);
app_dict.set_appearance(AppearanceState::Normal, normal_stream);
app_dict.set_appearance(AppearanceState::Down, down_stream);
assert!(app_dict.get_appearance(AppearanceState::Normal).is_some());
assert!(app_dict.get_appearance(AppearanceState::Down).is_some());
assert!(app_dict.get_appearance(AppearanceState::Rollover).is_none());
}
#[test]
fn test_text_field_appearance() {
let widget = Widget::new(Rectangle {
lower_left: Point { x: 0.0, y: 0.0 },
upper_right: Point { x: 200.0, y: 30.0 },
});
let generator = TextFieldAppearance::default();
let result =
generator.generate_appearance(&widget, Some("Test Text"), AppearanceState::Normal);
assert!(result.is_ok());
let stream = result.unwrap();
assert_eq!(stream.bbox, [0.0, 0.0, 200.0, 30.0]);
let content = String::from_utf8_lossy(&stream.content);
assert!(content.contains("BT"));
assert!(content.contains("(Test Text) Tj"));
assert!(content.contains("ET"));
}
#[test]
fn test_checkbox_appearance_checked() {
let widget = Widget::new(Rectangle {
lower_left: Point { x: 0.0, y: 0.0 },
upper_right: Point { x: 20.0, y: 20.0 },
});
let generator = CheckBoxAppearance::default();
let result = generator.generate_appearance(&widget, Some("Yes"), AppearanceState::Normal);
assert!(result.is_ok());
let stream = result.unwrap();
let content = String::from_utf8_lossy(&stream.content);
assert!(content.contains(" m"));
assert!(content.contains(" l"));
assert!(content.contains(" S"));
}
#[test]
fn test_checkbox_appearance_unchecked() {
let widget = Widget::new(Rectangle {
lower_left: Point { x: 0.0, y: 0.0 },
upper_right: Point { x: 20.0, y: 20.0 },
});
let generator = CheckBoxAppearance::default();
let result = generator.generate_appearance(&widget, Some("No"), AppearanceState::Normal);
assert!(result.is_ok());
let stream = result.unwrap();
let content = String::from_utf8_lossy(&stream.content);
assert!(content.contains("q"));
assert!(content.contains("Q"));
}
#[test]
fn test_radio_button_appearance() {
let widget = Widget::new(Rectangle {
lower_left: Point { x: 0.0, y: 0.0 },
upper_right: Point { x: 20.0, y: 20.0 },
});
let generator = RadioButtonAppearance::default();
let result = generator.generate_appearance(&widget, Some("Yes"), AppearanceState::Normal);
assert!(result.is_ok());
let stream = result.unwrap();
let content = String::from_utf8_lossy(&stream.content);
assert!(
content.contains(" c"),
"Content should contain curve commands"
);
assert!(
content.contains("f\n"),
"Content should contain fill commands"
);
}
#[test]
fn test_push_button_appearance() {
let mut generator = PushButtonAppearance::default();
generator.label = "Click Me".to_string();
let widget = Widget::new(Rectangle {
lower_left: Point { x: 0.0, y: 0.0 },
upper_right: Point { x: 100.0, y: 30.0 },
});
let result = generator.generate_appearance(&widget, None, AppearanceState::Normal);
assert!(result.is_ok());
let stream = result.unwrap();
let content = String::from_utf8_lossy(&stream.content);
assert!(content.contains("(Click Me) Tj"));
assert!(!stream.resources.is_empty());
}
#[test]
fn test_push_button_states() {
let generator = PushButtonAppearance::default();
let widget = Widget::new(Rectangle {
lower_left: Point { x: 0.0, y: 0.0 },
upper_right: Point { x: 100.0, y: 30.0 },
});
let normal = generator
.generate_appearance(&widget, None, AppearanceState::Normal)
.unwrap();
let down = generator
.generate_appearance(&widget, None, AppearanceState::Down)
.unwrap();
let rollover = generator
.generate_appearance(&widget, None, AppearanceState::Rollover)
.unwrap();
assert_ne!(normal.content, down.content);
assert_ne!(normal.content, rollover.content);
assert_ne!(down.content, rollover.content);
}
#[test]
fn test_check_styles() {
let widget = Widget::new(Rectangle {
lower_left: Point { x: 0.0, y: 0.0 },
upper_right: Point { x: 20.0, y: 20.0 },
});
for style in [
CheckStyle::Check,
CheckStyle::Cross,
CheckStyle::Square,
CheckStyle::Circle,
CheckStyle::Star,
] {
let mut generator = CheckBoxAppearance::default();
generator.check_style = style;
let result =
generator.generate_appearance(&widget, Some("Yes"), AppearanceState::Normal);
assert!(result.is_ok(), "Failed for style {:?}", style);
}
}
#[test]
fn test_appearance_state_pdf_names() {
assert_eq!(AppearanceState::Normal.pdf_name(), "N");
assert_eq!(AppearanceState::Rollover.pdf_name(), "R");
assert_eq!(AppearanceState::Down.pdf_name(), "D");
}
#[test]
fn test_appearance_stream_creation_advanced() {
let content = b"q 1 0 0 1 0 0 cm Q".to_vec();
let bbox = [0.0, 0.0, 100.0, 50.0];
let stream = AppearanceStream::new(content.clone(), bbox);
assert_eq!(stream.content, content);
assert_eq!(stream.bbox, bbox);
assert!(stream.resources.is_empty());
}
#[test]
fn test_appearance_stream_with_resources_advanced() {
let mut resources = Dictionary::new();
resources.set("Font", Object::Dictionary(Dictionary::new()));
let stream =
AppearanceStream::new(vec![], [0.0, 0.0, 10.0, 10.0]).with_resources(resources.clone());
assert_eq!(stream.resources, resources);
}
#[test]
fn test_appearance_dictionary_new() {
let dict = AppearanceDictionary::new();
assert!(dict.appearances.is_empty());
assert!(dict.down_appearances.is_empty());
}
#[test]
fn test_appearance_dictionary_set_get() {
let mut dict = AppearanceDictionary::new();
let stream = AppearanceStream::new(vec![1, 2, 3], [0.0, 0.0, 10.0, 10.0]);
dict.set_appearance(AppearanceState::Normal, stream);
assert!(dict.get_appearance(AppearanceState::Normal).is_some());
assert!(dict.get_appearance(AppearanceState::Down).is_none());
}
#[test]
fn test_text_field_multiline() {
let mut generator = TextFieldAppearance::default();
generator.multiline = true;
let widget = Widget::new(Rectangle {
lower_left: Point { x: 0.0, y: 0.0 },
upper_right: Point { x: 200.0, y: 100.0 },
});
let text = "Line 1\nLine 2\nLine 3";
let result = generator.generate_appearance(&widget, Some(text), AppearanceState::Normal);
assert!(result.is_ok());
}
#[test]
fn test_appearance_with_custom_colors() {
let mut generator = TextFieldAppearance::default();
generator.text_color = Color::rgb(1.0, 0.0, 0.0); generator.font_size = 14.0;
generator.justification = 1;
let widget = Widget::new(Rectangle {
lower_left: Point { x: 0.0, y: 0.0 },
upper_right: Point { x: 100.0, y: 30.0 },
});
let result =
generator.generate_appearance(&widget, Some("Colored"), AppearanceState::Normal);
assert!(result.is_ok());
}
}