rpdfium-doc 7676.6.4

Document-level features for rpdfium
Documentation
// Derived from PDFium's cpdf_apsettings.h/cpp
// Original: Copyright 2014 The PDFium Authors
// Licensed under BSD-3-Clause / Apache-2.0
// See pdfium-upstream/LICENSE for the original license.

//! Widget annotation appearance characteristics dictionary (`/MK`).
//!
//! The MK dictionary (ISO 32000-2 section 12.5.6.19) specifies the appearance
//! characteristics of a widget annotation used to display an interactive form field.

use std::collections::HashMap;

use rpdfium_core::{Name, PdfSource};
use rpdfium_parser::{Object, ObjectId, ObjectStore};

use crate::icon_fit::{IconFit, parse_icon_fit};

/// Appearance characteristics dictionary for widget annotations.
///
/// Parsed from the `/MK` entry of a widget annotation dictionary.
#[derive(Debug, Clone)]
pub struct MkDict {
    /// Rotation angle (in degrees, multiple of 90). Default 0.
    pub rotation: u32,
    /// Border color array (0, 1, 3, or 4 components).
    pub border_color: Option<Vec<f32>>,
    /// Background color array.
    pub background_color: Option<Vec<f32>>,
    /// Normal caption string (`/CA`).
    pub caption: Option<String>,
    /// Rollover caption string (`/RC`).
    pub rollover_caption: Option<String>,
    /// Alternate (down) caption string (`/AC`).
    pub alt_caption: Option<String>,
    /// Normal icon stream reference (`/I`).
    pub icon_normal: Option<ObjectId>,
    /// Rollover icon stream reference (`/RI`).
    pub icon_rollover: Option<ObjectId>,
    /// Alternate (down) icon stream reference (`/IX`).
    pub icon_alt: Option<ObjectId>,
    /// Icon/caption positioning. Default 0 (caption only).
    pub text_position: u32,
    /// Icon fit parameters (from `/IF` sub-dictionary).
    pub icon_fit: Option<IconFit>,
}

/// Parse an MK dictionary from a widget annotation dictionary.
///
/// Returns `None` if the `/MK` key is absent or cannot be resolved.
pub fn parse_mk_dict<S: PdfSource>(
    dict: &HashMap<Name, Object>,
    store: &ObjectStore<S>,
) -> Option<MkDict> {
    let mk_obj = dict.get(&Name::mk())?;
    let resolved = store.deep_resolve(mk_obj).ok()?;
    let mk_dict = resolved.as_dict()?;

    let rotation = mk_dict
        .get(&Name::r())
        .and_then(|o| o.as_i64())
        .map(|v| v as u32)
        .unwrap_or(0);

    let border_color = mk_dict
        .get(&Name::bc())
        .and_then(|o| store.deep_resolve(o).ok())
        .and_then(parse_float_array);

    let background_color = mk_dict
        .get(&Name::bg_color())
        .and_then(|o| store.deep_resolve(o).ok())
        .and_then(parse_float_array);

    let caption = mk_dict
        .get(&Name::ca_display())
        .and_then(|o| store.deep_resolve(o).ok())
        .and_then(|o| o.as_string().map(|s| s.to_string_lossy()));

    let rollover_caption = mk_dict
        .get(&Name::rc())
        .and_then(|o| store.deep_resolve(o).ok())
        .and_then(|o| o.as_string().map(|s| s.to_string_lossy()));

    let alt_caption = mk_dict
        .get(&Name::ac())
        .and_then(|o| store.deep_resolve(o).ok())
        .and_then(|o| o.as_string().map(|s| s.to_string_lossy()));

    let icon_normal = mk_dict.get(&Name::i()).and_then(|o| o.as_reference());

    let icon_rollover = mk_dict.get(&Name::ri()).and_then(|o| o.as_reference());

    let icon_alt = mk_dict.get(&Name::ix()).and_then(|o| o.as_reference());

    let text_position = mk_dict
        .get(&Name::tp())
        .and_then(|o| o.as_i64())
        .map(|v| v as u32)
        .unwrap_or(0);

    let icon_fit = parse_icon_fit(mk_dict, store);

    Some(MkDict {
        rotation,
        border_color,
        background_color,
        caption,
        rollover_caption,
        alt_caption,
        icon_normal,
        icon_rollover,
        icon_alt,
        text_position,
        icon_fit,
    })
}

impl MkDict {
    /// Return the number of color components in the border color.
    ///
    /// Returns 0 (transparent), 1 (gray), 3 (RGB), or 4 (CMYK).
    pub fn border_color_components(&self) -> usize {
        self.border_color.as_ref().map_or(0, |c| c.len())
    }

    /// Return the number of color components in the background color.
    ///
    /// Returns 0 (transparent), 1 (gray), 3 (RGB), or 4 (CMYK).
    pub fn background_color_components(&self) -> usize {
        self.background_color.as_ref().map_or(0, |c| c.len())
    }
}

/// Parsed icon stream properties (from a Form XObject referenced by `/I`, `/RI`, `/IX`).
///
/// Corresponds to PDFium's `CPDF_Icon` class.
#[derive(Debug, Clone)]
pub struct IconProperties {
    /// Icon image width (from `/BBox`).
    pub width: f32,
    /// Icon image height (from `/BBox`).
    pub height: f32,
    /// Transformation matrix (from `/Matrix`, defaults to identity).
    pub matrix: [f32; 6],
    /// Icon alias name (from `/Name` key).
    pub alias: Option<String>,
}

/// Extract icon properties from a stream dictionary referenced by an ObjectId.
pub fn parse_icon_properties<S: PdfSource>(
    icon_id: ObjectId,
    store: &ObjectStore<S>,
) -> Option<IconProperties> {
    let obj = store.resolve(icon_id).ok()?;
    let dict = match obj {
        Object::Stream { dict, .. } => dict,
        Object::Dictionary(d) => d,
        _ => return None,
    };

    // BBox → width/height
    let (width, height) = dict
        .get(&Name::b_box())
        .and_then(|o| store.deep_resolve(o).ok())
        .and_then(|o| {
            let arr = o.as_array()?;
            if arr.len() >= 4 {
                let left = arr[0].as_f64().unwrap_or(0.0) as f32;
                let bottom = arr[1].as_f64().unwrap_or(0.0) as f32;
                let right = arr[2].as_f64().unwrap_or(0.0) as f32;
                let top = arr[3].as_f64().unwrap_or(0.0) as f32;
                Some((right - left, top - bottom))
            } else {
                None
            }
        })
        .unwrap_or((0.0, 0.0));

    // Matrix
    let matrix = dict
        .get(&Name::matrix())
        .and_then(|o| store.deep_resolve(o).ok())
        .and_then(|o| {
            let arr = o.as_array()?;
            if arr.len() >= 6 {
                Some([
                    arr[0].as_f64().unwrap_or(1.0) as f32,
                    arr[1].as_f64().unwrap_or(0.0) as f32,
                    arr[2].as_f64().unwrap_or(0.0) as f32,
                    arr[3].as_f64().unwrap_or(1.0) as f32,
                    arr[4].as_f64().unwrap_or(0.0) as f32,
                    arr[5].as_f64().unwrap_or(0.0) as f32,
                ])
            } else {
                None
            }
        })
        .unwrap_or([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);

    // Name (alias)
    let alias = dict
        .get(&Name::from("Name"))
        .and_then(|o| o.as_name())
        .map(|n| n.as_str().into_owned());

    Some(IconProperties {
        width,
        height,
        matrix,
        alias,
    })
}

/// Parse a PDF array of numbers into a Vec<f32>.
fn parse_float_array(obj: &Object) -> Option<Vec<f32>> {
    let arr = obj.as_array()?;
    let values: Vec<f32> = arr
        .iter()
        .filter_map(|o| o.as_f64().map(|f| f as f32))
        .collect();
    if values.is_empty() && !arr.is_empty() {
        return None;
    }
    Some(values)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::icon_fit::ScaleMethod;
    use rpdfium_core::PdfString;

    fn build_store() -> ObjectStore<Vec<u8>> {
        let pdf = build_minimal_pdf();
        ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
    }

    fn build_minimal_pdf() -> Vec<u8> {
        let mut pdf = Vec::new();
        pdf.extend_from_slice(b"%PDF-1.4\n");
        let obj1_offset = pdf.len();
        pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
        let obj2_offset = pdf.len();
        pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
        let xref_offset = pdf.len();
        pdf.extend_from_slice(b"xref\n0 3\n");
        pdf.extend_from_slice(b"0000000000 65535 f \r\n");
        pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
        pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
        pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
        pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
        pdf
    }

    #[test]
    fn test_parse_mk_dict_with_colors_and_caption() {
        let store = build_store();

        let mut mk = HashMap::new();
        mk.insert(Name::r(), Object::Integer(90));
        mk.insert(
            Name::bc(),
            Object::Array(vec![
                Object::Real(1.0),
                Object::Real(0.0),
                Object::Real(0.0),
            ]),
        );
        mk.insert(
            Name::bg_color(),
            Object::Array(vec![
                Object::Real(0.9),
                Object::Real(0.9),
                Object::Real(0.9),
            ]),
        );
        mk.insert(
            Name::ca_display(),
            Object::String(PdfString::from_bytes(b"Submit".to_vec())),
        );
        mk.insert(
            Name::rc(),
            Object::String(PdfString::from_bytes(b"Hover".to_vec())),
        );
        mk.insert(
            Name::ac(),
            Object::String(PdfString::from_bytes(b"Pressed".to_vec())),
        );
        mk.insert(Name::tp(), Object::Integer(2));

        let mut dict = HashMap::new();
        dict.insert(Name::mk(), Object::Dictionary(mk));

        let result = parse_mk_dict(&dict, &store).unwrap();
        assert_eq!(result.rotation, 90);
        assert_eq!(result.border_color, Some(vec![1.0, 0.0, 0.0]));
        assert_eq!(result.background_color, Some(vec![0.9, 0.9, 0.9]));
        assert_eq!(result.caption.as_deref(), Some("Submit"));
        assert_eq!(result.rollover_caption.as_deref(), Some("Hover"));
        assert_eq!(result.alt_caption.as_deref(), Some("Pressed"));
        assert_eq!(result.text_position, 2);
    }

    #[test]
    fn test_parse_mk_dict_defaults() {
        let store = build_store();

        let mk = HashMap::new();
        let mut dict = HashMap::new();
        dict.insert(Name::mk(), Object::Dictionary(mk));

        let result = parse_mk_dict(&dict, &store).unwrap();
        assert_eq!(result.rotation, 0);
        assert!(result.border_color.is_none());
        assert!(result.background_color.is_none());
        assert!(result.caption.is_none());
        assert_eq!(result.text_position, 0);
    }

    #[test]
    fn test_parse_mk_dict_with_icon_refs() {
        let store = build_store();

        let mut mk = HashMap::new();
        mk.insert(Name::i(), Object::Reference(ObjectId::new(10, 0)));
        mk.insert(Name::ri(), Object::Reference(ObjectId::new(11, 0)));
        mk.insert(Name::ix(), Object::Reference(ObjectId::new(12, 0)));

        let mut dict = HashMap::new();
        dict.insert(Name::mk(), Object::Dictionary(mk));

        let result = parse_mk_dict(&dict, &store).unwrap();
        assert_eq!(result.icon_normal, Some(ObjectId::new(10, 0)));
        assert_eq!(result.icon_rollover, Some(ObjectId::new(11, 0)));
        assert_eq!(result.icon_alt, Some(ObjectId::new(12, 0)));
    }

    #[test]
    fn test_parse_mk_dict_absent_returns_none() {
        let store = build_store();
        let dict = HashMap::new();
        assert!(parse_mk_dict(&dict, &store).is_none());
    }

    #[test]
    fn test_mk_dict_includes_icon_fit() {
        let store = build_store();
        let mut if_dict = HashMap::new();
        if_dict.insert(Name::sw(), Object::Name(Name::from("N")));

        let mut mk = HashMap::new();
        mk.insert(Name::if_dict(), Object::Dictionary(if_dict));

        let mut dict = HashMap::new();
        dict.insert(Name::mk(), Object::Dictionary(mk));

        let result = parse_mk_dict(&dict, &store).unwrap();
        assert!(result.icon_fit.is_some());
        assert_eq!(result.icon_fit.unwrap().scale_method, ScaleMethod::Never);
    }

    #[test]
    fn test_mk_dict_border_color_components() {
        let store = build_store();
        let mut mk = HashMap::new();
        mk.insert(
            Name::bc(),
            Object::Array(vec![
                Object::Real(1.0),
                Object::Real(0.0),
                Object::Real(0.0),
            ]),
        );
        let mut dict = HashMap::new();
        dict.insert(Name::mk(), Object::Dictionary(mk));
        let result = parse_mk_dict(&dict, &store).unwrap();
        assert_eq!(result.border_color_components(), 3); // RGB
    }

    #[test]
    fn test_mk_dict_no_color_components() {
        let store = build_store();
        let mk = HashMap::new();
        let mut dict = HashMap::new();
        dict.insert(Name::mk(), Object::Dictionary(mk));
        let result = parse_mk_dict(&dict, &store).unwrap();
        assert_eq!(result.border_color_components(), 0); // transparent
        assert_eq!(result.background_color_components(), 0);
    }

    #[test]
    fn test_mk_dict_gray_color_components() {
        let store = build_store();
        let mut mk = HashMap::new();
        mk.insert(Name::bg_color(), Object::Array(vec![Object::Real(0.5)]));
        let mut dict = HashMap::new();
        dict.insert(Name::mk(), Object::Dictionary(mk));
        let result = parse_mk_dict(&dict, &store).unwrap();
        assert_eq!(result.background_color_components(), 1); // gray
    }
}