use krilla::action::{Action, LinkAction};
use krilla::annotation::Target;
use krilla::destination::XyzDestination;
use krilla::geom as kg;
use typst_library::diag::{At, ExpectInternal, SourceResult, bail};
use typst_library::layout::{Abs, Point, Position, Size};
use typst_library::model::Destination;
use typst_syntax::Span;
use crate::convert::{FrameContext, GlobalContext, PageIndexConverter};
use crate::tags::{self, AnnotationId, GroupId};
use crate::util::PointExt;
pub(crate) struct LinkAnnotation {
pub kind: LinkAnnotationKind,
pub alt: Option<String>,
pub span: Span,
pub rects: Vec<kg::Rect>,
pub target: Target,
}
pub(crate) enum LinkAnnotationKind {
Tagged(AnnotationId),
Artifact,
}
pub(crate) fn handle_link(
fc: &mut FrameContext,
gc: &mut GlobalContext,
dest: &Destination,
size: Size,
) -> SourceResult<()> {
let target = match dest {
Destination::Url(u) => {
Target::Action(Action::Link(LinkAction::new(u.to_string())))
}
Destination::Position(p) => {
let Some(dest) = pos_to_xyz(&gc.page_index_converter, *p) else {
return Ok(());
};
Target::Destination(krilla::destination::Destination::Xyz(dest))
}
Destination::Location(loc) => {
if let Some(nd) = gc.loc_to_names.get(loc) {
Target::Destination(krilla::destination::Destination::Named(nd.clone()))
} else {
let pos = gc.document.introspector.position(*loc);
let Some(dest) = pos_to_xyz(&gc.page_index_converter, pos) else {
return Ok(());
};
Target::Destination(krilla::destination::Destination::Xyz(dest))
}
}
};
let rect = bounding_box(fc, size);
if tags::disabled(gc) {
if gc.tags.in_tiling && gc.options.is_pdf_ua() {
let validator = gc.options.standards.config.validator().as_str();
bail!(
Span::detached(),
"{validator} error: PDF artifacts may not contain links";
hint: "a link was used within a tiling";
hint: "references, citations, and footnotes \
are also considered links in PDF"
);
}
fc.push_link_annotation(
GroupId::INVALID,
LinkAnnotation {
kind: LinkAnnotationKind::Artifact,
alt: None,
span: Span::detached(),
rects: vec![rect],
target,
},
);
return Ok(());
}
let (group_id, link) = (gc.tags.tree.parent_link())
.expect_internal("expected link ancestor in logical tree")
.at(Span::detached())?;
let alt = link.alt.as_ref().map(Into::into);
if gc.tags.tree.parent_artifact().is_some() {
if gc.options.is_pdf_ua() {
let validator = gc.options.standards.config.validator().as_str();
bail!(
link.span(),
"{validator} error: PDF artifacts may not contain links";
hint: "references, citations, and footnotes \
are also considered links in PDF"
);
}
fc.push_link_annotation(
group_id,
LinkAnnotation {
kind: LinkAnnotationKind::Artifact,
alt,
span: link.span(),
rects: vec![rect],
target,
},
);
return Ok(());
}
let join_annotations = gc.options.is_pdf_ua();
match fc.get_link_annotation(group_id) {
Some(annotation) if join_annotations => annotation.rects.push(rect),
_ => {
let annot_id = gc.tags.annotations.reserve();
fc.push_link_annotation(
group_id,
LinkAnnotation {
kind: LinkAnnotationKind::Tagged(annot_id),
alt,
span: link.span(),
rects: vec![rect],
target,
},
);
let group = gc.tags.tree.groups.get_mut(group_id);
group.push_annotation(annot_id);
}
}
Ok(())
}
fn bounding_box(fc: &FrameContext, size: Size) -> kg::Rect {
let pos = Point::zero();
let points = [
pos + Point::with_y(size.y),
pos + size.to_point(),
pos + Point::with_x(size.x),
pos,
];
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut max_y = f32::NEG_INFINITY;
for point in points {
let p = point.transform(fc.state().transform()).to_krilla();
min_x = min_x.min(p.x);
min_y = min_y.min(p.y);
max_x = max_x.max(p.x);
max_y = max_y.max(p.y);
}
kg::Rect::from_ltrb(min_x, min_y, max_x, max_y).unwrap()
}
pub(crate) fn pos_to_xyz(
pic: &PageIndexConverter,
pos: Position,
) -> Option<XyzDestination> {
let page_index = pic.pdf_page_index(pos.page.get() - 1)?;
let adjusted =
Point::new(pos.point.x, (pos.point.y - Abs::pt(10.0)).max(Abs::zero()));
Some(XyzDestination::new(page_index, adjusted.to_krilla()))
}