rpdfium-doc 7676.6.4

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

//! Link annotation accessor — `CPDF_Link`.
//!
//! Provides a typed view of a Link annotation dictionary, exposing its
//! rectangle, destination, and action (ISO 32000-2 section 12.5.6.5).
//!
//! Corresponds to upstream `CPDF_Link` which wraps a link annotation
//! dictionary and provides `GetRect()`, `GetDest()`, and `GetAction()`.

use std::collections::HashMap;

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

use crate::action::{Action, parse_action};
use crate::destination::{Destination, parse_destination};
use crate::error::DocResult;

/// A typed view of a Link annotation dictionary.
///
/// Provides access to the annotation's rectangle, optional destination,
/// and optional action. Corresponds to upstream `CPDF_Link`.
#[derive(Debug, Clone)]
pub struct Link {
    /// Annotation rectangle in user space: `[x1, y1, x2, y2]`.
    pub rect: [f32; 4],
    /// Destination, if specified via `/Dest`.
    pub destination: Option<Destination>,
    /// Action, if specified via `/A`.
    pub action: Option<Action>,
}

/// Parse a `Link` from a link annotation dictionary.
///
/// Corresponds to upstream `CPDF_Link` construction from an annotation dict.
pub fn parse_link<S: PdfSource>(
    dict: &HashMap<Name, Object>,
    store: &ObjectStore<S>,
) -> DocResult<Link> {
    // /Rect
    let rect_obj = dict
        .get(&Name::rect())
        .ok_or_else(|| crate::error::DocError::MissingKey("/Rect".into()))?;
    let resolved = store
        .deep_resolve(rect_obj)
        .map_err(|e| crate::error::DocError::Parser(e.to_string()))?;
    let arr = resolved
        .as_array()
        .ok_or(crate::error::DocError::UnexpectedType)?;
    let get = |idx: usize| -> f32 {
        arr.get(idx)
            .and_then(|o| o.as_f64())
            .map(|f| f as f32)
            .unwrap_or(0.0)
    };
    let rect = [get(0), get(1), get(2), get(3)];

    // /Dest
    let destination = dict
        .get(&Name::dest())
        .and_then(|o| parse_destination(o, store).ok());

    // /A
    let action = dict
        .get(&Name::a())
        .and_then(|o| parse_action(o, store).ok());

    Ok(Link {
        rect,
        destination,
        action,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::action::Action;
    use crate::destination::Destination;
    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_link_with_rect_only() {
        let store = build_store();
        let mut dict = HashMap::new();
        dict.insert(
            Name::rect(),
            Object::Array(vec![
                Object::Real(10.0),
                Object::Real(20.0),
                Object::Real(100.0),
                Object::Real(50.0),
            ]),
        );
        let link = parse_link(&dict, &store).unwrap();
        assert_eq!(link.rect, [10.0, 20.0, 100.0, 50.0]);
        assert!(link.destination.is_none());
        assert!(link.action.is_none());
    }

    #[test]
    fn test_parse_link_with_dest() {
        let store = build_store();
        let mut dict = HashMap::new();
        dict.insert(
            Name::rect(),
            Object::Array(vec![
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(200.0),
                Object::Real(20.0),
            ]),
        );
        dict.insert(
            Name::dest(),
            Object::String(PdfString::from_bytes(b"page2".to_vec())),
        );
        let link = parse_link(&dict, &store).unwrap();
        match &link.destination {
            Some(Destination::Named(n)) => assert_eq!(n, "page2"),
            _ => panic!("expected named destination"),
        }
        assert!(link.action.is_none());
    }

    #[test]
    fn test_parse_link_with_uri_action() {
        let store = build_store();
        let mut action_dict = HashMap::new();
        action_dict.insert(Name::s(), Object::Name(Name::from("URI")));
        action_dict.insert(
            Name::uri(),
            Object::String(PdfString::from_bytes(b"https://example.com".to_vec())),
        );

        let mut dict = HashMap::new();
        dict.insert(
            Name::rect(),
            Object::Array(vec![
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(100.0),
                Object::Real(20.0),
            ]),
        );
        dict.insert(Name::a(), Object::Dictionary(action_dict));

        let link = parse_link(&dict, &store).unwrap();
        assert!(link.destination.is_none());
        match &link.action {
            Some(Action::Uri(uri)) => assert_eq!(uri, "https://example.com"),
            _ => panic!("expected URI action"),
        }
    }

    #[test]
    fn test_parse_link_missing_rect_returns_error() {
        let store = build_store();
        let dict = HashMap::new();
        let result = parse_link(&dict, &store);
        assert!(result.is_err());
    }
}