From 6d0c4c620db388bcdde55370461aab1d31bb3fae Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Sat, 26 Jul 2025 14:49:41 +0200 Subject: [PATCH] perf: optimize bbox computation --- crates/typst-library/src/text/item.rs | 1 + crates/typst-pdf/src/tags/mod.rs | 79 +++++++++++++++++++-------- 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/crates/typst-library/src/text/item.rs b/crates/typst-library/src/text/item.rs index fb9f05a6f..996e95548 100644 --- a/crates/typst-library/src/text/item.rs +++ b/crates/typst-library/src/text/item.rs @@ -42,6 +42,7 @@ impl TextItem { } /// The bounding box of the text run. + #[comemo::memoize] pub fn bbox(&self) -> Rect { let mut min = Point::splat(Abs::inf()); let mut max = Point::splat(-Abs::inf()); diff --git a/crates/typst-pdf/src/tags/mod.rs b/crates/typst-pdf/src/tags/mod.rs index 5df67756e..5f35eaf0e 100644 --- a/crates/typst-pdf/src/tags/mod.rs +++ b/crates/typst-pdf/src/tags/mod.rs @@ -295,6 +295,9 @@ pub(crate) fn handle_end( // Assign a new link id, so a new link annotation will be created. *id = gc.tags.next_link_id(); } + if let Some(bbox) = kind.bbox_mut() { + bbox.reset(); + } broken_entries.push(StackEntry { loc: entry.loc, @@ -454,8 +457,8 @@ pub(crate) fn update_bbox( fc: &FrameContext, compute_bbox: impl FnOnce() -> Rect, ) { - if gc.options.standards.config.validator() == Validator::UA1 - && let Some(bbox) = gc.tags.stack.find_parent_bbox() + if let Some(bbox) = gc.tags.stack.find_parent_bbox() + && gc.options.standards.config.validator() == Validator::UA1 { bbox.expand_frame(fc, compute_bbox()); } @@ -485,7 +488,7 @@ pub(crate) struct Tags { impl Tags { pub(crate) fn new() -> Self { Self { - stack: TagStack(Vec::new()), + stack: TagStack::new(), placeholders: Placeholders(Vec::new()), footnotes: HashMap::new(), in_artifact: None, @@ -557,47 +560,65 @@ impl Tags { } #[derive(Debug)] -pub(crate) struct TagStack(Vec); +pub(crate) struct TagStack { + items: Vec, + /// The index of the topmost stack entry that has a bbox. + bbox_idx: Option, +} impl> std::ops::Index for TagStack { type Output = I::Output; #[inline] fn index(&self, index: I) -> &Self::Output { - std::ops::Index::index(&self.0, index) + std::ops::Index::index(&self.items, index) } } impl> std::ops::IndexMut for TagStack { #[inline] fn index_mut(&mut self, index: I) -> &mut Self::Output { - std::ops::IndexMut::index_mut(&mut self.0, index) + std::ops::IndexMut::index_mut(&mut self.items, index) } } impl TagStack { + pub(crate) fn new() -> Self { + Self { items: Vec::new(), bbox_idx: None } + } + pub(crate) fn len(&self) -> usize { - self.0.len() + self.items.len() } pub(crate) fn last(&self) -> Option<&StackEntry> { - self.0.last() + self.items.last() } pub(crate) fn last_mut(&mut self) -> Option<&mut StackEntry> { - self.0.last_mut() + self.items.last_mut() } pub(crate) fn iter(&self) -> std::slice::Iter { - self.0.iter() + self.items.iter() } pub(crate) fn push(&mut self, entry: StackEntry) { - self.0.push(entry); + if entry.kind.bbox().is_some() { + self.bbox_idx = Some(self.len()); + } + self.items.push(entry); } pub(crate) fn extend(&mut self, iter: impl IntoIterator) { - self.0.extend(iter); + let start = self.len(); + self.items.extend(iter); + let last_bbox_offset = self.items[start..] + .iter() + .rposition(|entry| entry.kind.bbox().is_some()); + if let Some(offset) = last_bbox_offset { + self.bbox_idx = Some(start + offset); + } } /// Remove the last stack entry if the predicate returns true. @@ -606,24 +627,30 @@ impl TagStack { &mut self, mut predicate: impl FnMut(&mut StackEntry) -> bool, ) -> Option { - let last = self.0.last_mut()?; + let last = self.items.last_mut()?; if predicate(last) { self.pop() } else { None } } /// Remove the last stack entry. /// This takes care of updating the parent bboxes. pub(crate) fn pop(&mut self) -> Option { - let entry = self.0.pop()?; - if let Some((page_idx, rect)) = entry.kind.bbox().and_then(|b| b.rect) - && let Some(bbox) = self.find_parent_bbox() - { - bbox.expand_page(page_idx, rect); - } - Some(entry) + let removed = self.items.pop()?; + + let Some(inner_bbox) = removed.kind.bbox() else { return Some(removed) }; + + self.bbox_idx = self.items.iter_mut().enumerate().rev().find_map(|(i, entry)| { + let outer_bbox = entry.kind.bbox_mut()?; + if let Some((page_idx, rect)) = inner_bbox.rect { + outer_bbox.expand_page(page_idx, rect); + } + Some(i) + }); + + Some(removed) } pub(crate) fn parent(&mut self) -> Option<&mut StackEntryKind> { - self.0.last_mut().map(|e| &mut e.kind) + self.items.last_mut().map(|e| &mut e.kind) } pub(crate) fn parent_table(&mut self) -> Option<&mut TableCtx> { @@ -641,7 +668,7 @@ impl TagStack { pub(crate) fn parent_outline( &mut self, ) -> Option<(&mut OutlineCtx, &mut Vec)> { - self.0.last_mut().and_then(|e| { + self.items.last_mut().and_then(|e| { let ctx = e.kind.as_outline_mut()?; Some((ctx, &mut e.nodes)) }) @@ -650,7 +677,7 @@ impl TagStack { pub(crate) fn find_parent_link( &mut self, ) -> Option<(LinkId, &Packed, &mut Vec)> { - self.0.iter_mut().rev().find_map(|e| { + self.items.iter_mut().rev().find_map(|e| { let (link_id, link) = e.kind.as_link()?; Some((link_id, link, &mut e.nodes)) }) @@ -658,7 +685,7 @@ impl TagStack { /// 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()) + self.items[self.bbox_idx?].kind.bbox_mut() } } @@ -836,6 +863,10 @@ impl BBoxCtx { Self { rect: None, multi_page: false } } + pub(crate) fn reset(&mut self) { + *self = Self::new(); + } + /// 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) {