pdfcrate 0.1.1

A Rust library for creating and manipulating PDF documents
Documentation
//! Form field PDF object generation
//!
//! This module handles creating PDF dictionaries for form fields.

use super::{FieldType, FormField, TextAlign};
use crate::objects::{PdfArray, PdfDict, PdfName, PdfObject, PdfRef, PdfString};

/// Checkbox appearance references (Off state and Yes state)
pub struct CheckboxAppearanceRefs {
    pub off_ref: PdfRef,
    pub yes_ref: PdfRef,
}

/// Creates the widget annotation dictionary for a form field
///
/// For checkboxes, pass `checkbox_ap` with Off/Yes appearance refs to generate
/// the correct `/AP << /N << /Yes ref /Off ref >> >>` structure and `/AS` entry.
/// For other field types, pass `appearance_ref` as a single normal appearance stream.
pub fn create_widget_annotation(
    field: &FormField,
    page_ref: PdfRef,
    appearance_ref: Option<PdfRef>,
    checkbox_ap: Option<CheckboxAppearanceRefs>,
) -> PdfDict {
    let mut dict = PdfDict::new();

    // Type and Subtype
    dict.set("Type", PdfObject::Name(PdfName::new("Annot")));
    dict.set("Subtype", PdfObject::Name(PdfName::new("Widget")));

    // Rectangle
    let rect = PdfArray::from(vec![
        PdfObject::Real(field.rect[0]),
        PdfObject::Real(field.rect[1]),
        PdfObject::Real(field.rect[2]),
        PdfObject::Real(field.rect[3]),
    ]);
    dict.set("Rect", PdfObject::Array(rect));

    // Page reference
    dict.set("P", PdfObject::Reference(page_ref));

    // Field name (use from_text for proper Unicode encoding)
    dict.set("T", PdfObject::String(PdfString::from_text(&field.name)));

    // Field type
    dict.set(
        "FT",
        PdfObject::Name(PdfName::new(field.field_type.pdf_name())),
    );

    // Flags
    if field.flags.value() != 0 {
        dict.set("Ff", PdfObject::Integer(field.flags.value() as i64));
    }

    // Value - type depends on field type per PDF spec
    // Text/Choice fields: string; Checkbox/Radio: name (Yes/Off)
    if let Some(ref value) = field.value {
        let v_obj = match field.field_type {
            FieldType::Text | FieldType::Choice => PdfObject::String(PdfString::from_text(value)),
            FieldType::CheckBox | FieldType::Radio | FieldType::Button => {
                PdfObject::Name(PdfName::new(value))
            }
            FieldType::Signature => PdfObject::Name(PdfName::new(value)),
        };
        dict.set("V", v_obj.clone());
        dict.set("DV", v_obj);
    }

    // Tooltip (use from_text for proper Unicode encoding)
    if let Some(ref tooltip) = field.tooltip {
        dict.set("TU", PdfObject::String(PdfString::from_text(tooltip)));
    }

    // Text alignment (Q)
    if field.align != TextAlign::Left {
        dict.set("Q", PdfObject::Integer(field.align as i64));
    }

    // Max length (for text fields)
    if let Some(max_len) = field.max_length {
        dict.set("MaxLen", PdfObject::Integer(max_len as i64));
    }

    // Options (for choice fields, use from_text for proper Unicode encoding)
    if !field.options.is_empty() {
        let opts: Vec<PdfObject> = field
            .options
            .iter()
            .map(|s| PdfObject::String(PdfString::from_text(s)))
            .collect();
        dict.set("Opt", PdfObject::Array(PdfArray::from(opts)));
    }

    // Default appearance
    let da = format!(
        "/{} {} Tf {} {} {} rg",
        field.font,
        if field.font_size == 0.0 {
            12.0
        } else {
            field.font_size
        },
        field.text_color[0],
        field.text_color[1],
        field.text_color[2]
    );
    dict.set("DA", PdfObject::String(da.into()));

    // Border style - only set if border color is specified
    if field.border_color.is_some() {
        let mut bs = PdfDict::new();
        bs.set("W", PdfObject::Integer(1)); // 1pt border
        bs.set("S", PdfObject::Name(PdfName::new("S"))); // Solid
        dict.set("BS", PdfObject::Dict(bs));
    }

    // Appearance characteristics (MK) - only if needed
    let has_bg = field.background_color.is_some();
    let has_bc = field.border_color.is_some();
    let is_checkbox = field.field_type == FieldType::CheckBox;

    if has_bg || has_bc || is_checkbox {
        let mut mk = PdfDict::new();
        if let Some(ref bg) = field.background_color {
            let bg_arr = PdfArray::from(vec![
                PdfObject::Real(bg[0]),
                PdfObject::Real(bg[1]),
                PdfObject::Real(bg[2]),
            ]);
            mk.set("BG", PdfObject::Array(bg_arr));
        }
        if let Some(ref bc) = field.border_color {
            let bc_arr = PdfArray::from(vec![
                PdfObject::Real(bc[0]),
                PdfObject::Real(bc[1]),
                PdfObject::Real(bc[2]),
            ]);
            mk.set("BC", PdfObject::Array(bc_arr));
        }
        // Caption for checkboxes
        if is_checkbox {
            mk.set("CA", PdfObject::String("4".into())); // Checkmark character
        }
        dict.set("MK", PdfObject::Dict(mk));
    }

    // Appearance dictionary
    if let Some(cb_ap) = checkbox_ap {
        // Checkbox: /AP << /N << /Yes ref /Off ref >> >>
        let mut n_dict = PdfDict::new();
        n_dict.set("Yes", PdfObject::Reference(cb_ap.yes_ref));
        n_dict.set("Off", PdfObject::Reference(cb_ap.off_ref));
        let mut ap = PdfDict::new();
        ap.set("N", PdfObject::Dict(n_dict));
        dict.set("AP", PdfObject::Dict(ap));

        // /AS entry: current appearance state
        let is_checked = field.value.as_deref() == Some("Yes");
        dict.set(
            "AS",
            PdfObject::Name(PdfName::new(if is_checked { "Yes" } else { "Off" })),
        );
    } else if let Some(ap_ref) = appearance_ref {
        let mut ap = PdfDict::new();
        ap.set("N", PdfObject::Reference(ap_ref));
        dict.set("AP", PdfObject::Dict(ap));
    }

    // Annotation flags (print, no zoom, no rotate)
    dict.set("F", PdfObject::Integer(4)); // Print flag

    dict
}

/// Creates the AcroForm dictionary for the document catalog
pub fn create_acro_form_dict(
    field_refs: &[PdfRef],
    font_refs: &[(String, PdfRef)],
    need_appearances: bool,
    default_appearance: Option<&str>,
) -> PdfDict {
    let mut dict = PdfDict::new();

    // Fields array
    let fields: Vec<PdfObject> = field_refs
        .iter()
        .map(|r| PdfObject::Reference(*r))
        .collect();
    dict.set("Fields", PdfObject::Array(PdfArray::from(fields)));

    // NeedAppearances
    if need_appearances {
        dict.set("NeedAppearances", PdfObject::Bool(true));
    }

    // Default appearance
    if let Some(da) = default_appearance {
        dict.set("DA", PdfObject::String(da.into()));
    }

    // Default resources (fonts)
    if !font_refs.is_empty() {
        let mut dr = PdfDict::new();
        let mut font_dict = PdfDict::new();

        for (name, font_ref) in font_refs {
            font_dict.set(name, PdfObject::Reference(*font_ref));
        }

        // Add standard font aliases
        add_standard_font_resources(&mut font_dict);

        dr.set("Font", PdfObject::Dict(font_dict));
        dict.set("DR", PdfObject::Dict(dr));
    }

    dict
}

/// Adds standard font resource aliases commonly used in forms
fn add_standard_font_resources(_font_dict: &mut PdfDict) {
    // These are aliases commonly used in DA strings
    // The actual font objects would need to be created separately
    // For now, we just note that forms often reference these names
}

/// Creates a text field value for the V entry
pub fn create_text_value(text: &str) -> PdfObject {
    PdfObject::String(PdfString::from_text(text))
}

/// Creates a choice field value for the V entry
pub fn create_choice_value(selected: &str) -> PdfObject {
    PdfObject::String(PdfString::from_text(selected))
}

/// Creates a checkbox/radio button value
pub fn create_button_value(checked: bool) -> PdfObject {
    if checked {
        PdfObject::Name(PdfName::new("Yes"))
    } else {
        PdfObject::Name(PdfName::new("Off"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_create_widget_annotation() {
        let field = FormField::text("test", [100.0, 700.0, 300.0, 720.0]);
        let page_ref = PdfRef::new(1);

        let dict = create_widget_annotation(&field, page_ref, None, None);

        assert_eq!(dict.get_type(), Some("Annot"));
        assert!(dict.get("Rect").is_some());
        assert!(dict.get("T").is_some());
    }

    #[test]
    fn test_checkbox_widget_annotation_has_ap_states_and_as() {
        let field = FormField::checkbox("agree", [100.0, 650.0, 120.0, 670.0], true);
        let page_ref = PdfRef::new(1);
        let cb_ap = CheckboxAppearanceRefs {
            off_ref: PdfRef::new(10),
            yes_ref: PdfRef::new(11),
        };

        let dict = create_widget_annotation(&field, page_ref, None, Some(cb_ap));

        // Should have AP dictionary
        let ap = dict.get("AP").expect("missing AP");
        if let PdfObject::Dict(ap_dict) = ap {
            // AP/N should be a dictionary (not a reference)
            let n = ap_dict.get("N").expect("missing AP/N");
            if let PdfObject::Dict(n_dict) = n {
                assert!(n_dict.get("Yes").is_some(), "missing AP/N/Yes");
                assert!(n_dict.get("Off").is_some(), "missing AP/N/Off");
            } else {
                panic!("AP/N should be a Dict, got {:?}", n);
            }
        } else {
            panic!("AP should be a Dict");
        }

        // Should have AS entry
        let as_entry = dict.get("AS").expect("missing AS");
        if let PdfObject::Name(name) = as_entry {
            assert_eq!(name.as_str(), "Yes");
        } else {
            panic!("AS should be a Name");
        }
    }

    #[test]
    fn test_checkbox_unchecked_has_as_off() {
        let field = FormField::checkbox("agree", [100.0, 650.0, 120.0, 670.0], false);
        let page_ref = PdfRef::new(1);
        let cb_ap = CheckboxAppearanceRefs {
            off_ref: PdfRef::new(10),
            yes_ref: PdfRef::new(11),
        };

        let dict = create_widget_annotation(&field, page_ref, None, Some(cb_ap));

        let as_entry = dict.get("AS").expect("missing AS");
        if let PdfObject::Name(name) = as_entry {
            assert_eq!(name.as_str(), "Off");
        } else {
            panic!("AS should be a Name");
        }
    }

    #[test]
    fn test_create_acro_form_dict() {
        let field_refs = vec![PdfRef::new(5), PdfRef::new(6)];
        let font_refs = vec![];

        let dict = create_acro_form_dict(&field_refs, &font_refs, true, Some("/Helv 12 Tf 0 g"));

        assert!(dict.get("Fields").is_some());
        assert!(dict.get("NeedAppearances").is_some());
    }
}