mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 07:39:23 +00:00
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:
1 parent
e179d42418
commit
2ab6049d90
9 files changed
+477
-45
No files matched your search
@@ -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:?}");
|
||||
}
|
||||
Vendored
+20
@@ -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`.
|
||||
Vendored
+5
-5
@@ -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
@@ -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(())
|
||||
Vendored
+23
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+7
-7
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -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(())
|
||||
|
||||
Reference in new issue
Block a user