feat: write BBox for Table, Formula, and Figure tags

This commit is contained in:
Tobias Schmitz 2025-07-21 14:36:02 +02:00
parent 71425fc2b3
commit 820ea27a41
No known key found for this signature in database
12 changed files with 309 additions and 43 deletions

View File

@ -24,6 +24,7 @@ mod page;
mod place; mod place;
mod point; mod point;
mod ratio; mod ratio;
mod rect;
mod regions; mod regions;
mod rel; mod rel;
mod repeat; mod repeat;
@ -55,6 +56,7 @@ pub use self::page::*;
pub use self::place::*; pub use self::place::*;
pub use self::point::*; pub use self::point::*;
pub use self::ratio::*; pub use self::ratio::*;
pub use self::rect::*;
pub use self::regions::*; pub use self::regions::*;
pub use self::rel::*; pub use self::rel::*;
pub use self::repeat::*; pub use self::repeat::*;

View File

@ -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)
}
}

View File

@ -4,7 +4,7 @@ use std::ops::Range;
use ecow::EcoString; use ecow::EcoString;
use typst_syntax::Span; 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::text::{Font, Lang, Region, is_default_ignorable};
use crate::visualize::{FixedStroke, Paint}; use crate::visualize::{FixedStroke, Paint};
@ -40,6 +40,44 @@ impl TextItem {
pub fn height(&self) -> Abs { pub fn height(&self) -> Abs {
self.glyphs.iter().map(|g| g.y_advance).sum::<Em>().at(self.size) self.glyphs.iter().map(|g| g.y_advance).sum::<Em>().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 { impl Debug for TextItem {

View File

@ -4,7 +4,7 @@ use typst_utils::Numeric;
use crate::diag::{HintedStrResult, HintedString, bail}; use crate::diag::{HintedStrResult, HintedString, bail};
use crate::foundations::{Content, Packed, Smart, cast, elem}; 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 crate::visualize::{FillRule, Paint, Stroke};
use super::FixedStroke; use super::FixedStroke;
@ -474,8 +474,8 @@ impl Curve {
} }
} }
/// Computes the size of the bounding box of this curve. /// Computes the bounding box of this curve.
pub fn bbox_size(&self) -> Size { pub fn bbox(&self) -> Rect {
let mut min = Point::splat(Abs::inf()); let mut min = Point::splat(Abs::inf());
let mut max = 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()
} }
} }

View File

@ -1,5 +1,5 @@
use crate::foundations::{Cast, Content, Smart, elem}; 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}; use crate::visualize::{Curve, FixedStroke, Paint, Stroke};
/// A rectangle with optional content. /// 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. /// The bounding box of the geometry.
pub fn bbox_size(&self) -> Size { pub fn bbox_size(&self) -> Size {
match self { match self {

View File

@ -107,7 +107,8 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul
let mut page = document.start_page_with(settings); let mut page = document.start_page_with(settings);
let mut surface = page.surface(); 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); tags::page_start(gc, &mut surface);
@ -176,13 +177,17 @@ impl State {
/// Context needed for converting a single frame. /// Context needed for converting a single frame.
pub(crate) struct FrameContext { 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<usize>,
states: Vec<State>, states: Vec<State>,
link_annotations: Vec<LinkAnnotation>, link_annotations: Vec<LinkAnnotation>,
} }
impl FrameContext { impl FrameContext {
pub(crate) fn new(size: Size) -> Self { pub(crate) fn new(page_idx: Option<usize>, size: Size) -> Self {
Self { Self {
page_idx,
states: vec![State::new(size)], states: vec![State::new(size)],
link_annotations: Vec::new(), link_annotations: Vec::new(),
} }

View File

@ -9,7 +9,7 @@ use krilla::tagging::SpanTag;
use krilla_svg::{SurfaceExt, SvgSettings}; use krilla_svg::{SurfaceExt, SvgSettings};
use typst_library::diag::{SourceResult, bail}; use typst_library::diag::{SourceResult, bail};
use typst_library::foundations::Smart; 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::{ use typst_library::visualize::{
ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat, RasterImage, ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat, RasterImage,
}; };
@ -35,6 +35,8 @@ pub(crate) fn handle_image(
gc.image_spans.insert(span); gc.image_spans.insert(span);
tags::update_bbox(gc, fc, || Rect::from_pos_size(Point::zero(), size));
let mut handle = let mut handle =
tags::start_span(gc, surface, SpanTag::empty().with_alt_text(image.alt())); tags::start_span(gc, surface, SpanTag::empty().with_alt_text(image.alt()));
let surface = handle.surface(); let surface = handle.surface();

View File

@ -127,7 +127,7 @@ fn convert_pattern(
let mut stream_builder = surface.stream_builder(); let mut stream_builder = surface.stream_builder();
let mut surface = stream_builder.surface(); 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)?; handle_frame(&mut fc, pattern.frame(), None, &mut surface, gc)?;
surface.finish(); surface.finish();
let stream = stream_builder.finish(); let stream = stream_builder.finish();

View File

@ -17,6 +17,8 @@ pub(crate) fn handle_shape(
gc: &mut GlobalContext, gc: &mut GlobalContext,
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
tags::update_bbox(gc, fc, || shape.geometry.bbox());
let mut handle = tags::start_artifact(gc, surface, ArtifactKind::Other); let mut handle = tags::start_artifact(gc, surface, ArtifactKind::Other);
let surface = handle.surface(); let surface = handle.surface();

View File

@ -1,15 +1,15 @@
use std::cell::OnceCell; use std::cell::OnceCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::num::NonZeroU32; use std::num::NonZeroU32;
use std::ops::{Deref, DerefMut};
use ecow::EcoString; use ecow::EcoString;
use krilla::configure::Validator; use krilla::configure::Validator;
use krilla::geom as kg;
use krilla::page::Page; use krilla::page::Page;
use krilla::surface::Surface; use krilla::surface::Surface;
use krilla::tagging::{ use krilla::tagging::{
ArtifactType, ContentTag, Identifier, ListNumbering, Node, SpanTag, Tag, TagGroup, ArtifactType, BBox, ContentTag, Identifier, ListNumbering, Node, SpanTag, Tag,
TagKind, TagTree, TagGroup, TagKind, TagTree,
}; };
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{ use typst_library::foundations::{
@ -17,7 +17,7 @@ use typst_library::foundations::{
SettableProperty, StyleChain, SettableProperty, StyleChain,
}; };
use typst_library::introspection::Location; 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::math::EquationElem;
use typst_library::model::{ use typst_library::model::{
Destination, EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, 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::pdf::{ArtifactElem, ArtifactKind, PdfMarkerTag, PdfMarkerTagKind};
use typst_library::visualize::ImageElem; use typst_library::visualize::ImageElem;
use crate::convert::GlobalContext; use crate::convert::{FrameContext, GlobalContext};
use crate::link::LinkAnnotation; use crate::link::LinkAnnotation;
use crate::tags::list::ListCtx; use crate::tags::list::ListCtx;
use crate::tags::outline::OutlineCtx; use crate::tags::outline::OutlineCtx;
use crate::tags::table::TableCtx; use crate::tags::table::TableCtx;
use crate::util::AbsExt;
mod list; mod list;
mod outline; mod outline;
@ -66,7 +67,8 @@ pub(crate) fn handle_start(
} }
PdfMarkerTagKind::FigureBody(alt) => { PdfMarkerTagKind::FigureBody(alt) => {
let alt = alt.as_ref().map(|s| s.to_string()); 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) => { PdfMarkerTagKind::Bibliography(numbered) => {
let numbering = let numbering =
@ -114,19 +116,20 @@ pub(crate) fn handle_start(
} else if let Some(image) = elem.to_packed::<ImageElem>() { } else if let Some(image) = elem.to_packed::<ImageElem>() {
let alt = image.alt.get_as_ref().map(|s| s.to_string()); 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(figure_ctx) = gc.tags.stack.parent_figure() {
if let Some(TagKind::Figure(figure_tag)) = figure_tag {
// Set alt text of outer figure tag, if not present. // Set alt text of outer figure tag, if not present.
if figure_tag.alt_text().is_none() { if figure_ctx.alt.is_none() {
figure_tag.set_alt_text(alt); figure_ctx.alt = alt;
} }
return Ok(()); return Ok(());
} else { } else {
Tag::Figure(alt).into() push_stack(gc, loc, StackEntryKind::Figure(FigureCtx::new(alt)))?;
return Ok(());
} }
} else if let Some(equation) = elem.to_packed::<EquationElem>() { } else if let Some(equation) = elem.to_packed::<EquationElem>() {
let alt = equation.alt.get_as_ref().map(|s| s.to_string()); 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::<TableElem>() { } else if let Some(table) = elem.to_packed::<TableElem>() {
let table_id = gc.tags.next_table_id(); let table_id = gc.tags.next_table_id();
let summary = table.summary.get_as_ref().map(|s| s.to_string()); 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); list_ctx.push_bib_entry(entry.nodes);
return; 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) => { StackEntryKind::Link(_, link) => {
let alt = link.alt.as_ref().map(EcoString::to_string); let alt = link.alt.as_ref().map(EcoString::to_string);
let tag = Tag::Link.with_alt_text(alt); 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 { pub(crate) struct Tags {
/// The intermediary stack of nested tag groups. /// The intermediary stack of nested tag groups.
pub(crate) stack: TagStack, pub(crate) stack: TagStack,
@ -434,21 +457,36 @@ impl Tags {
pub(crate) struct TagStack(Vec<StackEntry>); pub(crate) struct TagStack(Vec<StackEntry>);
impl Deref for TagStack {
type Target = Vec<StackEntry>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for TagStack {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl TagStack { 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<StackEntry> {
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> { pub(crate) fn parent(&mut self) -> Option<&mut StackEntryKind> {
self.0.last_mut().map(|e| &mut e.kind) self.0.last_mut().map(|e| &mut e.kind)
} }
@ -461,6 +499,10 @@ impl TagStack {
self.parent()?.as_list_mut() 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( pub(crate) fn parent_outline(
&mut self, &mut self,
) -> Option<(&mut OutlineCtx, &mut Vec<TagNode>)> { ) -> Option<(&mut OutlineCtx, &mut Vec<TagNode>)> {
@ -478,6 +520,11 @@ impl TagStack {
Some((link_id, link.as_ref(), &mut e.nodes)) 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<OnceCell<Node>>); pub(crate) struct Placeholders(Vec<OnceCell<Node>>);
@ -525,6 +572,8 @@ pub(crate) enum StackEntryKind {
ListItemLabel, ListItemLabel,
ListItemBody, ListItemBody,
BibEntry, BibEntry,
Figure(FigureCtx),
Formula(FigureCtx),
Link(LinkId, Packed<LinkMarker>), Link(LinkId, Packed<LinkMarker>),
/// The footnote reference in the text. /// The footnote reference in the text.
FootNoteRef, FootNoteRef,
@ -534,10 +583,6 @@ pub(crate) enum StackEntryKind {
} }
impl 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> { pub(crate) fn as_outline_mut(&mut self) -> Option<&mut OutlineCtx> {
if let Self::Outline(v) = self { Some(v) } else { None } 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 } 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<LinkMarker>)> { pub(crate) fn as_link(&self) -> Option<(LinkId, &Packed<LinkMarker>)> {
if let Self::Link(id, link) = self { Some((*id, link)) } else { None } 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<String>,
bbox: BBoxCtx,
}
impl FigureCtx {
fn new(alt: Option<String>) -> 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<BBox> {
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)] #[derive(Debug, Clone, PartialEq)]

View File

@ -9,19 +9,26 @@ use typst_library::model::TableCell;
use typst_library::pdf::{TableCellKind, TableHeaderScope}; use typst_library::pdf::{TableCellKind, TableHeaderScope};
use typst_syntax::Span; use typst_syntax::Span;
use crate::tags::{TableId, TagNode}; use crate::tags::{BBoxCtx, TableId, TagNode};
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct TableCtx { pub(crate) struct TableCtx {
pub(crate) id: TableId, pub(crate) id: TableId,
pub(crate) summary: Option<String>, pub(crate) summary: Option<String>,
pub(crate) bbox: BBoxCtx,
rows: Vec<Vec<GridCell>>, rows: Vec<Vec<GridCell>>,
min_width: usize, min_width: usize,
} }
impl TableCtx { impl TableCtx {
pub(crate) fn new(id: TableId, summary: Option<String>) -> Self { pub(crate) fn new(id: TableId, summary: Option<String>) -> 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> { fn get(&self, x: usize, y: usize) -> Option<&TableCtxCell> {
@ -220,7 +227,11 @@ impl TableCtx {
nodes.push(TagNode::Group(tag.into(), row_chunk)); 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<F>( fn resolve_cell_headers<F>(

View File

@ -24,6 +24,8 @@ pub(crate) fn handle_text(
) -> SourceResult<()> { ) -> SourceResult<()> {
*gc.languages.entry(t.lang).or_insert(0) += t.glyphs.len(); *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 mut handle = tags::start_span(gc, surface, SpanTag::empty());
let surface = handle.surface(); let surface = handle.surface();