mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-15 19:24:48 +00:00
* 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)
300 lines
12 KiB
Diff
Generated
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(())
|