diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs index ef1ecdb36..dfff30a8f 100644 --- a/crates/typst-library/src/layout/mod.rs +++ b/crates/typst-library/src/layout/mod.rs @@ -24,6 +24,7 @@ mod page; mod place; mod point; mod ratio; +mod rect; mod regions; mod rel; mod repeat; @@ -55,6 +56,7 @@ pub use self::page::*; pub use self::place::*; pub use self::point::*; pub use self::ratio::*; +pub use self::rect::*; pub use self::regions::*; pub use self::rel::*; pub use self::repeat::*; diff --git a/crates/typst-library/src/layout/rect.rs b/crates/typst-library/src/layout/rect.rs new file mode 100644 index 000000000..9936193f2 --- /dev/null +++ b/crates/typst-library/src/layout/rect.rs @@ -0,0 +1,27 @@ +use crate::layout::{Point, Size}; + +/// A rectangle in 2D. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Rect { + /// The top left corner (minimum coordinate). + pub min: Point, + /// The bottom right corner (maximum coordinate). + pub max: Point, +} + +impl Rect { + /// Create a new rectangle from the minimum/maximum coordinate. + pub fn new(min: Point, max: Point) -> Self { + Self { min, max } + } + + /// Create a new rectangle from the position and size. + pub fn from_pos_size(pos: Point, size: Size) -> Self { + Self { min: pos, max: pos + size.to_point() } + } + + /// Compute the size of the rectangle. + pub fn size(&self) -> Size { + Size::new(self.max.x - self.min.x, self.max.y - self.min.y) + } +} diff --git a/crates/typst-library/src/text/item.rs b/crates/typst-library/src/text/item.rs index 2668aa54e..fb9f05a6f 100644 --- a/crates/typst-library/src/text/item.rs +++ b/crates/typst-library/src/text/item.rs @@ -4,7 +4,7 @@ use std::ops::Range; use ecow::EcoString; use typst_syntax::Span; -use crate::layout::{Abs, Em}; +use crate::layout::{Abs, Em, Point, Rect}; use crate::text::{Font, Lang, Region, is_default_ignorable}; use crate::visualize::{FixedStroke, Paint}; @@ -40,6 +40,44 @@ impl TextItem { pub fn height(&self) -> Abs { self.glyphs.iter().map(|g| g.y_advance).sum::().at(self.size) } + + /// The bounding box of the text run. + pub fn bbox(&self) -> Rect { + let mut min = Point::splat(Abs::inf()); + let mut max = Point::splat(-Abs::inf()); + let mut cursor = Point::zero(); + + for glyph in self.glyphs.iter() { + let advance = + Point::new(glyph.x_advance.at(self.size), glyph.y_advance.at(self.size)); + let offset = + Point::new(glyph.x_offset.at(self.size), glyph.y_offset.at(self.size)); + if let Some(rect) = + self.font.ttf().glyph_bounding_box(ttf_parser::GlyphId(glyph.id)) + { + let pos = cursor + offset; + let a = pos + + Point::new( + self.font.to_em(rect.x_min).at(self.size), + self.font.to_em(rect.y_min).at(self.size), + ); + let b = pos + + Point::new( + self.font.to_em(rect.x_max).at(self.size), + self.font.to_em(rect.y_max).at(self.size), + ); + min = min.min(a).min(b); + max = max.max(a).max(b); + } + cursor += advance; + } + + // Text runs use a y-up coordinate system, in contrary to the default + // frame orientation. + min.y *= -1.0; + max.y *= -1.0; + Rect::new(min, max) + } } impl Debug for TextItem { diff --git a/crates/typst-library/src/visualize/curve.rs b/crates/typst-library/src/visualize/curve.rs index 9f4a21a4e..f925d3d0a 100644 --- a/crates/typst-library/src/visualize/curve.rs +++ b/crates/typst-library/src/visualize/curve.rs @@ -4,7 +4,7 @@ use typst_utils::Numeric; use crate::diag::{HintedStrResult, HintedString, bail}; use crate::foundations::{Content, Packed, Smart, cast, elem}; -use crate::layout::{Abs, Axes, Length, Point, Rel, Size}; +use crate::layout::{Abs, Axes, Length, Point, Rect, Rel, Size}; use crate::visualize::{FillRule, Paint, Stroke}; use super::FixedStroke; @@ -474,8 +474,8 @@ impl Curve { } } - /// Computes the size of the bounding box of this curve. - pub fn bbox_size(&self) -> Size { + /// Computes the bounding box of this curve. + pub fn bbox(&self) -> Rect { let mut min = Point::splat(Abs::inf()); let mut max = Point::splat(-Abs::inf()); @@ -509,7 +509,12 @@ impl Curve { } } - Size::new(max.x - min.x, max.y - min.y) + Rect::new(min, max) + } + + /// Computes the size of the bounding box of this curve. + pub fn bbox_size(&self) -> Size { + self.bbox().size() } } diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs index d4382e6f0..fa9082425 100644 --- a/crates/typst-library/src/visualize/shape.rs +++ b/crates/typst-library/src/visualize/shape.rs @@ -1,5 +1,5 @@ use crate::foundations::{Cast, Content, Smart, elem}; -use crate::layout::{Abs, Corners, Length, Point, Rel, Sides, Size, Sizing}; +use crate::layout::{Abs, Corners, Length, Point, Rect, Rel, Sides, Size, Sizing}; use crate::visualize::{Curve, FixedStroke, Paint, Stroke}; /// A rectangle with optional content. @@ -375,6 +375,24 @@ impl Geometry { } } + /// The bounding box of the geometry. + pub fn bbox(&self) -> Rect { + match self { + Self::Line(end) => { + let min = end.min(Point::zero()); + let max = end.max(Point::zero()); + Rect::new(min, max) + } + Self::Rect(size) => { + let p = size.to_point(); + let min = p.min(Point::zero()); + let max = p.max(Point::zero()); + Rect::new(min, max) + } + Self::Curve(curve) => curve.bbox(), + } + } + /// The bounding box of the geometry. pub fn bbox_size(&self) -> Size { match self { diff --git a/crates/typst-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs index f24445811..e9a8d3186 100644 --- a/crates/typst-pdf/src/convert.rs +++ b/crates/typst-pdf/src/convert.rs @@ -107,7 +107,8 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul let mut page = document.start_page_with(settings); let mut surface = page.surface(); - let mut fc = FrameContext::new(typst_page.frame.size()); + let page_idx = gc.page_index_converter.pdf_page_index(i); + let mut fc = FrameContext::new(page_idx, typst_page.frame.size()); tags::page_start(gc, &mut surface); @@ -176,13 +177,17 @@ impl State { /// Context needed for converting a single frame. pub(crate) struct FrameContext { + /// The logical page index. This might be `None` if the page isn't exported, + /// of if the FrameContext has been built to convert a pattern. + pub(crate) page_idx: Option, states: Vec, link_annotations: Vec, } impl FrameContext { - pub(crate) fn new(size: Size) -> Self { + pub(crate) fn new(page_idx: Option, size: Size) -> Self { Self { + page_idx, states: vec![State::new(size)], link_annotations: Vec::new(), } diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index a8398d90e..e0788dd92 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -9,7 +9,7 @@ use krilla::tagging::SpanTag; use krilla_svg::{SurfaceExt, SvgSettings}; use typst_library::diag::{SourceResult, bail}; use typst_library::foundations::Smart; -use typst_library::layout::{Abs, Angle, Ratio, Size, Transform}; +use typst_library::layout::{Abs, Angle, Point, Ratio, Rect, Size, Transform}; use typst_library::visualize::{ ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat, RasterImage, }; @@ -35,6 +35,8 @@ pub(crate) fn handle_image( gc.image_spans.insert(span); + tags::update_bbox(gc, fc, || Rect::from_pos_size(Point::zero(), size)); + let mut handle = tags::start_span(gc, surface, SpanTag::empty().with_alt_text(image.alt())); let surface = handle.surface(); diff --git a/crates/typst-pdf/src/paint.rs b/crates/typst-pdf/src/paint.rs index e03d0e945..2cac56b39 100644 --- a/crates/typst-pdf/src/paint.rs +++ b/crates/typst-pdf/src/paint.rs @@ -127,7 +127,7 @@ fn convert_pattern( let mut stream_builder = surface.stream_builder(); let mut surface = stream_builder.surface(); - let mut fc = FrameContext::new(pattern.frame().size()); + let mut fc = FrameContext::new(None, pattern.frame().size()); handle_frame(&mut fc, pattern.frame(), None, &mut surface, gc)?; surface.finish(); let stream = stream_builder.finish(); diff --git a/crates/typst-pdf/src/shape.rs b/crates/typst-pdf/src/shape.rs index 011abe921..95e9ada4d 100644 --- a/crates/typst-pdf/src/shape.rs +++ b/crates/typst-pdf/src/shape.rs @@ -17,6 +17,8 @@ pub(crate) fn handle_shape( gc: &mut GlobalContext, span: Span, ) -> SourceResult<()> { + tags::update_bbox(gc, fc, || shape.geometry.bbox()); + let mut handle = tags::start_artifact(gc, surface, ArtifactKind::Other); let surface = handle.surface(); diff --git a/crates/typst-pdf/src/tags/mod.rs b/crates/typst-pdf/src/tags/mod.rs index 80d640199..052cb7083 100644 --- a/crates/typst-pdf/src/tags/mod.rs +++ b/crates/typst-pdf/src/tags/mod.rs @@ -1,15 +1,15 @@ use std::cell::OnceCell; use std::collections::HashMap; use std::num::NonZeroU32; -use std::ops::{Deref, DerefMut}; use ecow::EcoString; use krilla::configure::Validator; +use krilla::geom as kg; use krilla::page::Page; use krilla::surface::Surface; use krilla::tagging::{ - ArtifactType, ContentTag, Identifier, ListNumbering, Node, SpanTag, Tag, TagGroup, - TagKind, TagTree, + ArtifactType, BBox, ContentTag, Identifier, ListNumbering, Node, SpanTag, Tag, + TagGroup, TagKind, TagTree, }; use typst_library::diag::SourceResult; use typst_library::foundations::{ @@ -17,7 +17,7 @@ use typst_library::foundations::{ SettableProperty, StyleChain, }; use typst_library::introspection::Location; -use typst_library::layout::RepeatElem; +use typst_library::layout::{Abs, Point, Rect, RepeatElem}; use typst_library::math::EquationElem; use typst_library::model::{ Destination, EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, @@ -27,11 +27,12 @@ use typst_library::model::{ use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfMarkerTag, PdfMarkerTagKind}; use typst_library::visualize::ImageElem; -use crate::convert::GlobalContext; +use crate::convert::{FrameContext, GlobalContext}; use crate::link::LinkAnnotation; use crate::tags::list::ListCtx; use crate::tags::outline::OutlineCtx; use crate::tags::table::TableCtx; +use crate::util::AbsExt; mod list; mod outline; @@ -66,7 +67,8 @@ pub(crate) fn handle_start( } PdfMarkerTagKind::FigureBody(alt) => { let alt = alt.as_ref().map(|s| s.to_string()); - Tag::Figure(alt).into() + push_stack(gc, loc, StackEntryKind::Figure(FigureCtx::new(alt)))?; + return Ok(()); } PdfMarkerTagKind::Bibliography(numbered) => { let numbering = @@ -114,19 +116,20 @@ pub(crate) fn handle_start( } else if let Some(image) = elem.to_packed::() { let alt = image.alt.get_as_ref().map(|s| s.to_string()); - let figure_tag = gc.tags.stack.parent().and_then(StackEntryKind::as_standard_mut); - if let Some(TagKind::Figure(figure_tag)) = figure_tag { + if let Some(figure_ctx) = gc.tags.stack.parent_figure() { // Set alt text of outer figure tag, if not present. - if figure_tag.alt_text().is_none() { - figure_tag.set_alt_text(alt); + if figure_ctx.alt.is_none() { + figure_ctx.alt = alt; } return Ok(()); } else { - Tag::Figure(alt).into() + push_stack(gc, loc, StackEntryKind::Figure(FigureCtx::new(alt)))?; + return Ok(()); } } else if let Some(equation) = elem.to_packed::() { let alt = equation.alt.get_as_ref().map(|s| s.to_string()); - Tag::Formula(alt).into() + push_stack(gc, loc, StackEntryKind::Formula(FigureCtx::new(alt)))?; + return Ok(()); } else if let Some(table) = elem.to_packed::() { let table_id = gc.tags.next_table_id(); let summary = table.summary.get_as_ref().map(|s| s.to_string()); @@ -241,6 +244,14 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, surface: &mut Surface, loc: Loc list_ctx.push_bib_entry(entry.nodes); return; } + StackEntryKind::Figure(ctx) => { + let tag = Tag::Figure(ctx.alt).with_bbox(ctx.bbox.get()); + TagNode::Group(tag.into(), entry.nodes) + } + StackEntryKind::Formula(ctx) => { + let tag = Tag::Formula(ctx.alt).with_bbox(ctx.bbox.get()); + TagNode::Group(tag.into(), entry.nodes) + } StackEntryKind::Link(_, link) => { let alt = link.alt.as_ref().map(EcoString::to_string); let tag = Tag::Link.with_alt_text(alt); @@ -337,6 +348,18 @@ pub(crate) fn add_annotations( } } +pub(crate) fn update_bbox( + gc: &mut GlobalContext, + fc: &FrameContext, + compute_bbox: impl FnOnce() -> Rect, +) { + if gc.options.standards.config.validator() == Validator::UA1 + && let Some(bbox) = gc.tags.stack.find_parent_bbox() + { + bbox.expand_frame(fc, compute_bbox()); + } +} + pub(crate) struct Tags { /// The intermediary stack of nested tag groups. pub(crate) stack: TagStack, @@ -434,21 +457,36 @@ impl Tags { pub(crate) struct TagStack(Vec); -impl Deref for TagStack { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for TagStack { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - impl TagStack { + pub(crate) fn last(&self) -> Option<&StackEntry> { + self.0.last() + } + + pub(crate) fn last_mut(&mut self) -> Option<&mut StackEntry> { + self.0.last_mut() + } + + pub(crate) fn push(&mut self, entry: StackEntry) { + self.0.push(entry); + } + + pub(crate) fn pop_if( + &mut self, + predicate: impl FnMut(&mut StackEntry) -> bool, + ) -> Option { + let entry = self.0.pop_if(predicate)?; + + // TODO: If tags of the items were overlapping, only updating the + // direct parent bounding box might produce too large bounding boxes. + if let Some((page_idx, rect)) = entry.kind.bbox().and_then(|b| b.rect) + && let Some(parent) = self.find_parent_bbox() + { + parent.expand_page(page_idx, rect); + } + + Some(entry) + } + pub(crate) fn parent(&mut self) -> Option<&mut StackEntryKind> { self.0.last_mut().map(|e| &mut e.kind) } @@ -461,6 +499,10 @@ impl TagStack { self.parent()?.as_list_mut() } + pub(crate) fn parent_figure(&mut self) -> Option<&mut FigureCtx> { + self.parent()?.as_figure_mut() + } + pub(crate) fn parent_outline( &mut self, ) -> Option<(&mut OutlineCtx, &mut Vec)> { @@ -478,6 +520,11 @@ impl TagStack { Some((link_id, link.as_ref(), &mut e.nodes)) }) } + + /// Finds the first parent that has a bounding box. + pub(crate) fn find_parent_bbox(&mut self) -> Option<&mut BBoxCtx> { + self.0.iter_mut().rev().find_map(|e| e.kind.bbox_mut()) + } } pub(crate) struct Placeholders(Vec>); @@ -525,6 +572,8 @@ pub(crate) enum StackEntryKind { ListItemLabel, ListItemBody, BibEntry, + Figure(FigureCtx), + Formula(FigureCtx), Link(LinkId, Packed), /// The footnote reference in the text. FootNoteRef, @@ -534,10 +583,6 @@ pub(crate) enum StackEntryKind { } impl StackEntryKind { - pub(crate) fn as_standard_mut(&mut self) -> Option<&mut TagKind> { - if let Self::Standard(v) = self { Some(v) } else { None } - } - pub(crate) fn as_outline_mut(&mut self) -> Option<&mut OutlineCtx> { if let Self::Outline(v) = self { Some(v) } else { None } } @@ -550,9 +595,118 @@ impl StackEntryKind { if let Self::List(v) = self { Some(v) } else { None } } + pub(crate) fn as_figure_mut(&mut self) -> Option<&mut FigureCtx> { + if let Self::Figure(v) = self { Some(v) } else { None } + } + pub(crate) fn as_link(&self) -> Option<(LinkId, &Packed)> { if let Self::Link(id, link) = self { Some((*id, link)) } else { None } } + + pub(crate) fn bbox(&self) -> Option<&BBoxCtx> { + match self { + Self::Table(ctx) => Some(&ctx.bbox), + Self::Figure(ctx) => Some(&ctx.bbox), + Self::Formula(ctx) => Some(&ctx.bbox), + _ => None, + } + } + + pub(crate) fn bbox_mut(&mut self) -> Option<&mut BBoxCtx> { + match self { + Self::Table(ctx) => Some(&mut ctx.bbox), + Self::Figure(ctx) => Some(&mut ctx.bbox), + Self::Formula(ctx) => Some(&mut ctx.bbox), + _ => None, + } + } +} + +/// Figure/Formula context +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct FigureCtx { + alt: Option, + bbox: BBoxCtx, +} + +impl FigureCtx { + fn new(alt: Option) -> Self { + Self { alt, bbox: BBoxCtx::new() } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct BBoxCtx { + rect: Option<(usize, Rect)>, + multi_page: bool, +} + +impl BBoxCtx { + pub(crate) fn new() -> Self { + Self { rect: None, multi_page: false } + } + + /// Expand the bounding box with a `rect` relative to the current frame + /// context transform. + pub(crate) fn expand_frame(&mut self, fc: &FrameContext, rect: Rect) { + let Some(page_idx) = fc.page_idx else { return }; + if self.multi_page { + return; + } + let (idx, bbox) = self.rect.get_or_insert(( + page_idx, + Rect::new(Point::splat(Abs::inf()), Point::splat(-Abs::inf())), + )); + if *idx != page_idx { + self.multi_page = true; + self.rect = None; + return; + } + + let size = rect.size(); + for point in [ + rect.min, + rect.min + Point::with_x(size.x), + rect.min + Point::with_y(size.y), + rect.max, + ] { + let p = point.transform(fc.state().transform()); + bbox.min = bbox.min.min(p); + bbox.max = bbox.max.max(p); + } + } + + /// Expand the bounding box with a rectangle that's already transformed into + /// page coordinates. + pub(crate) fn expand_page(&mut self, page_idx: usize, rect: Rect) { + if self.multi_page { + return; + } + let (idx, bbox) = self.rect.get_or_insert(( + page_idx, + Rect::new(Point::splat(Abs::inf()), Point::splat(-Abs::inf())), + )); + if *idx != page_idx { + self.multi_page = true; + self.rect = None; + return; + } + + bbox.min = bbox.min.min(rect.min); + bbox.max = bbox.max.max(rect.max); + } + + pub(crate) fn get(&self) -> Option { + let (page_idx, rect) = self.rect?; + let rect = kg::Rect::from_ltrb( + rect.min.x.to_f32(), + rect.min.y.to_f32(), + rect.max.x.to_f32(), + rect.max.y.to_f32(), + ) + .unwrap(); + Some(BBox::new(page_idx as usize, rect)) + } } #[derive(Debug, Clone, PartialEq)] diff --git a/crates/typst-pdf/src/tags/table.rs b/crates/typst-pdf/src/tags/table.rs index 9bf916809..1c017e559 100644 --- a/crates/typst-pdf/src/tags/table.rs +++ b/crates/typst-pdf/src/tags/table.rs @@ -9,19 +9,26 @@ use typst_library::model::TableCell; use typst_library::pdf::{TableCellKind, TableHeaderScope}; use typst_syntax::Span; -use crate::tags::{TableId, TagNode}; +use crate::tags::{BBoxCtx, TableId, TagNode}; #[derive(Debug)] pub(crate) struct TableCtx { pub(crate) id: TableId, pub(crate) summary: Option, + pub(crate) bbox: BBoxCtx, rows: Vec>, min_width: usize, } impl TableCtx { pub(crate) fn new(id: TableId, summary: Option) -> Self { - Self { id, summary, rows: Vec::new(), min_width: 0 } + Self { + id, + summary, + bbox: BBoxCtx::new(), + rows: Vec::new(), + min_width: 0, + } } fn get(&self, x: usize, y: usize) -> Option<&TableCtxCell> { @@ -220,7 +227,11 @@ impl TableCtx { nodes.push(TagNode::Group(tag.into(), row_chunk)); } - TagNode::Group(Tag::Table.with_summary(self.summary).into(), nodes) + let tag = Tag::Table + .with_summary(self.summary) + .with_bbox(self.bbox.get()) + .into(); + TagNode::Group(tag, nodes) } fn resolve_cell_headers( diff --git a/crates/typst-pdf/src/text.rs b/crates/typst-pdf/src/text.rs index c70f2b3c2..466b6f430 100644 --- a/crates/typst-pdf/src/text.rs +++ b/crates/typst-pdf/src/text.rs @@ -24,6 +24,8 @@ pub(crate) fn handle_text( ) -> SourceResult<()> { *gc.languages.entry(t.lang).or_insert(0) += t.glyphs.len(); + tags::update_bbox(gc, fc, || t.bbox()); + let mut handle = tags::start_span(gc, surface, SpanTag::empty()); let surface = handle.surface();