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)
This commit is contained in:
aiongg authored and GitHub committed 2026-10-24 09:38:24 -04:00
1 parent e179d42418
commit 2ab6049d90
9 files changed
+477 -45

No files matched your search

+66
View File
@@ -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<LinkAnnotation>) -> Result<Vec<u8>, 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:?}");
}
+20
View File
@@ -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<Rect>` 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`.
+5 -5
View File
@@ -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",
+299
View File
@@ -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<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(())
+23
View File
@@ -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();
+41 -19
View File
@@ -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,
+15 -13
View File
@@ -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(())
}
}
+7 -7
View File
@@ -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());
}
+1 -1
View File
@@ -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(())