From ff8f9768c62229e89dc251065de9b28e434169a9 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Wed, 11 Jun 2025 16:10:34 +0200 Subject: [PATCH] feat: pdf.artifact element --- crates/typst-layout/src/pages/finalize.rs | 39 ++------------ crates/typst-layout/src/pages/mod.rs | 10 ++-- crates/typst-layout/src/pages/run.rs | 12 +++-- .../typst-library/src/foundations/content.rs | 7 +++ crates/typst-library/src/layout/page.rs | 24 +-------- crates/typst-library/src/pdf/accessibility.rs | 54 +++++++++++++++++++ crates/typst-library/src/pdf/mod.rs | 3 ++ crates/typst-pdf/src/tags.rs | 17 +++--- 8 files changed, 92 insertions(+), 74 deletions(-) create mode 100644 crates/typst-library/src/pdf/accessibility.rs diff --git a/crates/typst-layout/src/pages/finalize.rs b/crates/typst-layout/src/pages/finalize.rs index 543dbb0ce..b16d95699 100644 --- a/crates/typst-layout/src/pages/finalize.rs +++ b/crates/typst-layout/src/pages/finalize.rs @@ -1,10 +1,7 @@ use typst_library::diag::SourceResult; use typst_library::engine::Engine; -use typst_library::foundations::{Content, NativeElement}; -use typst_library::introspection::{ManualPageCounter, SplitLocator, Tag}; -use typst_library::layout::{ - ArtifactKind, ArtifactMarker, Frame, FrameItem, Page, Point, -}; +use typst_library::introspection::{ManualPageCounter, Tag}; +use typst_library::layout::{Frame, FrameItem, Page, Point}; use super::LayoutedPage; @@ -13,7 +10,6 @@ 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 { @@ -49,12 +45,10 @@ pub fn finalize( // important as it affects the relative ordering of introspectable elements // and thus how counters resolve. if let Some(background) = background { - let tag = ArtifactMarker::new(ArtifactKind::Page).pack(); - push_tagged(engine, locator, &mut frame, Point::zero(), background, tag); + frame.push_frame(Point::zero(), background); } if let Some(header) = header { - let tag = ArtifactMarker::new(ArtifactKind::Header).pack(); - push_tagged(engine, locator, &mut frame, Point::with_x(margin.left), header, tag); + frame.push_frame(Point::with_x(margin.left), header); } // Add the inner contents. @@ -63,8 +57,7 @@ pub fn finalize( // Add the "after" marginals. if let Some(footer) = footer { let y = frame.height() - footer.height(); - let tag = ArtifactMarker::new(ArtifactKind::Footer).pack(); - push_tagged(engine, locator, &mut frame, Point::new(margin.left, y), footer, tag); + frame.push_frame(Point::new(margin.left, y), footer); } if let Some(foreground) = foreground { frame.push_frame(Point::zero(), foreground); @@ -79,25 +72,3 @@ 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 a64fee4b3..14dc0f3fb 100644 --- a/crates/typst-layout/src/pages/mod.rs +++ b/crates/typst-layout/src/pages/mod.rs @@ -123,19 +123,17 @@ fn layout_pages<'a>( Item::Run(..) => { let layouted = runs.next().unwrap()?; for layouted in layouted { - let page = - finalize(engine, locator, &mut counter, &mut tags, layouted)?; + let page = finalize(engine, &mut counter, &mut tags, layouted)?; pages.push(page); } } - Item::Parity(parity, initial, page_locator) => { + Item::Parity(parity, initial, locator) => { if !parity.matches(pages.len()) { continue; } - let layouted = - layout_blank_page(engine, page_locator.relayout(), *initial)?; - let page = finalize(engine, locator, &mut counter, &mut tags, layouted)?; + let layouted = layout_blank_page(engine, locator.relayout(), *initial)?; + let page = finalize(engine, &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 6d2d29da5..233608386 100644 --- a/crates/typst-layout/src/pages/run.rs +++ b/crates/typst-layout/src/pages/run.rs @@ -13,6 +13,7 @@ use typst_library::layout::{ VAlignment, }; use typst_library::model::Numbering; +use typst_library::pdf::ArtifactKind; use typst_library::routines::{Pair, Routines}; use typst_library::text::{LocalName, TextElem}; use typst_library::visualize::Paint; @@ -200,6 +201,11 @@ fn layout_page_run_impl( // Layout marginals. let mut layouted = Vec::with_capacity(fragment.len()); + + let header = header.as_ref().map(|h| h.clone().artifact(ArtifactKind::Header)); + let footer = footer.as_ref().map(|f| f.clone().artifact(ArtifactKind::Footer)); + let background = background.as_ref().map(|b| b.clone().artifact(ArtifactKind::Page)); + for inner in fragment { let header_size = Size::new(inner.width(), margin.top - header_ascent); let footer_size = Size::new(inner.width(), margin.bottom - footer_descent); @@ -210,9 +216,9 @@ fn layout_page_run_impl( fill: fill.clone(), numbering: numbering.clone(), supplement: supplement.clone(), - header: layout_marginal(header, header_size, Alignment::BOTTOM)?, - footer: layout_marginal(footer, footer_size, Alignment::TOP)?, - background: layout_marginal(background, full_size, mid)?, + header: layout_marginal(&header, header_size, Alignment::BOTTOM)?, + footer: layout_marginal(&footer, footer_size, Alignment::TOP)?, + background: layout_marginal(&background, full_size, mid)?, foreground: layout_marginal(foreground, full_size, mid)?, margin, binding, diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs index 278d49401..8cd46f0dd 100644 --- a/crates/typst-library/src/foundations/content.rs +++ b/crates/typst-library/src/foundations/content.rs @@ -22,6 +22,7 @@ use crate::foundations::{ use crate::introspection::Location; use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; use crate::model::{Destination, EmphElem, LinkElem, StrongElem}; +use crate::pdf::{ArtifactElem, ArtifactKind}; use crate::text::UnderlineElem; /// A piece of document content. @@ -534,6 +535,12 @@ impl Content { .pack() .spanned(span) } + + /// Link the content somewhere. + pub fn artifact(self, kind: ArtifactKind) -> Self { + let span = self.span(); + ArtifactElem::new(self).with_kind(kind).pack().spanned(span) + } } #[scope] diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index b6fa5d0be..98afbd06f 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, Locatable}; +use crate::introspection::Introspector; use crate::layout::{ Abs, Alignment, FlushElem, Frame, HAlignment, Length, OuterVAlignment, Ratio, Rel, Sides, SpecificAlignment, @@ -451,28 +451,6 @@ 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/pdf/accessibility.rs b/crates/typst-library/src/pdf/accessibility.rs new file mode 100644 index 000000000..586e2cbb1 --- /dev/null +++ b/crates/typst-library/src/pdf/accessibility.rs @@ -0,0 +1,54 @@ +use typst_macros::{cast, elem}; + +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{Content, Packed, Show, StyleChain}; +use crate::introspection::Locatable; + +// TODO: docs + +/// Mark content as a PDF artifact. +/// TODO: also use to mark html elements with `aria-hidden="true"`? +#[elem(Locatable, Show)] +pub struct ArtifactElem { + #[default(ArtifactKind::Other)] + pub kind: ArtifactKind, + + /// The content to underline. + #[required] + pub body: Content, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub enum ArtifactKind { + /// Page header artifacts. + Header, + /// Page footer artifacts. + Footer, + /// Other page artifacts. + Page, + /// Other artifacts. + #[default] + Other, +} + +cast! { + ArtifactKind, + self => match self { + ArtifactKind::Header => "header".into_value(), + ArtifactKind::Footer => "footer".into_value(), + ArtifactKind::Page => "page".into_value(), + ArtifactKind::Other => "other".into_value(), + }, + "header" => Self::Header, + "footer" => Self::Footer, + "page" => Self::Page, + "other" => Self::Other, +} + +impl Show for Packed { + #[typst_macros::time(name = "underline", span = self.span())] + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + Ok(self.body.clone()) + } +} diff --git a/crates/typst-library/src/pdf/mod.rs b/crates/typst-library/src/pdf/mod.rs index 786a36372..835cc69fe 100644 --- a/crates/typst-library/src/pdf/mod.rs +++ b/crates/typst-library/src/pdf/mod.rs @@ -1,7 +1,9 @@ //! PDF-specific functionality. +mod accessibility; mod embed; +pub use self::accessibility::*; pub use self::embed::*; use crate::foundations::{Module, Scope}; @@ -11,5 +13,6 @@ pub fn module() -> Module { let mut pdf = Scope::deduplicating(); pdf.start_category(crate::Category::Pdf); pdf.define_elem::(); + pdf.define_elem::(); Module::new("pdf", pdf) } diff --git a/crates/typst-pdf/src/tags.rs b/crates/typst-pdf/src/tags.rs index ae15674f4..d6415adeb 100644 --- a/crates/typst-pdf/src/tags.rs +++ b/crates/typst-pdf/src/tags.rs @@ -1,5 +1,4 @@ use std::cell::OnceCell; -use std::ops::Deref; use krilla::annotation::Annotation; use krilla::page::Page; @@ -9,8 +8,8 @@ use krilla::tagging::{ }; 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 typst_library::pdf::{ArtifactElem, ArtifactKind}; use crate::convert::GlobalContext; @@ -18,7 +17,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: Option<(Location, ArtifactMarker)>, + pub(crate) in_artifact: Option<(Location, ArtifactKind)>, /// The output. pub(crate) tree: Vec, @@ -161,8 +160,8 @@ impl Tags { /// 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 { - start_artifact(gc, surface, marker.kind); + if let Some((_, kind)) = gc.tags.in_artifact { + start_artifact(gc, surface, kind); } else if let Some((_, _, nodes)) = gc.tags.stack.last_mut() { let id = surface.start_tagged(ContentTag::Other); nodes.push(TagNode::Leaf(id)); @@ -200,12 +199,13 @@ pub(crate) fn handle_start( let loc = elem.location().unwrap(); - if let Some(marker) = elem.to_packed::() { + if let Some(artifact) = elem.to_packed::() { if !gc.tags.stack.is_empty() { surface.end_tagged(); } - start_artifact(gc, surface, marker.kind); - gc.tags.in_artifact = Some((loc, *marker.deref())); + let kind = artifact.kind(StyleChain::default()); + start_artifact(gc, surface, kind); + gc.tags.in_artifact = Some((loc, kind)); return; } @@ -282,5 +282,6 @@ fn artifact_type(kind: ArtifactKind) -> ArtifactType { ArtifactKind::Header => ArtifactType::Header, ArtifactKind::Footer => ArtifactType::Footer, ArtifactKind::Page => ArtifactType::Page, + ArtifactKind::Other => ArtifactType::Other, } }