feat: [WIP] include links in tag tree

skip-checks:true
This commit is contained in:
Tobias Schmitz 2025-05-28 15:08:47 +02:00
parent 8c861d2d27
commit 5912b1e6f1
No known key found for this signature in database
4 changed files with 107 additions and 32 deletions

View File

@ -388,8 +388,6 @@ impl IntrospectorBuilder {
); );
} }
dbg!(elems.len());
self.finalize(elems) self.finalize(elems)
} }

View File

@ -31,7 +31,7 @@ use crate::metadata::build_metadata;
use crate::outline::build_outline; use crate::outline::build_outline;
use crate::page::PageLabelExt; use crate::page::PageLabelExt;
use crate::shape::handle_shape; use crate::shape::handle_shape;
use crate::tags::{handle_close_tag, handle_open_tag, Tags}; use crate::tags::{handle_close_tag, handle_open_tag, Placeholder, TagNode, Tags};
use crate::text::handle_text; use crate::text::handle_text;
use crate::util::{convert_path, display_font, AbsExt, TransformExt}; use crate::util::{convert_path, display_font, AbsExt, TransformExt};
use crate::PdfOptions; use crate::PdfOptions;
@ -42,6 +42,7 @@ pub fn convert(
options: &PdfOptions, options: &PdfOptions,
) -> SourceResult<Vec<u8>> { ) -> SourceResult<Vec<u8>> {
// HACK // HACK
// let config = Configuration::new();
let config = Configuration::new_with_validator(Validator::UA1); let config = Configuration::new_with_validator(Validator::UA1);
let settings = SerializeSettings { let settings = SerializeSettings {
compress_content_streams: true, compress_content_streams: true,
@ -73,7 +74,7 @@ pub fn convert(
document.set_outline(build_outline(&gc)); document.set_outline(build_outline(&gc));
document.set_metadata(build_metadata(&gc)); document.set_metadata(build_metadata(&gc));
document.set_tag_tree(gc.tags.take_tree()); document.set_tag_tree(gc.tags.build_tree());
finish(document, gc, options.standards.config) finish(document, gc, options.standards.config)
} }
@ -123,7 +124,7 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul
}; };
// TODO: somehow avoid empty marked-content sequences // TODO: somehow avoid empty marked-content sequences
let id = surface.start_tagged(tag); let id = surface.start_tagged(tag);
nodes.push(Node::Leaf(id)); nodes.push(TagNode::Leaf(id));
} }
handle_frame( handle_frame(
@ -141,8 +142,9 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul
surface.finish(); surface.finish();
for annotation in fc.annotations { for (placeholder, annotation) in fc.annotations {
page.add_annotation(annotation); let annotation_id = page.add_tagged_annotation(annotation);
gc.tags.init_placeholder(placeholder, Node::Leaf(annotation_id));
} }
} }
} }
@ -197,7 +199,7 @@ impl State {
/// Context needed for converting a single frame. /// Context needed for converting a single frame.
pub(crate) struct FrameContext { pub(crate) struct FrameContext {
states: Vec<State>, states: Vec<State>,
annotations: Vec<Annotation>, annotations: Vec<(Placeholder, Annotation)>,
} }
impl FrameContext { impl FrameContext {
@ -224,8 +226,12 @@ impl FrameContext {
self.states.last_mut().unwrap() self.states.last_mut().unwrap()
} }
pub(crate) fn push_annotation(&mut self, annotation: Annotation) { pub(crate) fn push_annotation(
self.annotations.push(annotation); &mut self,
placeholder: Placeholder,
annotation: Annotation,
) {
self.annotations.push((placeholder, annotation));
} }
} }

View File

@ -1,11 +1,12 @@
use krilla::action::{Action, LinkAction}; use krilla::action::{Action, LinkAction};
use krilla::annotation::{LinkAnnotation, Target}; use krilla::annotation::{Annotation, LinkAnnotation, Target};
use krilla::destination::XyzDestination; use krilla::destination::XyzDestination;
use krilla::geom::Rect; use krilla::geom::Rect;
use typst_library::layout::{Abs, Point, Size}; use typst_library::layout::{Abs, Point, Size};
use typst_library::model::Destination; use typst_library::model::Destination;
use crate::convert::{FrameContext, GlobalContext}; use crate::convert::{FrameContext, GlobalContext};
use crate::tags::TagNode;
use crate::util::{AbsExt, PointExt}; use crate::util::{AbsExt, PointExt};
pub(crate) fn handle_link( pub(crate) fn handle_link(
@ -44,15 +45,23 @@ pub(crate) fn handle_link(
// TODO: Support quad points. // TODO: Support quad points.
let placeholder = gc.tags.reserve_placeholder();
gc.tags.push(TagNode::Placeholder(placeholder));
// TODO: add some way to add alt text to annotations.
// probably through [typst_layout::modifiers::FrameModifiers]
let pos = match dest { let pos = match dest {
Destination::Url(u) => { Destination::Url(u) => {
fc.push_annotation( fc.push_annotation(
LinkAnnotation::new( placeholder,
rect, Annotation::new_link(
None, LinkAnnotation::new(
Target::Action(Action::Link(LinkAction::new(u.to_string()))), rect,
) None,
.into(), Target::Action(Action::Link(LinkAction::new(u.to_string()))),
),
Some(u.to_string()),
),
); );
return; return;
} }
@ -62,6 +71,7 @@ pub(crate) fn handle_link(
// If a named destination has been registered, it's already guaranteed to // If a named destination has been registered, it's already guaranteed to
// not point to an excluded page. // not point to an excluded page.
fc.push_annotation( fc.push_annotation(
placeholder,
LinkAnnotation::new( LinkAnnotation::new(
rect, rect,
None, None,
@ -81,6 +91,7 @@ pub(crate) fn handle_link(
let page_index = pos.page.get() - 1; let page_index = pos.page.get() - 1;
if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) { if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) {
fc.push_annotation( fc.push_annotation(
placeholder,
LinkAnnotation::new( LinkAnnotation::new(
rect, rect,
None, None,

View File

@ -1,5 +1,7 @@
use std::cell::OnceCell;
use krilla::surface::Surface; use krilla::surface::Surface;
use krilla::tagging::{ContentTag, Node, Tag, TagGroup, TagTree}; use krilla::tagging::{ContentTag, Identifier, Node, Tag, TagGroup, TagTree};
use typst_library::foundations::{Content, StyleChain}; use typst_library::foundations::{Content, StyleChain};
use typst_library::introspection::Location; use typst_library::introspection::Location;
use typst_library::model::{HeadingElem, OutlineElem, OutlineEntry}; use typst_library::model::{HeadingElem, OutlineElem, OutlineEntry};
@ -8,24 +10,87 @@ use crate::convert::GlobalContext;
pub(crate) struct Tags { pub(crate) struct Tags {
/// The intermediary stack of nested tag groups. /// The intermediary stack of nested tag groups.
pub(crate) stack: Vec<(Location, Tag, Vec<Node>)>, pub(crate) stack: Vec<(Location, Tag, Vec<TagNode>)>,
pub(crate) placeholders: Vec<OnceCell<Node>>,
pub(crate) in_artifact: bool, pub(crate) in_artifact: bool,
/// The output. /// The output.
pub(crate) tree: TagTree, pub(crate) tree: Vec<TagNode>,
} }
pub(crate) enum TagNode {
Group(Tag, Vec<TagNode>),
Leaf(Identifier),
/// Allows inserting a placeholder into the tag tree.
/// Currently used for [`krilla::page::Page::add_tagged_annotation`].
Placeholder(Placeholder),
}
#[derive(Clone, Copy)]
pub(crate) struct Placeholder(usize);
impl Tags { impl Tags {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
Self { Self {
stack: Vec::new(), stack: Vec::new(),
placeholders: Vec::new(),
in_artifact: false, in_artifact: false,
tree: TagTree::new(),
tree: Vec::new(),
} }
} }
pub(crate) fn take_tree(&mut self) -> TagTree { pub(crate) fn reserve_placeholder(&mut self) -> Placeholder {
std::mem::take(&mut self.tree) let idx = self.placeholders.len();
self.placeholders.push(OnceCell::new());
Placeholder(idx)
}
pub(crate) fn init_placeholder(&mut self, placeholder: Placeholder, node: Node) {
self.placeholders[placeholder.0]
.set(node)
.map_err(|_| ())
.expect("placeholder to be uninitialized");
}
pub(crate) fn take_placeholder(&mut self, placeholder: Placeholder) -> Node {
self.placeholders[placeholder.0]
.take()
.expect("initialized placeholder node")
}
pub(crate) fn push(&mut self, node: TagNode) {
if let Some((_, _, nodes)) = self.stack.last_mut() {
nodes.push(node);
} else {
self.tree.push(node);
}
}
pub(crate) fn build_tree(&mut self) -> TagTree {
let mut tree = TagTree::new();
let nodes = std::mem::take(&mut self.tree);
// PERF: collect into vec and construct TagTree directly from tag nodes.
for node in nodes.into_iter().map(|node| self.resolve_node(node)) {
tree.push(node);
}
tree
}
/// Resolves [`Placeholder`] nodes.
fn resolve_node(&mut self, node: TagNode) -> Node {
match node {
TagNode::Group(tag, nodes) => {
let mut group = TagGroup::new(tag);
// PERF: collect into vec and construct TagTree directly from tag nodes.
for node in nodes.into_iter().map(|node| self.resolve_node(node)) {
group.push(node);
}
Node::Group(group)
}
TagNode::Leaf(identifier) => Node::Leaf(identifier),
TagNode::Placeholder(placeholder) => self.take_placeholder(placeholder),
}
} }
pub(crate) fn context_supports(&self, tag: &Tag) -> bool { pub(crate) fn context_supports(&self, tag: &Tag) -> bool {
@ -118,7 +183,7 @@ pub(crate) fn handle_open_tag(
} }
let content_id = surface.start_tagged(krilla::tagging::ContentTag::Other); let content_id = surface.start_tagged(krilla::tagging::ContentTag::Other);
gc.tags.stack.push((loc, tag, vec![Node::Leaf(content_id)])); gc.tags.stack.push((loc, tag, vec![TagNode::Leaf(content_id)]));
} }
pub(crate) fn handle_close_tag( pub(crate) fn handle_close_tag(
@ -129,21 +194,16 @@ pub(crate) fn handle_close_tag(
let Some((_, tag, nodes)) = gc.tags.stack.pop_if(|(l, ..)| l == loc) else { let Some((_, tag, nodes)) = gc.tags.stack.pop_if(|(l, ..)| l == loc) else {
return; return;
}; };
// TODO: contstruct group directly from nodes
let mut tag_group = TagGroup::new(tag);
for node in nodes {
tag_group.push(node);
}
surface.end_tagged(); surface.end_tagged();
if let Some((_, _, parent_nodes)) = gc.tags.stack.last_mut() { if let Some((_, _, parent_nodes)) = gc.tags.stack.last_mut() {
parent_nodes.push(Node::Group(tag_group)); parent_nodes.push(TagNode::Group(tag, nodes));
// TODO: somehow avoid empty marked-content sequences // TODO: somehow avoid empty marked-content sequences
let id = surface.start_tagged(ContentTag::Other); let id = surface.start_tagged(ContentTag::Other);
parent_nodes.push(Node::Leaf(id)); parent_nodes.push(TagNode::Leaf(id));
} else { } else {
gc.tags.tree.push(Node::Group(tag_group)); gc.tags.tree.push(TagNode::Group(tag, nodes));
} }
} }