From 2ab6049d9031bc8fae7f7df669b5fccaf5b9966f Mon Sep 17 00:00:00 2001 From: aiongg <67543387+aiongg@users.noreply.github.com> Date: Sat, 24 Oct 2026 09:38:24 -0400 Subject: [PATCH] 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) --- crates/pdf/tests/link_annotations.rs | 66 ++++ vendor/krilla/README.craft.md | 20 ++ vendor/krilla/SOURCE.json | 10 +- .../krilla/patches/13-annotation-bounds.patch | 299 ++++++++++++++++++ vendor/krilla/src/error.rs | 23 ++ vendor/krilla/src/interactive/annotation.rs | 60 ++-- vendor/krilla/src/interactive/destination.rs | 28 +- vendor/krilla/src/page.rs | 14 +- vendor/krilla/src/serialize.rs | 2 +- 9 files changed, 477 insertions(+), 45 deletions(-) create mode 100644 crates/pdf/tests/link_annotations.rs create mode 100644 vendor/krilla/patches/13-annotation-bounds.patch diff --git a/crates/pdf/tests/link_annotations.rs b/crates/pdf/tests/link_annotations.rs new file mode 100644 index 00000000..e637b3c7 --- /dev/null +++ b/crates/pdf/tests/link_annotations.rs @@ -0,0 +1,66 @@ +//! Link annotations, destinations and page boxes whose geometry has no finite place on the +//! page make `Writer::finish` return an error instead of panicking (krilla patch 13). +#![cfg(feature = "write")] +#![allow(clippy::unwrap_used, clippy::panic)] + +use backend::action::LinkAction; +use backend::annotation::{LinkAnnotation, Target}; +use backend::destination::XyzDestination; +use backend::error::KrillaError; +use backend::geom::{Point, Quadrilateral, Rect}; +use backend::page::PageSettings; +use craft_pdf::write::{Metadata, WriteError, Writer, backend}; + +fn link() -> Target { + Target::Action(LinkAction::new("https://example.com".into()).into()) +} + +fn quad(x: f32, y: f32) -> Quadrilateral { + Quadrilateral([Point::from_xy(x, y); 4]) +} + +fn write(settings: PageSettings, annotation: Option) -> Result, KrillaError> { + let mut writer = Writer::new(Default::default(), Metadata::default()); + let mut page = writer.document_mut().start_page_with(settings); + if let Some(annotation) = annotation { + page.add_annotation(annotation.into()); + } + page.finish(); + match writer.finish() { + Ok(pdf) => Ok(pdf), + Err(WriteError::Serialization(error)) => Err(*error), + Err(error) => panic!("unexpected error: {error}"), + } +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn unplaceable_geometry_is_an_error() { + let page = || PageSettings::from_wh(40.0, 20.0).unwrap(); + let big_page = || PageSettings::from_wh(40.0, 2e38).unwrap(); + + let ok = write(page(), Some(LinkAnnotation::new_with_quad_points(vec![quad(1.0, 2.0)], link()))); + assert!(ok.is_ok(), "{ok:?}"); + + let quads = [vec![], vec![quad(f32::NAN, f32::NAN)], vec![quad(1.0, 2.0), quad(f32::INFINITY, 2.0)], vec![quad(-3e38, 0.0), quad(3e38, 0.0)]]; + for (case, quads) in quads.into_iter().enumerate() { + let result = write(page(), Some(LinkAnnotation::new_with_quad_points(quads, link()))); + assert!(matches!(result, Err(KrillaError::InvalidAnnotationGeometry(None))), "case {case}: {result:?}"); + } + + // Finite, but 2e38 + 2e38 overflows when flipped into PDF coordinates. + let far = Rect::from_ltrb(0.0, -2e38, 10.0, -1e38).unwrap(); + let result = write(big_page(), Some(LinkAnnotation::new(far, link()))); + assert!(matches!(result, Err(KrillaError::InvalidAnnotationGeometry(None))), "{result:?}"); + + let result = write(big_page().with_crop_box(Some(far)), None); + assert!(matches!(result, Err(KrillaError::InvalidPageBox(0))), "{result:?}"); + + let missing_page = Target::Destination(XyzDestination::new(5, Point::from_xy(1.0, 1.0)).into()); + let result = write(page(), Some(LinkAnnotation::new(Rect::from_xywh(0.0, 0.0, 5.0, 5.0).unwrap(), missing_page))); + assert!(matches!(result, Err(KrillaError::InvalidDestination(5))), "{result:?}"); + + let far_point = Target::Destination(XyzDestination::new(0, Point::from_xy(1.0, -3e38)).into()); + let result = write(big_page(), Some(LinkAnnotation::new(Rect::from_xywh(0.0, 0.0, 5.0, 5.0).unwrap(), far_point))); + assert!(matches!(result, Err(KrillaError::InvalidDestination(0))), "{result:?}"); +} diff --git a/vendor/krilla/README.craft.md b/vendor/krilla/README.craft.md index 3b55e756..c47779c7 100644 --- a/vendor/krilla/README.craft.md +++ b/vendor/krilla/README.craft.md @@ -22,6 +22,7 @@ itself omits those texts. Its examples and four CC0 ICC profiles are retained wi | `patches/10-mesh-gradients.patch` | Mesh gradients | `content.rs`, `error.rs`, `graphics/{mask,paint,shading_function}.rs`, `serialize.rs`, `surface.rs` | | `patches/11-png-decode-cap.patch` | PNG decode cap | `graphics/image.rs` | | `patches/12-optional-content.patch` | Optional content | `chunk_container.rs`, `configure/{PDF_A1,PDF_A2,PDF_A4,PDF_UA1}.md`, `configure/validate.rs`, `content.rs`, `error.rs`, `graphics/{mod,optional_content}.rs`, `lib.rs`, `resource.rs`, `serialize.rs`, `surface.rs` | +| `patches/13-annotation-bounds.patch` | Annotation and destination bounds | `error.rs`, `interactive/{annotation,destination}.rs`, `page.rs`, `serialize.rs` | Each patch applies with `patch -p1` from this directory, in numeric order, to the pristine crate. `SOURCE.json` records, for every retained file, the upstream hash (`upstream_sha256`) @@ -336,3 +337,22 @@ get small hooks. `CRAFT_OPTIONAL_CONTENT` marks the patch; craft-pdf requires it at compile time. Tests: `crates/pdf/tests/optional_content.rs`. + +## 13: annotation and destination bounds + +Upstream panics on link geometry that has no finite place on the page, which aborts a WASM +app. `Document::finish` returns an error instead and writes nothing for the object: + +| Input | Upstream | Error | +|---|---|---| +| `LinkAnnotation::new_with_quad_points` with no quads, a non-finite point, or a bounding box whose width or height overflows | `assert!` or `unwrap` | `InvalidAnnotationGeometry(location)` | +| A link rect or quad bounding box that overflows when flipped into PDF coordinates (`page_height - y`) | `unwrap` | `InvalidAnnotationGeometry(location)` | +| A media, crop, bleed, trim or art box that overflows the same way | `unwrap` | `InvalidPageBox(page_index)` | +| An `XyzDestination` page index past the last page | `panic!` | `InvalidDestination(page_index)` | +| An `XyzDestination` point that maps to a non-finite coordinate | writes `inf` or `NaN` | `InvalidDestination(page_index)` | + +The constructors keep their signatures: `LinkAnnotation` holds an `Option` and the check +runs at serialization, before the annotation object starts. Refusing a single annotation +instead of skipping it keeps the indices of tagged annotations in the structure tree valid. + +Tests: `crates/pdf/tests/link_annotations.rs`. diff --git a/vendor/krilla/SOURCE.json b/vendor/krilla/SOURCE.json index ce4d6ed2..be542dd6 100644 --- a/vendor/krilla/SOURCE.json +++ b/vendor/krilla/SOURCE.json @@ -137,7 +137,7 @@ { "path": "src/error.rs", "upstream_sha256": "656b260bd99d250535099f17f2db8e6e6135e72c74fb6dff304371db9f646be5", - "sha256": "fadd496a1d3e782ef8dfc40a59bc0c070406846d8a24b8b21eaa6130d4c58f5a" + "sha256": "8d06efd85136668d1a491f2f04ba2d69e82347a89f275d9299998445f28172ac" }, { "path": "src/geom.rs", @@ -232,12 +232,12 @@ { "path": "src/interactive/annotation.rs", "upstream_sha256": "45b01acfef124a5ab9b6799ab4c83734f929b3573fdb28c218b108e220afad39", - "sha256": "45b01acfef124a5ab9b6799ab4c83734f929b3573fdb28c218b108e220afad39" + "sha256": "50f2a4bdc99d82bda275533e3a269eca6e7ae3ecb68b781203160122c744e55e" }, { "path": "src/interactive/destination.rs", "upstream_sha256": "fbe7ce81ce195dbc86502e8555268c026a7c795cfe8a94a975e23cab16b657c3", - "sha256": "fbe7ce81ce195dbc86502e8555268c026a7c795cfe8a94a975e23cab16b657c3" + "sha256": "3043468f3caafe0fd0d3cac6cdd6bafe5ccbd89c4c3a5817ce505d5fe9403c36" }, { "path": "src/interactive/mod.rs", @@ -302,7 +302,7 @@ { "path": "src/page.rs", "upstream_sha256": "ef85a771013896b185c9393414175ee91e5541c9a3fad4760c161764088ac177", - "sha256": "0c85a6b7be5c1e07180cea54f62b53e5a9e6c5c37790122f2ef5c9f8adc4efd7" + "sha256": "9822a623229000c1017d636867971a9f7aa3a4cc3094ba3057786ee146ee3dcd" }, { "path": "src/pdf.rs", @@ -317,7 +317,7 @@ { "path": "src/serialize.rs", "upstream_sha256": "4f2c439dbd20006eea112e056b2f1a5ba98a1ede9227a45f66a6df46fb1a07e8", - "sha256": "99cda1c62910281776726de63b1600830484be0972b8d60adbca2d9b341cd49b" + "sha256": "f5ccb6b92f546e1068e51e0791dbac8b802f07f29df0c2a6ccd1dcf8a1bdc938" }, { "path": "src/stream.rs", diff --git a/vendor/krilla/patches/13-annotation-bounds.patch b/vendor/krilla/patches/13-annotation-bounds.patch new file mode 100644 index 00000000..f7945f15 --- /dev/null +++ b/vendor/krilla/patches/13-annotation-bounds.patch @@ -0,0 +1,299 @@ +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), ++ /// 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), ++ /// 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::(); + + 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, + pub(crate) quad_points: Option>, + pub(crate) target: Target, + pub(crate) border: Option, +@@ -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, 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 { ++ 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 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(()) diff --git a/vendor/krilla/src/error.rs b/vendor/krilla/src/error.rs index a19ebf62..71580526 100644 --- a/vendor/krilla/src/error.rs +++ b/vendor/krilla/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), + /// 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), + /// 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/vendor/krilla/src/interactive/annotation.rs b/vendor/krilla/src/interactive/annotation.rs index 91c4cff6..f2121f1a 100644 --- a/vendor/krilla/src/interactive/annotation.rs +++ b/vendor/krilla/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::(); 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, pub(crate) quad_points: Option>, pub(crate) target: Target, pub(crate) border: Option, @@ -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, 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 { + 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/vendor/krilla/src/interactive/destination.rs b/vendor/krilla/src/interactive/destination.rs index de40085d..ec509511 100644 --- a/vendor/krilla/src/interactive/destination.rs +++ b/vendor/krilla/src/interactive/destination.rs @@ -117,7 +117,10 @@ impl From 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/vendor/krilla/src/page.rs b/vendor/krilla/src/page.rs index 3b3227b8..c5966776 100644 --- a/vendor/krilla/src/page.rs +++ b/vendor/krilla/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/vendor/krilla/src/serialize.rs b/vendor/krilla/src/serialize.rs index 4f4af4c9..f2986fc8 100644 --- a/vendor/krilla/src/serialize.rs +++ b/vendor/krilla/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(())