From e10050a23fd543e26c84e67aa96133142894002e Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Fri, 20 Jun 2025 15:55:24 +0200 Subject: [PATCH] feat: [no ci] write tags for links and use quadpoints in link annotations --- crates/typst-ide/src/jump.rs | 4 +- crates/typst-layout/src/modifiers.rs | 19 +- .../typst-library/src/foundations/content.rs | 41 ++++- crates/typst-library/src/layout/frame.rs | 8 +- .../typst-library/src/model/bibliography.rs | 4 +- crates/typst-library/src/model/footnote.rs | 4 +- crates/typst-library/src/model/link.rs | 9 +- crates/typst-library/src/model/outline.rs | 2 +- crates/typst-library/src/model/reference.rs | 2 +- crates/typst-pdf/src/convert.rs | 29 +-- crates/typst-pdf/src/image.rs | 8 - crates/typst-pdf/src/link.rs | 133 +++++++++----- crates/typst-pdf/src/tags.rs | 166 +++++++++--------- tests/src/run.rs | 2 +- 14 files changed, 238 insertions(+), 193 deletions(-) diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index 0f9f84ff7..1c66cb785 100644 --- a/crates/typst-ide/src/jump.rs +++ b/crates/typst-ide/src/jump.rs @@ -36,9 +36,9 @@ pub fn jump_from_click( ) -> Option { // Try to find a link first. for (pos, item) in frame.items() { - if let FrameItem::Link(_, dest, size) = item { + if let FrameItem::Link(link, size) = item { if is_in_rect(*pos, *size, click) { - return Some(match dest { + return Some(match &link.dest { Destination::Url(url) => Jump::Url(url.clone()), Destination::Position(pos) => Jump::Position(*pos), Destination::Location(loc) => { diff --git a/crates/typst-layout/src/modifiers.rs b/crates/typst-layout/src/modifiers.rs index a7d882617..00d42e42c 100644 --- a/crates/typst-layout/src/modifiers.rs +++ b/crates/typst-layout/src/modifiers.rs @@ -1,7 +1,6 @@ -use ecow::EcoString; -use typst_library::foundations::StyleChain; +use typst_library::foundations::{LinkMarker, Packed, StyleChain}; use typst_library::layout::{Abs, Fragment, Frame, FrameItem, HideElem, Point, Sides}; -use typst_library::model::{Destination, LinkElem, ParElem}; +use typst_library::model::ParElem; /// Frame-level modifications resulting from styles that do not impose any /// layout structure. @@ -21,8 +20,7 @@ use typst_library::model::{Destination, LinkElem, ParElem}; #[derive(Debug, Clone)] pub struct FrameModifiers { /// A destination to link to. - dest: Option, - alt: Option, + link: Option>, /// Whether the contents of the frame should be hidden. hidden: bool, } @@ -32,8 +30,7 @@ impl FrameModifiers { pub fn get_in(styles: StyleChain) -> Self { // TODO: maybe verify that an alt text was provided here Self { - dest: LinkElem::current_in(styles), - alt: LinkElem::alt_in(styles), + link: LinkMarker::current_in(styles), hidden: HideElem::hidden_in(styles), } } @@ -98,7 +95,7 @@ fn modify_frame( modifiers: &FrameModifiers, link_box_outset: Option>, ) { - if let Some(dest) = &modifiers.dest { + if let Some(link) = &modifiers.link { let mut pos = Point::zero(); let mut size = frame.size(); if let Some(outset) = link_box_outset { @@ -106,7 +103,7 @@ fn modify_frame( pos.x -= outset.left; size += outset.sum_by_axis(); } - frame.push(pos, FrameItem::Link(modifiers.alt.clone(), dest.clone(), size)); + frame.push(pos, FrameItem::Link(link.clone(), size)); } if modifiers.hidden { @@ -133,8 +130,8 @@ where let reset; let outer = styles; let mut styles = styles; - if modifiers.dest.is_some() { - reset = LinkElem::set_current(None).wrap(); + if modifiers.link.is_some() { + reset = LinkMarker::set_current(None).wrap(); styles = outer.chain(&reset); } diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs index 8cd46f0dd..518deca75 100644 --- a/crates/typst-library/src/foundations/content.rs +++ b/crates/typst-library/src/foundations/content.rs @@ -16,12 +16,12 @@ use crate::diag::{SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ elem, func, scope, ty, Context, Dict, Element, Fields, IntoValue, Label, - NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, - Value, + NativeElement, Recipe, RecipeIndex, Repr, Selector, Show, Str, Style, StyleChain, + Styles, Value, }; -use crate::introspection::Location; +use crate::introspection::{Locatable, Location}; use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; -use crate::model::{Destination, EmphElem, LinkElem, StrongElem}; +use crate::model::{Destination, EmphElem, StrongElem}; use crate::pdf::{ArtifactElem, ArtifactKind}; use crate::text::UnderlineElem; @@ -504,9 +504,13 @@ impl Content { } /// Link the content somewhere. - pub fn linked(self, alt: Option, dest: Destination) -> Self { - self.styled(LinkElem::set_alt(alt)) - .styled(LinkElem::set_current(Some(dest))) + pub fn linked(self, dest: Destination, alt: Option) -> Self { + let span = self.span(); + let link = Packed::new(LinkMarker::new(self, dest, alt)); + link.clone() + .pack() + .spanned(span) + .styled(LinkMarker::set_current(Some(link))) } /// Set alignments for this content. @@ -988,6 +992,29 @@ pub trait PlainText { fn plain_text(&self, text: &mut EcoString); } +/// An element that associates the body of a link with the destination. +#[elem(Show, Locatable)] +pub struct LinkMarker { + /// The content. + #[required] + pub body: Content, + #[required] + pub dest: Destination, + #[required] + pub alt: Option, + + /// A link style that should be applied to elements. + #[internal] + #[ghost] + pub current: Option>, +} + +impl Show for Packed { + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + Ok(self.body.clone()) + } +} + /// An error arising when trying to access a field of content. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum FieldAccessError { diff --git a/crates/typst-library/src/layout/frame.rs b/crates/typst-library/src/layout/frame.rs index 5b9d1f1a7..71bb9aa1b 100644 --- a/crates/typst-library/src/layout/frame.rs +++ b/crates/typst-library/src/layout/frame.rs @@ -4,14 +4,12 @@ use std::fmt::{self, Debug, Formatter}; use std::num::NonZeroUsize; use std::sync::Arc; -use ecow::EcoString; use typst_syntax::Span; use typst_utils::{LazyHash, Numeric}; -use crate::foundations::{cast, dict, Dict, Label, Value}; +use crate::foundations::{cast, dict, Dict, Label, LinkMarker, Packed, Value}; use crate::introspection::{Location, Tag}; use crate::layout::{Abs, Axes, FixedAlignment, Length, Point, Size, Transform}; -use crate::model::Destination; use crate::text::TextItem; use crate::visualize::{Color, Curve, FixedStroke, Geometry, Image, Paint, Shape}; @@ -474,7 +472,7 @@ pub enum FrameItem { /// An image and its size. Image(Image, Size, Span), /// An internal or external link to a destination. - Link(Option, Destination, Size), + Link(Packed, Size), /// An introspectable element that produced something within this frame. Tag(Tag), } @@ -486,7 +484,7 @@ impl Debug for FrameItem { Self::Text(text) => write!(f, "{text:?}"), Self::Shape(shape, _) => write!(f, "{shape:?}"), Self::Image(image, _, _) => write!(f, "{image:?}"), - Self::Link(alt, dest, _) => write!(f, "Link({alt:?}, {dest:?})"), + Self::Link(link, _) => write!(f, "Link({:?}, {:?})", link.dest, link.alt), Self::Tag(tag) => write!(f, "{tag:?}"), } } diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 1a5d28d92..54a53f268 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -874,7 +874,7 @@ impl<'a> Generator<'a> { if let Some(location) = first_occurrences.get(item.key.as_str()) { let dest = Destination::Location(*location); // TODO: accept user supplied alt text - content = content.linked(None, dest); + content = content.linked(dest, None); } StrResult::Ok(content) }) @@ -1010,7 +1010,7 @@ impl ElemRenderer<'_> { if let Some(location) = (self.link)(i) { let dest = Destination::Location(location); // TODO: accept user supplied alt text - content = content.linked(None, dest); + content = content.linked(dest, None); } } diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs index af6664cb9..872827d90 100644 --- a/crates/typst-library/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -148,7 +148,7 @@ impl Show for Packed { let loc = loc.variant(1); // Add zero-width weak spacing to make the footnote "sticky". // TODO: accept user supplied alt text - Ok(HElem::hole().pack() + sup.linked(None, Destination::Location(loc))) + Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc), None)) } } @@ -298,7 +298,7 @@ impl Show for Packed { .pack() .spanned(span) // TODO: accept user supplied alt text - .linked(None, Destination::Location(loc)) + .linked(Destination::Location(loc), None) .located(loc.variant(1)); Ok(Content::sequence([ diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index d64192f29..34ce14877 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -91,11 +91,6 @@ pub struct LinkElem { _ => args.expect("body")?, })] pub body: Content, - - /// A destination style that should be applied to elements. - #[internal] - #[ghost] - pub current: Option, } impl LinkElem { @@ -128,11 +123,11 @@ impl Show for Packed { } else { let alt = self.alt(styles); match &self.dest { - LinkTarget::Dest(dest) => body.linked(alt, dest.clone()), + LinkTarget::Dest(dest) => body.linked(dest.clone(), alt), LinkTarget::Label(label) => { let elem = engine.introspector.query_label(*label).at(self.span())?; let dest = Destination::Location(elem.location().unwrap()); - body.clone().linked(alt, dest) + body.linked(dest, alt) } } }) diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index 746a8ae06..f88cee342 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -445,7 +445,7 @@ impl Show for Packed { }; let loc = self.element_location().at(span)?; - Ok(block.linked(Some(alt), Destination::Location(loc))) + Ok(block.linked(Destination::Location(loc), Some(alt))) } } diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index 3d0056ffd..6c0f97bc3 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -317,7 +317,7 @@ fn show_reference( } // TODO: accept user supplied alt text - Ok(content.linked(None, Destination::Location(loc))) + Ok(content.linked(Destination::Location(loc), None)) } /// Turn a reference into a citation. diff --git a/crates/typst-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs index 464766c0a..8d824fce8 100644 --- a/crates/typst-pdf/src/convert.rs +++ b/crates/typst-pdf/src/convert.rs @@ -1,8 +1,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::num::NonZeroU64; -use ecow::{eco_format, EcoString, EcoVec}; -use krilla::annotation::Annotation; +use ecow::{eco_format, EcoVec}; use krilla::configure::{Configuration, ValidationError, Validator}; use krilla::destination::{NamedDestination, XyzDestination}; use krilla::embed::EmbedError; @@ -25,12 +24,12 @@ use typst_syntax::Span; use crate::embed::embed_files; use crate::image::handle_image; -use crate::link::handle_link; +use crate::link::{handle_link, LinkAnnotation}; use crate::metadata::build_metadata; use crate::outline::build_outline; use crate::page::PageLabelExt; use crate::shape::handle_shape; -use crate::tags::{self, Placeholder, Tags}; +use crate::tags::{self, Tags}; use crate::text::handle_text; use crate::util::{convert_path, display_font, AbsExt, TransformExt}; use crate::PdfOptions; @@ -111,7 +110,7 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul let mut surface = page.surface(); let mut fc = FrameContext::new(typst_page.frame.size()); - tags::restart(gc, &mut surface); + tags::restart_open(gc, &mut surface); handle_frame( &mut fc, @@ -125,7 +124,7 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul surface.finish(); - tags::add_annotations(gc, &mut page, fc.annotations); + tags::add_annotations(gc, &mut page, fc.link_annotations); } } @@ -179,14 +178,14 @@ impl State { /// Context needed for converting a single frame. pub(crate) struct FrameContext { states: Vec, - annotations: Vec<(Placeholder, Annotation)>, + pub(crate) link_annotations: HashMap, } impl FrameContext { pub(crate) fn new(size: Size) -> Self { Self { states: vec![State::new(size)], - annotations: vec![], + link_annotations: HashMap::new(), } } @@ -205,14 +204,6 @@ impl FrameContext { pub(crate) fn state_mut(&mut self) -> &mut State { self.states.last_mut().unwrap() } - - pub(crate) fn push_annotation( - &mut self, - placeholder: Placeholder, - annotation: Annotation, - ) { - self.annotations.push((placeholder, annotation)); - } } /// Globally needed context for converting a typst document. @@ -294,14 +285,12 @@ pub(crate) fn handle_frame( FrameItem::Image(image, size, span) => { handle_image(gc, fc, image, *size, surface, *span)? } - FrameItem::Link(alt, dest, size) => { - handle_link(fc, gc, alt.as_ref().map(EcoString::to_string), dest, *size) - } + FrameItem::Link(link, size) => handle_link(fc, gc, link, *size), FrameItem::Tag(introspection::Tag::Start(elem)) => { tags::handle_start(gc, surface, elem) } FrameItem::Tag(introspection::Tag::End(loc, _)) => { - tags::handle_end(gc, surface, loc); + tags::handle_end(gc, surface, *loc); } } diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index 93bdb1950..0809ae046 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -30,10 +30,6 @@ pub(crate) fn handle_image( let interpolate = image.scaling() == Smart::Custom(ImageScaling::Smooth); - if let Some(alt) = image.alt() { - surface.start_alt_text(alt); - } - gc.image_spans.insert(span); match image.kind() { @@ -62,10 +58,6 @@ pub(crate) fn handle_image( } } - if image.alt().is_some() { - surface.end_alt_text(); - } - surface.pop(); surface.reset_location(); diff --git a/crates/typst-pdf/src/link.rs b/crates/typst-pdf/src/link.rs index 6dfefbc11..2d360cfc3 100644 --- a/crates/typst-pdf/src/link.rs +++ b/crates/typst-pdf/src/link.rs @@ -1,52 +1,33 @@ +use std::collections::hash_map::Entry; + +use ecow::EcoString; use krilla::action::{Action, LinkAction}; -use krilla::annotation::{Annotation, LinkAnnotation, Target}; +use krilla::annotation::Target; use krilla::destination::XyzDestination; -use krilla::geom::Rect; +use krilla::geom as kg; +use typst_library::foundations::LinkMarker; use typst_library::layout::{Abs, Point, Position, Size}; use typst_library::model::Destination; use crate::convert::{FrameContext, GlobalContext}; -use crate::tags::TagNode; +use crate::tags::{Placeholder, TagNode}; use crate::util::{AbsExt, PointExt}; +pub(crate) struct LinkAnnotation { + pub(crate) placeholder: Placeholder, + pub(crate) alt: Option, + pub(crate) rect: kg::Rect, + pub(crate) quad_points: Vec, + pub(crate) target: Target, +} + pub(crate) fn handle_link( fc: &mut FrameContext, gc: &mut GlobalContext, - alt: Option, - dest: &Destination, + link: &LinkMarker, size: Size, ) { - let mut min_x = Abs::inf(); - let mut min_y = Abs::inf(); - let mut max_x = -Abs::inf(); - let mut max_y = -Abs::inf(); - - let pos = Point::zero(); - - // Compute the bounding box of the transformed link. - for point in [ - pos, - pos + Point::with_x(size.x), - pos + Point::with_y(size.y), - pos + size.to_point(), - ] { - let t = point.transform(fc.state().transform()); - min_x.set_min(t.x); - min_y.set_min(t.y); - max_x.set_max(t.x); - max_y.set_max(t.y); - } - - let x1 = min_x.to_f32(); - let x2 = max_x.to_f32(); - let y1 = min_y.to_f32(); - let y2 = max_y.to_f32(); - - let rect = Rect::from_ltrb(x1, y1, x2, y2).unwrap(); - - // TODO: Support quad points. - - let target = match dest { + let target = match &link.dest { Destination::Url(u) => { Target::Action(Action::Link(LinkAction::new(u.to_string()))) } @@ -69,13 +50,81 @@ pub(crate) fn handle_link( } }; - let placeholder = gc.tags.reserve_placeholder(); - gc.tags.push(TagNode::Placeholder(placeholder)); + let entry = gc.tags.stack.last_mut().expect("a link parent"); + let link_id = entry.link_id.expect("a link parent"); - fc.push_annotation( - placeholder, - Annotation::new_link(LinkAnnotation::new(rect, None, target), alt), - ); + let rect = to_rect(fc, size); + let quadpoints = quadpoints(rect); + + match fc.link_annotations.entry(link_id) { + Entry::Occupied(occupied) => { + // Update the bounding box and add the quadpoints of an existing link annotation. + let annotation = occupied.into_mut(); + annotation.rect = bounding_rect(annotation.rect, rect); + annotation.quad_points.extend_from_slice(&quadpoints); + } + Entry::Vacant(vacant) => { + let placeholder = gc.tags.reserve_placeholder(); + gc.tags.push(TagNode::Placeholder(placeholder)); + + vacant.insert(LinkAnnotation { + placeholder, + rect, + quad_points: quadpoints.to_vec(), + alt: link.alt.as_ref().map(EcoString::to_string), + target, + }); + } + } +} + +// Compute the bounding box of the transformed link. +fn to_rect(fc: &FrameContext, size: Size) -> kg::Rect { + let mut min_x = Abs::inf(); + let mut min_y = Abs::inf(); + let mut max_x = -Abs::inf(); + let mut max_y = -Abs::inf(); + + let pos = Point::zero(); + + for point in [ + pos, + pos + Point::with_x(size.x), + pos + Point::with_y(size.y), + pos + size.to_point(), + ] { + let t = point.transform(fc.state().transform()); + min_x.set_min(t.x); + min_y.set_min(t.y); + max_x.set_max(t.x); + max_y.set_max(t.y); + } + + let x1 = min_x.to_f32(); + let x2 = max_x.to_f32(); + let y1 = min_y.to_f32(); + let y2 = max_y.to_f32(); + + kg::Rect::from_ltrb(x1, y1, x2, y2).unwrap() +} + +fn bounding_rect(a: kg::Rect, b: kg::Rect) -> kg::Rect { + kg::Rect::from_ltrb( + a.left().min(b.left()), + a.top().min(b.top()), + a.right().max(b.right()), + a.bottom().max(b.bottom()), + ) + .unwrap() +} + +fn quadpoints(rect: kg::Rect) -> [kg::Point; 4] { + [ + kg::Point::from_xy(rect.left(), rect.bottom()), + kg::Point::from_xy(rect.right(), rect.bottom()), + kg::Point::from_xy(rect.right(), rect.top()), + kg::Point::from_xy(rect.left(), rect.top()), + ] } fn pos_to_target(gc: &mut GlobalContext, pos: Position) -> Option { diff --git a/crates/typst-pdf/src/tags.rs b/crates/typst-pdf/src/tags.rs index d6415adeb..92d3bfe78 100644 --- a/crates/typst-pdf/src/tags.rs +++ b/crates/typst-pdf/src/tags.rs @@ -1,28 +1,43 @@ use std::cell::OnceCell; +use std::collections::HashMap; -use krilla::annotation::Annotation; use krilla::page::Page; use krilla::surface::Surface; use krilla::tagging::{ ArtifactType, ContentTag, Identifier, Node, Tag, TagGroup, TagTree, }; -use typst_library::foundations::{Content, StyleChain}; +use typst_library::foundations::{Content, LinkMarker, StyleChain}; use typst_library::introspection::Location; -use typst_library::model::{HeadingElem, OutlineElem, OutlineEntry}; +use typst_library::model::{ + Destination, HeadingElem, Outlinable, OutlineElem, OutlineEntry, +}; use typst_library::pdf::{ArtifactElem, ArtifactKind}; use crate::convert::GlobalContext; +use crate::link::LinkAnnotation; pub(crate) struct Tags { /// The intermediary stack of nested tag groups. - pub(crate) stack: Vec<(Location, Tag, Vec)>, + pub(crate) stack: Vec, + /// A list of placeholders corresponding to a [`TagNode::Placeholder`]. pub(crate) placeholders: Vec>, pub(crate) in_artifact: Option<(Location, ArtifactKind)>, + pub(crate) link_id: LinkId, /// The output. pub(crate) tree: Vec, } +pub(crate) struct StackEntry { + pub(crate) loc: Location, + pub(crate) link_id: Option, + /// A list of tags that are wrapped around this tag when it is inserted into + /// the tag tree. + pub(crate) wrappers: Vec, + pub(crate) tag: Tag, + pub(crate) nodes: Vec, +} + pub(crate) enum TagNode { Group(Tag, Vec), Leaf(Identifier), @@ -31,6 +46,9 @@ pub(crate) enum TagNode { Placeholder(Placeholder), } +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct LinkId(u32); + #[derive(Clone, Copy)] pub(crate) struct Placeholder(usize); @@ -42,6 +60,7 @@ impl Tags { in_artifact: None, tree: Vec::new(), + link_id: LinkId(0), } } @@ -64,14 +83,20 @@ impl Tags { .expect("initialized placeholder node") } - pub(crate) fn push(&mut self, node: TagNode) { - if let Some((_, _, nodes)) = self.stack.last_mut() { - nodes.push(node); + /// Returns the current parent's list of children and the structure type ([Tag]). + /// In case of the document root the structure type will be `None`. + pub(crate) fn parent(&mut self) -> (Option<&mut Tag>, &mut Vec) { + if let Some(entry) = self.stack.last_mut() { + (Some(&mut entry.tag), &mut entry.nodes) } else { - self.tree.push(node); + (None, &mut self.tree) } } + pub(crate) fn push(&mut self, node: TagNode) { + self.parent().1.push(node); + } + pub(crate) fn build_tree(&mut self) -> TagTree { let mut tree = TagTree::new(); let nodes = std::mem::take(&mut self.tree); @@ -98,73 +123,26 @@ impl Tags { } } - /// Returns the current parent's list of children and whether it is the tree root. - fn parent_nodes(&mut self) -> (bool, &mut Vec) { - if let Some((_, _, parent_nodes)) = self.stack.last_mut() { - (false, parent_nodes) - } else { - (true, &mut self.tree) - } + fn context_supports(&self, _tag: &Tag) -> bool { + // TODO: generate using: https://pdfa.org/resource/iso-ts-32005-hierarchical-inclusion-rules/ + true } - fn context_supports(&self, tag: &Tag) -> bool { - let Some((_, parent, _)) = self.stack.last() else { return true }; - - use Tag::*; - - match parent { - Part => true, - Article => !matches!(tag, Article), - Section => true, - BlockQuote => todo!(), - Caption => todo!(), - TOC => matches!(tag, TOC | TOCI), - // TODO: NonStruct is allowed to but (currently?) not supported by krilla - TOCI => matches!(tag, TOC | Lbl | Reference | P), - Index => todo!(), - P => todo!(), - H1(_) => todo!(), - H2(_) => todo!(), - H3(_) => todo!(), - H4(_) => todo!(), - H5(_) => todo!(), - H6(_) => todo!(), - L(_list_numbering) => todo!(), - LI => todo!(), - Lbl => todo!(), - LBody => todo!(), - Table => todo!(), - TR => todo!(), - TH(_table_header_scope) => todo!(), - TD => todo!(), - THead => todo!(), - TBody => todo!(), - TFoot => todo!(), - InlineQuote => todo!(), - Note => todo!(), - Reference => todo!(), - BibEntry => todo!(), - Code => todo!(), - Link => todo!(), - Annot => todo!(), - Figure(_) => todo!(), - Formula(_) => todo!(), - Datetime => todo!(), - Terms => todo!(), - Title => todo!(), - } + fn next_link_id(&mut self) -> LinkId { + self.link_id.0 += 1; + self.link_id } } /// Marked-content may not cross page boundaries: restart tag that was still open /// at the end of the last page. -pub(crate) fn restart(gc: &mut GlobalContext, surface: &mut Surface) { +pub(crate) fn restart_open(gc: &mut GlobalContext, surface: &mut Surface) { // TODO: somehow avoid empty marked-content sequences if let Some((_, kind)) = gc.tags.in_artifact { start_artifact(gc, surface, kind); - } else if let Some((_, _, nodes)) = gc.tags.stack.last_mut() { + } else if let Some(entry) = gc.tags.stack.last_mut() { let id = surface.start_tagged(ContentTag::Other); - nodes.push(TagNode::Leaf(id)); + entry.nodes.push(TagNode::Leaf(id)); } } @@ -179,11 +157,16 @@ pub(crate) fn end_open(gc: &mut GlobalContext, surface: &mut Surface) { pub(crate) fn add_annotations( gc: &mut GlobalContext, page: &mut Page, - annotations: Vec<(Placeholder, Annotation)>, + annotations: HashMap, ) { - for (placeholder, annotation) in annotations { - let annotation_id = page.add_tagged_annotation(annotation); - gc.tags.init_placeholder(placeholder, Node::Leaf(annotation_id)); + for annotation in annotations.into_values() { + let LinkAnnotation { placeholder, alt, rect, quad_points, target } = annotation; + let annot = krilla::annotation::Annotation::new_link( + krilla::annotation::LinkAnnotation::new(rect, Some(quad_points), target), + alt, + ); + let annot_id = page.add_tagged_annotation(annot); + gc.tags.init_placeholder(placeholder, Node::Leaf(annot_id)); } } @@ -209,8 +192,10 @@ pub(crate) fn handle_start( return; } + let mut link_id = None; + let mut wrappers = Vec::new(); let tag = if let Some(heading) = elem.to_packed::() { - let level = heading.resolve_level(StyleChain::default()); + let level = heading.level(); let name = heading.body.plain_text().to_string(); match level.get() { 1 => Tag::H1(Some(name)), @@ -223,8 +208,14 @@ pub(crate) fn handle_start( } } else if let Some(_) = elem.to_packed::() { Tag::TOC - } else if let Some(_outline_entry) = elem.to_packed::() { + } else if let Some(_) = elem.to_packed::() { Tag::TOCI + } else if let Some(link) = elem.to_packed::() { + link_id = Some(gc.tags.next_link_id()); + if let Destination::Position(_) | Destination::Location(_) = link.dest { + wrappers.push(Tag::Reference); + } + Tag::Link } else { return; }; @@ -234,35 +225,43 @@ pub(crate) fn handle_start( } // close previous marked-content and open a nested tag. - if !gc.tags.stack.is_empty() { - surface.end_tagged(); - } + end_open(gc, surface); let id = surface.start_tagged(krilla::tagging::ContentTag::Other); - gc.tags.stack.push((loc, tag, vec![TagNode::Leaf(id)])); + gc.tags.stack.push(StackEntry { + loc, + link_id, + wrappers, + tag, + nodes: vec![TagNode::Leaf(id)], + }); } -pub(crate) fn handle_end(gc: &mut GlobalContext, surface: &mut Surface, loc: &Location) { - if let Some((l, _)) = &gc.tags.in_artifact { +pub(crate) fn handle_end(gc: &mut GlobalContext, surface: &mut Surface, loc: Location) { + if let Some((l, _)) = gc.tags.in_artifact { if l == loc { gc.tags.in_artifact = None; surface.end_tagged(); - if let Some((_, _, nodes)) = gc.tags.stack.last_mut() { + if let Some(entry) = gc.tags.stack.last_mut() { let id = surface.start_tagged(ContentTag::Other); - nodes.push(TagNode::Leaf(id)); + entry.nodes.push(TagNode::Leaf(id)); } } return; } - let Some((_, tag, nodes)) = gc.tags.stack.pop_if(|(l, ..)| l == loc) else { + let Some(entry) = gc.tags.stack.pop_if(|e| e.loc == loc) else { return; }; surface.end_tagged(); - let (is_root, parent_nodes) = gc.tags.parent_nodes(); - parent_nodes.push(TagNode::Group(tag, nodes)); - if !is_root { + let (parent_tag, parent_nodes) = gc.tags.parent(); + let mut node = TagNode::Group(entry.tag, entry.nodes); + for tag in entry.wrappers { + node = TagNode::Group(tag, vec![node]); + } + parent_nodes.push(node); + if parent_tag.is_some() { // TODO: somehow avoid empty marked-content sequences let id = surface.start_tagged(ContentTag::Other); parent_nodes.push(TagNode::Leaf(id)); @@ -273,8 +272,7 @@ fn start_artifact(gc: &mut GlobalContext, surface: &mut Surface, kind: ArtifactK let ty = artifact_type(kind); let id = surface.start_tagged(ContentTag::Artifact(ty)); - let (_, parent_nodes) = gc.tags.parent_nodes(); - parent_nodes.push(TagNode::Leaf(id)); + gc.tags.push(TagNode::Leaf(id)); } fn artifact_type(kind: ArtifactKind) -> ArtifactType { diff --git a/tests/src/run.rs b/tests/src/run.rs index 91de686f7..76ce1299f 100644 --- a/tests/src/run.rs +++ b/tests/src/run.rs @@ -524,7 +524,7 @@ fn render_links(canvas: &mut sk::Pixmap, ts: sk::Transform, frame: &Frame) { let ts = ts.pre_concat(to_sk_transform(&group.transform)); render_links(canvas, ts, &group.frame); } - FrameItem::Link(_, _, size) => { + FrameItem::Link(_, size) => { let w = size.x.to_pt() as f32; let h = size.y.to_pt() as f32; let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();