mirror of
https://github.com/typst/typst
synced 2025-07-27 06:17:53 +08:00
feat: write BBox for Table, Formula, and Figure tags
This commit is contained in:
parent
71425fc2b3
commit
820ea27a41
@ -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::*;
|
||||||
|
27
crates/typst-library/src/layout/rect.rs
Normal file
27
crates/typst-library/src/layout/rect.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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)]
|
||||||
|
@ -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>(
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user