pdf_manip/error.rs
1//! Error types for `pdf-manip` operations.
2
3// Copyright (c) 2026 Innovation Trigger B.V.
4//
5// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
6// the PDFluent Commercial Licence. See the LICENSE file in this repository --
7// that file travels with the copy you received, which a URL does not.
8
9use thiserror::Error;
10
11/// Errors returned by the `pdf-manip` crate while performing page-level
12/// operations (merge, split, extract, rotate, watermark, encrypt, etc.).
13///
14/// These bubble up through the `pdfluent` facade as `Error::*` variants —
15/// customers will normally see them indirectly. Variants with explicit
16/// limit fields (image size, decompression cap) describe safety guards
17/// that refused to proceed; the source document is not modified.
18#[derive(Debug, Error)]
19pub enum ManipError {
20 /// The underlying PDF byte stream could not be parsed (malformed
21 /// xref, truncated stream, unsupported construct). The wrapped
22 /// `lopdf::Error` carries the detail.
23 #[error("PDF error: {0}")]
24 Pdf(#[from] lopdf::Error),
25
26 /// An I/O error while reading the source or writing the result.
27 #[error("IO error: {0}")]
28 Io(#[from] std::io::Error),
29
30 /// A page index passed to a manipulation routine does not exist in
31 /// the document. Fields: `(requested_index, total_pages)`. Page
32 /// indexes at this layer are 0-based; the public facade translates
33 /// to 1-based.
34 #[error("page index {0} out of range (document has {1} pages)")]
35 PageOutOfRange(usize, usize),
36
37 /// A range-based operation (extract, split) received an empty range.
38 /// Returned eagerly to surface the no-op rather than silently produce
39 /// an empty document.
40 #[error("empty page range")]
41 EmptyPageRange,
42
43 /// Setting up an encryption pipeline failed (key derivation, cipher
44 /// initialization, or invalid permission flag combination). The
45 /// wrapped string identifies the failing step.
46 #[error("encryption error: {0}")]
47 Encryption(String),
48
49 /// Decryption failed. Either the supplied password is wrong, the
50 /// document uses an algorithm this build does not support (RC4 in a
51 /// no-RC4 build), or the encryption dictionary is malformed.
52 #[error("decryption failed: wrong password or unsupported algorithm")]
53 DecryptionFailed,
54
55 /// The document advertises an encryption algorithm that the SDK
56 /// cannot read. The wrapped string identifies the algorithm
57 /// (e.g. `"V=4 RC4 with custom CFM"`).
58 #[error("unsupported encryption: {0}")]
59 UnsupportedEncryption(String),
60
61 /// A bookmark/outline operation received an invalid bookmark
62 /// description (missing title, invalid destination, malformed tree).
63 #[error("invalid bookmark: {0}")]
64 InvalidBookmark(String),
65
66 /// A watermark operation could not be applied (font/image resolution
67 /// failure, invalid coordinates, malformed text).
68 #[error("watermark error: {0}")]
69 Watermark(String),
70
71 /// An image operation (insertion, replacement, watermark image)
72 /// failed before the page was modified.
73 #[error("image error: {0}")]
74 Image(String),
75
76 /// An image operation refused to proceed because the requested image
77 /// would exceed the configured allocation safety limits.
78 ///
79 /// Allocation = `width * height * bytes_per_pixel`. The guards exist
80 /// to prevent denial-of-service via crafted PDFs that trigger huge
81 /// pixel-buffer allocations. If you genuinely need a larger image,
82 /// raise the limit in the operation's options or pre-resize the
83 /// image before insertion.
84 #[error(
85 "image {width}x{height} exceeds pdf-manip safety limits (max dimension {max_dimension}, max allocation {max_allocation_bytes} bytes at {bytes_per_pixel} bytes/pixel)"
86 )]
87 ImageTooLarge {
88 /// Image width in pixels as decoded from the source.
89 width: u32,
90 /// Image height in pixels as decoded from the source.
91 height: u32,
92 /// Pixel-format depth (e.g. 4 for RGBA8) used to compute the
93 /// allocation estimate.
94 bytes_per_pixel: u32,
95 /// The dimension cap (applied to both width and height) that the
96 /// image violated.
97 max_dimension: u32,
98 /// The total-allocation cap in bytes that the image would have
99 /// exceeded.
100 max_allocation_bytes: usize,
101 },
102
103 /// A FlateDecode-compressed stream expanded past the configured
104 /// decompression limit. Like [`ImageTooLarge`](Self::ImageTooLarge)
105 /// this is a denial-of-service guard. The `u64` is the byte limit
106 /// that was hit.
107 #[error("decompressed FlateDecode stream exceeds {0}-byte limit")]
108 DecompressionLimitExceeded(u64),
109
110 /// The requested bold or italic variant font is not embedded in the document.
111 ///
112 /// `set_text_run_style` only swaps to fonts that are already present in the
113 /// xref. Synthetic bold (stroke-and-fill) and system-font injection are
114 /// explicitly forbidden. The caller must embed the variant before styling.
115 ///
116 /// `available_variants` lists the BaseFont names of all variants of the same
117 /// family that *are* embedded, so the caller can surface actionable guidance.
118 #[error(
119 "font variant not embedded: cannot apply {requested:?} (current font {current:?}); \
120 available embedded variants: {available_variants:?}"
121 )]
122 FontVariantNotEmbedded {
123 /// BaseFont name of the current (pre-swap) font.
124 current: String,
125 /// BaseFont name of the desired variant that was not found.
126 requested: String,
127 /// All embedded font variants belonging to the same family.
128 available_variants: Vec<String>,
129 },
130
131 /// A non-categorised manipulation failure. Reserved for cases the
132 /// more specific variants do not cover; the message describes the
133 /// situation.
134 #[error("{0}")]
135 Other(String),
136}
137
138/// Convenience `Result` alias for fallible `pdf-manip` operations.
139pub type Result<T> = std::result::Result<T, ManipError>;