From 7ea16f25b5f5bbea35af0ab5b37e0a8d329088c1 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Mon, 4 Aug 2025 18:51:02 +0200 Subject: [PATCH] fix: ignore any tags inside of tilings --- crates/typst-pdf/src/paint.rs | 9 +- crates/typst-pdf/src/tags/context.rs | 10 ++- crates/typst-pdf/src/tags/mod.rs | 94 +++++++++++++-------- tests/ref/pdftags/disable-tags-artifact.yml | 8 ++ tests/ref/pdftags/disable-tags-tiling.yml | 4 + tests/suite/pdftags/disable.typ | 21 +++++ 6 files changed, 109 insertions(+), 37 deletions(-) create mode 100644 tests/ref/pdftags/disable-tags-artifact.yml create mode 100644 tests/ref/pdftags/disable-tags-tiling.yml create mode 100644 tests/suite/pdftags/disable.typ diff --git a/crates/typst-pdf/src/paint.rs b/crates/typst-pdf/src/paint.rs index 2cac56b39..fb3870335 100644 --- a/crates/typst-pdf/src/paint.rs +++ b/crates/typst-pdf/src/paint.rs @@ -16,6 +16,7 @@ use typst_library::visualize::{ use typst_utils::Numeric; use crate::convert::{FrameContext, GlobalContext, State, handle_frame}; +use crate::tags::{self, Disable}; use crate::util::{AbsExt, FillRuleExt, LineCapExt, LineJoinExt, TransformExt}; pub(crate) fn convert_fill( @@ -127,8 +128,12 @@ fn convert_pattern( let mut stream_builder = surface.stream_builder(); let mut surface = stream_builder.surface(); - let mut fc = FrameContext::new(None, pattern.frame().size()); - handle_frame(&mut fc, pattern.frame(), None, &mut surface, gc)?; + { + let mut handle = tags::disable(gc, &mut surface, Disable::Tiling); + let (gc, surface) = handle.reborrow(); + let mut fc = FrameContext::new(None, pattern.frame().size()); + handle_frame(&mut fc, pattern.frame(), None, surface, gc)?; + } surface.finish(); let stream = stream_builder.finish(); let pattern = Pattern { diff --git a/crates/typst-pdf/src/tags/context.rs b/crates/typst-pdf/src/tags/context.rs index 6e11867ab..b06afda6e 100644 --- a/crates/typst-pdf/src/tags/context.rs +++ b/crates/typst-pdf/src/tags/context.rs @@ -38,7 +38,7 @@ pub struct Tags { /// before the reference in the text, so we only resolve them once tags /// for the whole document are generated. pub footnotes: HashMap, - pub in_artifact: Option<(Location, ArtifactKind)>, + pub disable: Option, /// Used to group multiple link annotations using quad points. link_id: LinkId, /// Used to generate IDs referenced in table `Headers` attributes. @@ -57,7 +57,7 @@ impl Tags { stack: TagStack::new(), placeholders: Placeholders(Vec::new()), footnotes: HashMap::new(), - in_artifact: None, + disable: None, link_id: LinkId(0), table_id: TableId(0), @@ -187,6 +187,12 @@ impl Tags { } } +#[derive(Clone, Copy, Debug)] +pub enum Disable { + ArtifactTag(Location, ArtifactKind), + Tiling, +} + #[derive(Clone, Debug)] pub struct TextAttrs { lineheight: Option, diff --git a/crates/typst-pdf/src/tags/mod.rs b/crates/typst-pdf/src/tags/mod.rs index 6ea34d274..7984999c9 100644 --- a/crates/typst-pdf/src/tags/mod.rs +++ b/crates/typst-pdf/src/tags/mod.rs @@ -100,17 +100,17 @@ pub fn handle_start( return Ok(()); } - if gc.tags.in_artifact.is_some() { + if gc.tags.disable.is_some() { // Don't nest artifacts return Ok(()); } if let Some(artifact) = elem.to_packed::() { let kind = artifact.kind.val(); - push_artifact(gc, surface, elem, kind); + push_artifact_tag(gc, surface, elem, kind); return Ok(()); } else if let Some(_) = elem.to_packed::() { - push_artifact(gc, surface, elem, ArtifactKind::Other); + push_artifact_tag(gc, surface, elem, ArtifactKind::Other); return Ok(()); } @@ -207,7 +207,7 @@ pub fn handle_start( // first page. Maybe it should be the cell on the last page, but that // would require more changes in the layouting code, or a pre-pass // on the frames to figure out if there are other footers following. - push_artifact(gc, surface, elem, ArtifactKind::Other); + push_artifact_tag(gc, surface, elem, ArtifactKind::Other); } else { push_stack(gc, elem, StackEntryKind::TableCell(cell.clone()))?; } @@ -312,7 +312,7 @@ fn push_stack( Ok(()) } -fn push_artifact( +fn push_artifact_tag( gc: &mut GlobalContext, surface: &mut Surface, elem: &Content, @@ -321,7 +321,7 @@ fn push_artifact( let loc = elem.location().expect("elem to be locatable"); let ty = artifact_type(kind); surface.start_tagged(ContentTag::Artifact(ty)); - gc.tags.in_artifact = Some((loc, kind)); + gc.tags.disable = Some(Disable::ArtifactTag(loc, kind)); } pub fn handle_end( @@ -333,10 +333,11 @@ pub fn handle_end( return Ok(()); } - if let Some((l, _)) = gc.tags.in_artifact + if let Some(Disable::ArtifactTag(l, _)) = gc.tags.disable && l == loc { - pop_artifact(gc, surface); + surface.end_tagged(); + gc.tags.disable = None; return Ok(()); } @@ -533,17 +534,16 @@ fn pop_stack(gc: &mut GlobalContext, entry: StackEntry) { gc.tags.push(node); } -fn pop_artifact(gc: &mut GlobalContext, surface: &mut Surface) { - surface.end_tagged(); - gc.tags.in_artifact = None; -} - pub fn page_start(gc: &mut GlobalContext, surface: &mut Surface) { if gc.options.disable_tags { return; } - if let Some((_, kind)) = gc.tags.in_artifact { + if let Some(disable) = gc.tags.disable { + let kind = match disable { + Disable::ArtifactTag(_, kind) => kind, + Disable::Tiling => ArtifactKind::Other, + }; let ty = artifact_type(kind); surface.start_tagged(ContentTag::Artifact(ty)); } @@ -554,7 +554,7 @@ pub fn page_end(gc: &mut GlobalContext, surface: &mut Surface) { return; } - if gc.tags.in_artifact.is_some() { + if gc.tags.disable.is_some() { surface.end_tagged(); } } @@ -584,6 +584,43 @@ pub fn add_link_annotations( } } +pub struct DisableHandle<'a, 'b, 'c, 'd> { + gc: &'b mut GlobalContext<'a>, + surface: &'d mut Surface<'c>, + /// Whether this handle started the disabled range. + started: bool, +} + +impl Drop for DisableHandle<'_, '_, '_, '_> { + fn drop(&mut self) { + if self.started { + self.gc.tags.disable = None; + self.surface.end_tagged(); + } + } +} + +impl<'a, 'c> DisableHandle<'a, '_, 'c, '_> { + pub fn reborrow<'s>( + &'s mut self, + ) -> (&'s mut GlobalContext<'a>, &'s mut Surface<'c>) { + (self.gc, self.surface) + } +} + +pub fn disable<'a, 'b, 'c, 'd>( + gc: &'b mut GlobalContext<'a>, + surface: &'d mut Surface<'c>, + kind: Disable, +) -> DisableHandle<'a, 'b, 'c, 'd> { + let started = gc.tags.disable.is_none(); + if started { + gc.tags.disable = Some(kind); + surface.start_tagged(ContentTag::Artifact(ArtifactType::Other)); + } + DisableHandle { gc, surface, started } +} + pub fn text<'a, 'b>( gc: &mut GlobalContext, fc: &FrameContext, @@ -596,7 +633,7 @@ pub fn text<'a, 'b>( update_bbox(gc, fc, || text.bbox()); - if gc.tags.in_artifact.is_some() { + if gc.tags.disable.is_some() { return TagHandle { surface, started: false }; } @@ -682,25 +719,16 @@ fn start_content<'a, 'b>( surface: &'b mut Surface<'a>, content: ContentTag, ) -> TagHandle<'a, 'b> { - if gc.tags.in_artifact.is_some() { + if gc.tags.disable.is_some() { return TagHandle { surface, started: false }; - } else if let Some(StackEntryKind::Table(_)) = gc.tags.stack.last().map(|e| &e.kind) { - // TODO: handle this more like other artifacts - // Mark any direct child of a table as an aritfact. Any real content - // will be wrapped inside a `TableCell`. - - // Don't store artifact content ids, they will be omitted anyway when - // serializing the tag tree. - surface.start_tagged(ContentTag::Artifact(ArtifactType::Other)); - TagHandle { surface, started: true } - } else { - let artifact = matches!(content, ContentTag::Artifact(_)); - let id = surface.start_tagged(content); - if !artifact { - gc.tags.push(TagNode::Leaf(id)); - } - TagHandle { surface, started: true } } + + let artifact = matches!(content, ContentTag::Artifact(_)); + let id = surface.start_tagged(content); + if !artifact { + gc.tags.push(TagNode::Leaf(id)); + } + TagHandle { surface, started: true } } fn artifact_type(kind: ArtifactKind) -> ArtifactType { diff --git a/tests/ref/pdftags/disable-tags-artifact.yml b/tests/ref/pdftags/disable-tags-artifact.yml new file mode 100644 index 000000000..2f6317b83 --- /dev/null +++ b/tests/ref/pdftags/disable-tags-artifact.yml @@ -0,0 +1,8 @@ +- Tag: H1 + /T: "Heading 1" + /K: + - Content: page=0 mcid=0 +- Tag: H1 + /T: "Heading 2" + /K: + - Content: page=0 mcid=1 diff --git a/tests/ref/pdftags/disable-tags-tiling.yml b/tests/ref/pdftags/disable-tags-tiling.yml new file mode 100644 index 000000000..0b34b3416 --- /dev/null +++ b/tests/ref/pdftags/disable-tags-tiling.yml @@ -0,0 +1,4 @@ +- Tag: H1 + /T: "Rectangle" + /K: + - Content: page=0 mcid=0 diff --git a/tests/suite/pdftags/disable.typ b/tests/suite/pdftags/disable.typ new file mode 100644 index 000000000..4fa276510 --- /dev/null +++ b/tests/suite/pdftags/disable.typ @@ -0,0 +1,21 @@ +--- disable-tags-artifact pdftags --- += Heading 1 +#pdf.artifact[ + #table( + columns: 2, + [a], [b], + [c], [d], + ) +] + += Heading 2 + +--- disable-tags-tiling pdftags --- += Rectangle + +#let pat = tiling(size: (20pt, 20pt))[ + - a + - b + - c +] +#rect(fill: pat)