use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::fmt;
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::pdf::{PdfDocument, PdfObject};
use crate::{DestinationKind, Error, Matrix, Rect};
const URI_COMPONENT_SET: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
.remove(b'_')
.remove(b'.')
.remove(b'!')
.remove(b'~')
.remove(b'*')
.remove(b'\'')
.remove(b'(')
.remove(b')');
const URI_PATH_SET: &AsciiSet = &URI_COMPONENT_SET.remove(b'/');
mod build;
pub(crate) use build::build_link_annotation;
pub(crate) use build::set_link_action_on_annot_dict;
mod extraction;
pub(crate) use extraction::parse_external_link;
pub(crate) use extraction::parse_link_action_from_annot_dict;
mod link_annot;
pub use link_annot::PdfLinkAnnot;
#[cfg(test)]
mod tests_build;
#[cfg(test)]
mod tests_extraction;
#[cfg(test)]
mod tests_format;
#[cfg(test)]
mod tests_link_annot;
#[derive(Debug, Clone, PartialEq)]
pub struct PdfLink {
pub bounds: Rect,
pub action: LinkAction,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LinkAction {
Action(PdfAction),
Dest(PdfDestination),
}
impl LinkAction {
pub fn into_pdf_action(self) -> PdfAction {
match self {
LinkAction::Action(a) => a,
LinkAction::Dest(d) => PdfAction::GoTo(d),
}
}
pub fn destination(&self) -> Option<&PdfDestination> {
match self {
LinkAction::Action(PdfAction::GoTo(d)) => Some(d),
LinkAction::Dest(d) => Some(d),
_ => None,
}
}
pub fn to_uri(&self) -> String {
self.to_string()
}
}
impl fmt::Display for LinkAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LinkAction::Action(action) => write!(f, "{action}"),
LinkAction::Dest(dest) => write!(f, "#{dest}"),
}
}
}
impl From<PdfAction> for LinkAction {
fn from(action: PdfAction) -> Self {
LinkAction::Action(action)
}
}
impl From<PdfDestination> for LinkAction {
fn from(dest: PdfDestination) -> Self {
LinkAction::Dest(dest)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PdfAction {
GoTo(PdfDestination),
GoToR {
file: FileSpec,
dest: PdfDestination,
},
Launch(FileSpec),
Uri(String),
}
impl PdfAction {
pub fn to_uri(&self) -> String {
self.to_string()
}
}
impl fmt::Display for PdfAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PdfAction::GoTo(dest) => write!(f, "#{dest}"),
PdfAction::Uri(uri) => {
f.write_str(uri)
}
PdfAction::Launch(file) => {
let sep = match file {
FileSpec::Url(url) if url.contains('#') => '&',
_ => '#',
};
write!(f, "{file}{sep}page=1")
}
PdfAction::GoToR { file, dest } => {
let sep = match file {
FileSpec::Url(url) if url.contains('#') => '&',
_ => '#',
};
write!(f, "{file}{sep}{dest}")
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum FileSpec {
Path(String),
Url(String),
}
impl FileSpec {
pub(crate) fn encode_into(&self, doc: &mut PdfDocument) -> Result<PdfObject, Error> {
match self {
FileSpec::Path(path) => {
let mut spec = doc.new_dict_with_capacity(3)?;
spec.dict_put("Type", PdfObject::new_name("Filespec")?)?;
let asciiname: String = path
.chars()
.map(|c| if matches!(c, ' '..='~') { c } else { '_' })
.collect();
spec.dict_put("F", PdfObject::new_string(&asciiname)?)?;
spec.dict_put("UF", PdfObject::new_string(path)?)?;
doc.add_object(&spec)
}
FileSpec::Url(url) => {
let mut spec = doc.new_dict_with_capacity(3)?;
spec.dict_put("Type", PdfObject::new_name("Filespec")?)?;
spec.dict_put("FS", PdfObject::new_name("URL")?)?;
spec.dict_put("F", PdfObject::new_string(url)?)?;
doc.add_object(&spec)
}
}
}
}
impl fmt::Display for FileSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileSpec::Path(path) => {
let prefix = if path.starts_with('/') {
"file://"
} else {
"file:"
};
write!(f, "{prefix}{}", utf8_percent_encode(path, URI_PATH_SET))
}
FileSpec::Url(url) => f.write_str(url),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PdfDestination {
Page { page: u32, kind: DestinationKind },
Named(String),
}
impl PdfDestination {
pub fn to_uri(&self) -> String {
self.to_string()
}
pub(crate) fn encode_local(
&self,
doc: &mut PdfDocument,
resolver: &mut impl DestPageResolver,
) -> Result<PdfObject, Error> {
match self {
PdfDestination::Page { page, kind } => {
let mut dest = doc.new_array_with_capacity(6)?;
let (dest_page_obj, dest_inv_ctm) = resolver.resolve(doc, *page)?;
dest.array_push_ref(dest_page_obj)?;
let dest_kind = dest_inv_ctm
.as_ref()
.map(|inv_ctm| kind.transform(inv_ctm))
.unwrap_or(*kind);
dest_kind.encode_into(&mut dest)?;
Ok(dest)
}
PdfDestination::Named(name) => PdfObject::new_string(name),
}
}
pub(crate) fn encode_remote(&self, doc: &mut PdfDocument) -> Result<PdfObject, Error> {
match self {
PdfDestination::Page { page, kind } => {
let mut dest = doc.new_array_with_capacity(6)?;
dest.array_push(PdfObject::new_int(*page as i32)?)?;
kind.encode_into(&mut dest)?;
Ok(dest)
}
PdfDestination::Named(name) => PdfObject::new_string(name),
}
}
}
impl Default for PdfDestination {
fn default() -> Self {
Self::Page {
page: 0,
kind: DestinationKind::default(),
}
}
}
impl fmt::Display for PdfDestination {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PdfDestination::Page { page, kind } => {
write!(f, "page={}{kind}", page.saturating_add(1))
}
PdfDestination::Named(name) => {
write!(
f,
"nameddest={}",
utf8_percent_encode(name, URI_COMPONENT_SET)
)
}
}
}
}
pub trait DestPageResolver {
fn resolve(
&mut self,
doc: &PdfDocument,
page_num: u32,
) -> Result<(&PdfObject, Option<&Matrix>), Error>;
}
pub struct CachedResolver<'a, F> {
cache: &'a mut HashMap<u32, (PdfObject, Option<Matrix>)>,
fn_dest_inv_ctm: F,
}
impl<'a, F> CachedResolver<'a, F> {
pub fn new(
cache: &'a mut HashMap<u32, (PdfObject, Option<Matrix>)>,
fn_dest_inv_ctm: F,
) -> Self {
Self {
cache,
fn_dest_inv_ctm,
}
}
}
impl<'a, F> DestPageResolver for CachedResolver<'a, F>
where
F: FnMut(&PdfObject) -> Result<Option<Matrix>, Error>,
{
fn resolve(
&mut self,
doc: &PdfDocument,
page_num: u32,
) -> Result<(&PdfObject, Option<&Matrix>), Error> {
match self.cache.entry(page_num) {
Entry::Occupied(entry) => {
let (obj, mat) = entry.into_mut();
Ok((obj, mat.as_ref()))
}
Entry::Vacant(entry) => {
let page_obj = doc.find_page(page_num as i32)?;
let inv_ctm = (self.fn_dest_inv_ctm)(&page_obj)?;
let (obj, mat) = entry.insert((page_obj, inv_ctm));
Ok((obj, mat.as_ref()))
}
}
}
}
pub struct SingleResolver<F> {
slot: Option<(PdfObject, Option<Matrix>)>,
fn_dest_inv_ctm: F,
}
impl<F> SingleResolver<F> {
pub fn new(fn_dest_inv_ctm: F) -> Self {
Self {
slot: None,
fn_dest_inv_ctm,
}
}
}
impl<F> DestPageResolver for SingleResolver<F>
where
F: FnMut(&PdfObject) -> Result<Option<Matrix>, Error>,
{
fn resolve(
&mut self,
doc: &PdfDocument,
page_num: u32,
) -> Result<(&PdfObject, Option<&Matrix>), Error> {
let page_obj = doc.find_page(page_num as i32)?;
let inv_ctm = (self.fn_dest_inv_ctm)(&page_obj)?;
let (obj, mat) = self.slot.insert((page_obj, inv_ctm));
Ok((obj, mat.as_ref()))
}
}