diff --git a/crates/typst-layout/src/pages/finalize.rs b/crates/typst-layout/src/pages/finalize.rs index b16d95699..543dbb0ce 100644 --- a/crates/typst-layout/src/pages/finalize.rs +++ b/crates/typst-layout/src/pages/finalize.rs @@ -1,7 +1,10 @@ use typst_library::diag::SourceResult; use typst_library::engine::Engine; -use typst_library::introspection::{ManualPageCounter, Tag}; -use typst_library::layout::{Frame, FrameItem, Page, Point}; +use typst_library::foundations::{Content, NativeElement}; +use typst_library::introspection::{ManualPageCounter, SplitLocator, Tag}; +use typst_library::layout::{ + ArtifactKind, ArtifactMarker, Frame, FrameItem, Page, Point, +}; use super::LayoutedPage; @@ -10,6 +13,7 @@ use super::LayoutedPage; /// physical page number, which is unknown during parallel layout. pub fn finalize( engine: &mut Engine, + locator: &mut SplitLocator, counter: &mut ManualPageCounter, tags: &mut Vec, LayoutedPage { @@ -45,10 +49,12 @@ pub fn finalize( // important as it affects the relative ordering of introspectable elements // and thus how counters resolve. if let Some(background) = background { - frame.push_frame(Point::zero(), background); + let tag = ArtifactMarker::new(ArtifactKind::Page).pack(); + push_tagged(engine, locator, &mut frame, Point::zero(), background, tag); } if let Some(header) = header { - frame.push_frame(Point::with_x(margin.left), header); + let tag = ArtifactMarker::new(ArtifactKind::Header).pack(); + push_tagged(engine, locator, &mut frame, Point::with_x(margin.left), header, tag); } // Add the inner contents. @@ -57,7 +63,8 @@ pub fn finalize( // Add the "after" marginals. if let Some(footer) = footer { let y = frame.height() - footer.height(); - frame.push_frame(Point::new(margin.left, y), footer); + let tag = ArtifactMarker::new(ArtifactKind::Footer).pack(); + push_tagged(engine, locator, &mut frame, Point::new(margin.left, y), footer, tag); } if let Some(foreground) = foreground { frame.push_frame(Point::zero(), foreground); @@ -72,3 +79,25 @@ pub fn finalize( Ok(Page { frame, fill, numbering, supplement, number }) } + +fn push_tagged( + engine: &mut Engine, + locator: &mut SplitLocator, + frame: &mut Frame, + mut pos: Point, + inner: Frame, + mut tag: Content, +) { + // TODO: use general PDF Tagged/Artifact element that wraps some content and + // is also available to the user. + let key = typst_utils::hash128(&tag); + let loc = locator.next_location(engine.introspector, key); + tag.set_location(loc); + frame.push(pos, FrameItem::Tag(Tag::Start(tag))); + + let height = inner.height(); + frame.push_frame(pos, inner); + + pos.y += height; + frame.push(pos, FrameItem::Tag(Tag::End(loc, key))); +} diff --git a/crates/typst-layout/src/pages/mod.rs b/crates/typst-layout/src/pages/mod.rs index 14dc0f3fb..a64fee4b3 100644 --- a/crates/typst-layout/src/pages/mod.rs +++ b/crates/typst-layout/src/pages/mod.rs @@ -123,17 +123,19 @@ fn layout_pages<'a>( Item::Run(..) => { let layouted = runs.next().unwrap()?; for layouted in layouted { - let page = finalize(engine, &mut counter, &mut tags, layouted)?; + let page = + finalize(engine, locator, &mut counter, &mut tags, layouted)?; pages.push(page); } } - Item::Parity(parity, initial, locator) => { + Item::Parity(parity, initial, page_locator) => { if !parity.matches(pages.len()) { continue; } - let layouted = layout_blank_page(engine, locator.relayout(), *initial)?; - let page = finalize(engine, &mut counter, &mut tags, layouted)?; + let layouted = + layout_blank_page(engine, page_locator.relayout(), *initial)?; + let page = finalize(engine, locator, &mut counter, &mut tags, layouted)?; pages.push(page); } Item::Tags(items) => { diff --git a/crates/typst-layout/src/pages/run.rs b/crates/typst-layout/src/pages/run.rs index e9e4e1105..6d2d29da5 100644 --- a/crates/typst-layout/src/pages/run.rs +++ b/crates/typst-layout/src/pages/run.rs @@ -185,8 +185,6 @@ fn layout_page_run_impl( )?; // Layouts a single marginal. - // TODO: add some sort of tag that indicates the marginals and use it to - // mark them as artifacts for PDF/UA. let mut layout_marginal = |content: &Option, area, align| { let Some(content) = content else { return Ok(None) }; let aligned = content.clone().styled(AlignElem::set_alignment(align)); diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index 98afbd06f..b6fa5d0be 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -10,7 +10,7 @@ use crate::foundations::{ cast, elem, Args, AutoValue, Cast, Construct, Content, Dict, Fold, NativeElement, Set, Smart, Value, }; -use crate::introspection::Introspector; +use crate::introspection::{Introspector, Locatable}; use crate::layout::{ Abs, Alignment, FlushElem, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel, Sides, SpecificAlignment, @@ -451,6 +451,28 @@ impl PagebreakElem { } } +// HACK: this should probably not be an element +#[derive(Copy)] +#[elem(Construct, Locatable)] +pub struct ArtifactMarker { + #[internal] + #[required] + pub kind: ArtifactKind, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum ArtifactKind { + Header, + Footer, + Page, +} + +impl Construct for ArtifactMarker { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult { + bail!(args.span, "cannot be constructed manually"); + } +} + /// A finished document with metadata and page frames. #[derive(Debug, Default, Clone)] pub struct PagedDocument { diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index 48dd3025a..37852fd27 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -430,7 +430,7 @@ impl Show for Packed { let body = body.plain_text(); let page_str = PageElem::local_name_in(styles); let page_nr = page.plain_text(); - eco_format!("{prefix} {body} {page_str} {page_nr}") + eco_format!("{prefix} \"{body}\", {page_str} {page_nr}") }; let inner = self.inner(engine, context, span, body, page)?; let block = if self.element.is::() { diff --git a/crates/typst-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs index b72d4aef1..41ee23614 100644 --- a/crates/typst-pdf/src/convert.rs +++ b/crates/typst-pdf/src/convert.rs @@ -10,7 +10,6 @@ use krilla::error::KrillaError; use krilla::geom::PathBuilder; use krilla::page::{PageLabel, PageSettings}; use krilla::surface::Surface; -use krilla::tagging::{ArtifactType, ContentTag, Node}; use krilla::{Document, SerializeSettings}; use krilla_svg::render_svg_glyph; use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; @@ -31,7 +30,7 @@ use crate::metadata::build_metadata; use crate::outline::build_outline; use crate::page::PageLabelExt; use crate::shape::handle_shape; -use crate::tags::{handle_close_tag, handle_open_tag, Placeholder, TagNode, Tags}; +use crate::tags::{self, Placeholder, Tags}; use crate::text::handle_text; use crate::util::{convert_path, display_font, AbsExt, TransformExt}; use crate::PdfOptions; @@ -114,18 +113,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()); - // Marked-content may not cross page boundaries: reopen tag - // that was closed at the end of the last page. - if let Some((_, _, nodes)) = gc.tags.stack.last_mut() { - let tag = if gc.tags.in_artifact { - ContentTag::Artifact(ArtifactType::Other) - } else { - ContentTag::Other - }; - // TODO: somehow avoid empty marked-content sequences - let id = surface.start_tagged(tag); - nodes.push(TagNode::Leaf(id)); - } + tags::restart(gc, &mut surface); handle_frame( &mut fc, @@ -135,17 +123,11 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul gc, )?; - // Marked-content may not cross page boundaries: close open tag. - if !gc.tags.stack.is_empty() { - surface.end_tagged(); - } + tags::end_open(gc, &mut surface); surface.finish(); - for (placeholder, annotation) in fc.annotations { - let annotation_id = page.add_tagged_annotation(annotation); - gc.tags.init_placeholder(placeholder, Node::Leaf(annotation_id)); - } + tags::add_annotations(gc, &mut page, fc.annotations); } } @@ -318,10 +300,10 @@ pub(crate) fn handle_frame( handle_link(fc, gc, alt.as_ref().map(EcoString::to_string), dest, *size) } FrameItem::Tag(introspection::Tag::Start(elem)) => { - handle_open_tag(gc, surface, elem) + tags::handle_start(gc, surface, elem) } FrameItem::Tag(introspection::Tag::End(loc, _)) => { - handle_close_tag(gc, surface, loc); + tags::handle_end(gc, surface, loc); } } diff --git a/crates/typst-pdf/src/tags.rs b/crates/typst-pdf/src/tags.rs index 2c43c8495..ec4d39499 100644 --- a/crates/typst-pdf/src/tags.rs +++ b/crates/typst-pdf/src/tags.rs @@ -1,9 +1,15 @@ use std::cell::OnceCell; +use std::ops::Deref; +use krilla::annotation::Annotation; +use krilla::page::Page; use krilla::surface::Surface; -use krilla::tagging::{ContentTag, Identifier, Node, Tag, TagGroup, TagTree}; +use krilla::tagging::{ + ArtifactType, ContentTag, Identifier, Node, Tag, TagGroup, TagTree, +}; use typst_library::foundations::{Content, StyleChain}; use typst_library::introspection::Location; +use typst_library::layout::{ArtifactKind, ArtifactMarker}; use typst_library::model::{HeadingElem, OutlineElem, OutlineEntry}; use crate::convert::GlobalContext; @@ -12,7 +18,7 @@ pub(crate) struct Tags { /// The intermediary stack of nested tag groups. pub(crate) stack: Vec<(Location, Tag, Vec)>, pub(crate) placeholders: Vec>, - pub(crate) in_artifact: bool, + pub(crate) in_artifact: Option<(Location, ArtifactMarker)>, /// The output. pub(crate) tree: Vec, @@ -34,7 +40,7 @@ impl Tags { Self { stack: Vec::new(), placeholders: Vec::new(), - in_artifact: false, + in_artifact: None, tree: Vec::new(), } @@ -142,16 +148,61 @@ impl Tags { } } -pub(crate) fn handle_open_tag( +/// 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) { + // TODO: somehow avoid empty marked-content sequences + if let Some((_, marker)) = gc.tags.in_artifact { + let ty = artifact_type(marker.kind); + surface.start_tagged(ContentTag::Artifact(ty)); + } else if let Some((_, _, nodes)) = gc.tags.stack.last_mut() { + let id = surface.start_tagged(ContentTag::Other); + nodes.push(TagNode::Leaf(id)); + } +} + +/// Marked-content may not cross page boundaries: end any open tag. +pub(crate) fn end_open(gc: &mut GlobalContext, surface: &mut Surface) { + if !gc.tags.stack.is_empty() || gc.tags.in_artifact.is_some() { + surface.end_tagged(); + } +} + +/// Add all annotations that were found in the page frame. +pub(crate) fn add_annotations( + gc: &mut GlobalContext, + page: &mut Page, + annotations: Vec<(Placeholder, Annotation)>, +) { + for (placeholder, annotation) in annotations { + let annotation_id = page.add_tagged_annotation(annotation); + gc.tags.init_placeholder(placeholder, Node::Leaf(annotation_id)); + } +} + +pub(crate) fn handle_start( gc: &mut GlobalContext, surface: &mut Surface, elem: &Content, ) { - if gc.tags.in_artifact { + if gc.tags.in_artifact.is_some() { + // Don't nest artifacts return; } - let Some(loc) = elem.location() else { return }; + let loc = elem.location().unwrap(); + + if let Some(marker) = elem.to_packed::() { + if !gc.tags.stack.is_empty() { + surface.end_tagged(); + } + + let marker = *marker.deref(); + let ty = artifact_type(marker.kind); + surface.start_tagged(ContentTag::Artifact(ty)); + gc.tags.in_artifact = Some((loc, marker)); + return; + } let tag = if let Some(heading) = elem.to_packed::() { let level = heading.resolve_level(StyleChain::default()); @@ -186,11 +237,14 @@ pub(crate) fn handle_open_tag( gc.tags.stack.push((loc, tag, vec![TagNode::Leaf(content_id)])); } -pub(crate) fn handle_close_tag( - gc: &mut GlobalContext, - surface: &mut Surface, - loc: &Location, -) { +pub(crate) fn handle_end(gc: &mut GlobalContext, surface: &mut Surface, loc: &Location) { + if gc.tags.in_artifact.is_some_and(|(l, _)| l == *loc) { + gc.tags.in_artifact = None; + surface.end_tagged(); + restart(gc, surface); + return; + } + let Some((_, tag, nodes)) = gc.tags.stack.pop_if(|(l, ..)| l == loc) else { return; }; @@ -207,3 +261,11 @@ pub(crate) fn handle_close_tag( gc.tags.tree.push(TagNode::Group(tag, nodes)); } } + +fn artifact_type(kind: ArtifactKind) -> ArtifactType { + match kind { + ArtifactKind::Header => ArtifactType::Header, + ArtifactKind::Footer => ArtifactType::Footer, + ArtifactKind::Page => ArtifactType::Page, + } +}