Files
craft-libs/vendor/krilla/patches/13-annotation-bounds.patch
aiongg 2ab6049d90 krilla: link annotations and page boxes refuse non-finite geometry (patch 13) (#283)
* krilla: link annotations refuse non-finite or degenerate geometry (patch 13)

Link quad points that are empty or non-finite, link rects and page boxes
that overflow when flipped into PDF coordinates, and XYZ destinations with
a missing page or a non-finite point made krilla panic, which aborts a
WASM app. Document::finish returns InvalidAnnotationGeometry,
InvalidPageBox or InvalidDestination instead. Constructor signatures are
unchanged.

* krilla: error messages format page indices without overflow (patch 13)
2026-10-24 09:38:24 -04:00

300 lines
12 KiB
Diff
Generated

diff --git a/src/error.rs b/src/error.rs
index a19ebf6..7158052 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -43,6 +43,15 @@ pub enum KrillaError {
/// Optional content marks were not balanced: an end without a start, a mark still open
/// when its stream finished, or a mark overlapping a tagged section.
UnbalancedOptionalContent(Option<Location>),
+ /// A link annotation has no finite bounding box on its page: its quad points are empty
+ /// or not finite, or its rect doesn't map to finite page coordinates.
+ InvalidAnnotationGeometry(Option<Location>),
+ /// An XYZ destination targets a page the document doesn't have (the index is
+ /// zero-based), or a point that isn't finite on that page.
+ InvalidDestination(usize),
+ /// A page's media, crop, bleed, trim or art box doesn't map to finite PDF coordinates.
+ /// Holds the zero-based page index.
+ InvalidPageBox(usize),
/// An error while attempting to embed a font.
Font(Font, String),
/// A list of validation errors. Can only occur if you set the `validator` in
@@ -96,6 +105,20 @@ impl Display for KrillaError {
f.write_str("optional content marks are not balanced")?;
write_location(f, *location)
}
+ KrillaError::InvalidAnnotationGeometry(location) => {
+ f.write_str("a link annotation has no finite bounding box")?;
+ write_location(f, *location)
+ }
+ KrillaError::InvalidDestination(page_index) => write!(
+ f,
+ "a destination on page {} is not in the document or not finite",
+ page_index.saturating_add(1)
+ ),
+ KrillaError::InvalidPageBox(page_index) => write!(
+ f,
+ "a page box on page {} doesn't map to finite coordinates",
+ page_index.saturating_add(1)
+ ),
KrillaError::Font(_, message) => write!(f, "failed to embed font: {message}"),
KrillaError::Validation(errors) => {
let count = errors.len();
diff --git a/src/interactive/annotation.rs b/src/interactive/annotation.rs
index 91c4cff..f2121f1 100644
--- a/src/interactive/annotation.rs
+++ b/src/interactive/annotation.rs
@@ -14,7 +14,7 @@ use pdf_writer::{Finish, Name, Ref, TextStr};
use crate::chunk_container::ChunkContainer;
use crate::color::Color;
use crate::configure::{PdfVersion, ValidationError};
-use crate::error::KrillaResult;
+use crate::error::{KrillaError, KrillaResult};
use crate::geom::{Quadrilateral, Rect};
use crate::interactive::action::Action;
use crate::interactive::destination::Destination;
@@ -70,15 +70,20 @@ impl Annotation {
root_ref: Ref,
page_height: f32,
) -> KrillaResult<()> {
+ let AnnotationType::Link(l) = &self.annotation_type;
+ // Checked before the object starts, so a refused annotation writes nothing.
+ let rect = l
+ .page_rect(page_height)
+ .ok_or(KrillaError::InvalidAnnotationGeometry(self.location))?;
+
let chunk = &mut chunk_container.non_stream.annotations;
let mut annotation = chunk
.indirect(root_ref)
.start::<pdf_writer::writers::Annotation>();
self.annotation_type
- .serialize_type(sc, &mut annotation, page_height)?;
+ .serialize_type(sc, &mut annotation, page_height, rect)?;
- let AnnotationType::Link(l) = &self.annotation_type;
// Only set the print flag when really necessary (only PDF/A). Don't
// set it by default, so annotations with color borders will be shown
// on a screen but not printed.
@@ -124,9 +129,10 @@ impl AnnotationType {
sc: &mut SerializeContext,
annotation: &mut pdf_writer::writers::Annotation,
page_height: f32,
+ rect: Rect,
) -> KrillaResult<()> {
match self {
- AnnotationType::Link(l) => l.serialize_type(sc, annotation, page_height),
+ AnnotationType::Link(l) => l.serialize_type(sc, annotation, page_height, rect),
}
}
}
@@ -157,7 +163,8 @@ impl LinkBorder {
/// A link annotation.
pub struct LinkAnnotation {
- pub(crate) rect: Rect,
+ /// `None` when the quad points have no finite bounding box.
+ pub(crate) rect: Option<Rect>,
pub(crate) quad_points: Option<Vec<Quadrilateral>>,
pub(crate) target: Target,
pub(crate) border: Option<LinkBorder>,
@@ -170,7 +177,7 @@ impl LinkAnnotation {
/// `target`: The target of the link annotation.
pub fn new(rect: Rect, target: Target) -> Self {
Self {
- rect,
+ rect: Some(rect),
quad_points: None,
target,
border: None,
@@ -183,8 +190,16 @@ impl LinkAnnotation {
/// `quad_points`: An array of quadrilaterals that define where the link
/// annotation should be activated. This is useful if you for example have
/// a link annotation that is broken to one or multiple lines.
+ ///
+ /// An empty list, or a point that isn't finite, makes [`Document::finish`] return
+ /// [`KrillaError::InvalidAnnotationGeometry`].
+ ///
+ /// [`Document::finish`]: crate::Document::finish
pub fn new_with_quad_points(quad_points: Vec<Quadrilateral>, target: Target) -> Self {
- assert!(!quad_points.is_empty());
+ let finite = quad_points
+ .iter()
+ .flat_map(|q| q.0)
+ .all(|p| p.x.is_finite() && p.y.is_finite());
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
@@ -202,13 +217,16 @@ impl LinkAnnotation {
// that sometimes prevents the quadpoints from being used if the quad
// points lie exactly on the bounding rectangle.
const EPSILON: f32 = 0.001;
- let rect = Rect::from_ltrb(
- min_x - EPSILON,
- min_y - EPSILON,
- max_x + EPSILON,
- max_y + EPSILON,
- )
- .unwrap();
+ let rect = finite
+ .then(|| {
+ Rect::from_ltrb(
+ min_x - EPSILON,
+ min_y - EPSILON,
+ max_x + EPSILON,
+ max_y + EPSILON,
+ )
+ })
+ .flatten();
Self {
rect,
@@ -227,19 +245,23 @@ impl LinkAnnotation {
}
}
+ /// The rect in PDF page coordinates, or `None` when there is no rect or it doesn't
+ /// map to finite coordinates. The quad points lie inside the rect, so they map to
+ /// finite coordinates whenever it does.
+ fn page_rect(&self, page_height: f32) -> Option<Rect> {
+ self.rect?.transform(page_root_transform(page_height))
+ }
+
fn serialize_type(
&self,
sc: &mut SerializeContext,
annotation: &mut pdf_writer::writers::Annotation,
page_height: f32,
+ rect: Rect,
) -> KrillaResult<()> {
annotation.subtype(pdf_writer::types::AnnotationType::Link);
- let actual_rect = self
- .rect
- .transform(page_root_transform(page_height))
- .unwrap();
- annotation.rect(actual_rect.to_pdf_rect());
+ annotation.rect(rect.to_pdf_rect());
annotation.border(
0.0,
0.0,
diff --git a/src/interactive/destination.rs b/src/interactive/destination.rs
index de40085..ec50951 100644
--- a/src/interactive/destination.rs
+++ b/src/interactive/destination.rs
@@ -117,7 +117,10 @@ impl From<XyzDestination> for Destination {
impl XyzDestination {
/// Create a new XYZ destination. `page_index` should be the index (i.e. number) of the
/// target page, and point indicates the specific location on that page that should be
- /// targeted. If the `page_index` is out of range, export will panic.
+ /// targeted. A `page_index` out of range, or a point that isn't finite, makes
+ /// [`Document::finish`] return [`KrillaError::InvalidDestination`].
+ ///
+ /// [`Document::finish`]: crate::Document::finish
pub fn new(page_index: usize, point: Point) -> Self {
Self(Arc::new(XyzDestRepr { page_index, point }))
}
@@ -127,17 +130,9 @@ impl XyzDestination {
sc: &mut SerializeContext,
chunk_container: &mut ChunkContainer,
root_ref: Ref,
- ) {
- let chunk = &mut chunk_container.non_stream.destinations;
- let destination = chunk.destination(root_ref);
-
- let page_info = sc.page_infos().get(self.0.page_index).unwrap_or_else(|| {
- panic!(
- "attempted to link to page {}, but document only has {} pages",
- self.0.page_index + 1,
- sc.page_infos().len()
- )
- });
+ ) -> KrillaResult<()> {
+ let invalid = || KrillaError::InvalidDestination(self.0.page_index);
+ let page_info = sc.page_infos().get(self.0.page_index).ok_or_else(invalid)?;
let (ref_, surface_size) = match page_info {
PageInfo::Krilla {
@@ -153,10 +148,17 @@ impl XyzDestination {
// Convert to PDF coordinates
let invert_transform = Transform::from_row(1.0, 0.0, 0.0, -1.0, 0.0, page_size);
invert_transform.map_point(&mut mapped_point);
+ if !(mapped_point.x.is_finite() && mapped_point.y.is_finite()) {
+ return Err(invalid());
+ }
- destination
+ chunk_container
+ .non_stream
+ .destinations
+ .destination(root_ref)
.page(page_ref)
.xyz(mapped_point.x, mapped_point.y, None);
+ Ok(())
}
}
diff --git a/src/page.rs b/src/page.rs
index 3b3227b..c596677 100644
--- a/src/page.rs
+++ b/src/page.rs
@@ -12,7 +12,7 @@ use crate::chunk_container::ChunkContainer;
use crate::configure::validate::VersionedFeature;
use crate::configure::ValidationError;
use crate::content::ContentBuilder;
-use crate::error::KrillaResult;
+use crate::error::{KrillaError, KrillaResult};
use crate::geom::{Rect, Size, Transform};
use crate::graphics::xobject::write_group_color_space;
use crate::interactive::annotation::Annotation;
@@ -437,31 +437,31 @@ impl InternalPage {
rect.transform(page_root_transform(
self.page_settings.surface_size().height(),
))
- .unwrap()
+ .ok_or(KrillaError::InvalidPageBox(self.page_index))
};
// media box is mandatory, so we need to fall back to the default bbox
- let media_box = transform_rect(self.page_settings.media_box().unwrap_or(self.bbox));
+ let media_box = transform_rect(self.page_settings.media_box().unwrap_or(self.bbox))?;
page.media_box(media_box.to_pdf_rect());
// the remaining type of box are not mandatory, so we only set them if they are present
if let Some(crop_box) = self.page_settings.crop_box() {
- let crop_box = transform_rect(crop_box);
+ let crop_box = transform_rect(crop_box)?;
page.crop_box(crop_box.to_pdf_rect());
}
if let Some(bleed_box) = self.page_settings.bleed_box() {
- let bleed_box = transform_rect(bleed_box);
+ let bleed_box = transform_rect(bleed_box)?;
page.bleed_box(bleed_box.to_pdf_rect());
}
if let Some(trim_box) = self.page_settings.trim_box() {
- let trim_box = transform_rect(trim_box);
+ let trim_box = transform_rect(trim_box)?;
page.trim_box(trim_box.to_pdf_rect());
}
if let Some(art_box) = self.page_settings.art_box() {
- let art_box = transform_rect(art_box);
+ let art_box = transform_rect(art_box)?;
page.art_box(art_box.to_pdf_rect());
}
diff --git a/src/serialize.rs b/src/serialize.rs
index 4f4af4c..f2986fc 100644
--- a/src/serialize.rs
+++ b/src/serialize.rs
@@ -1019,7 +1019,7 @@ impl SerializeContext {
) -> KrillaResult<()> {
let xyz_destinations = self.global_objects.xyz_destinations.take();
for (ref_, dest) in &xyz_destinations {
- dest.serialize(self, chunk_container, *ref_);
+ dest.serialize(self, chunk_container, *ref_)?;
}
Ok(())