Skip to main content

pdf_engine/
api_error.rs

1use std::fmt;
2use std::path::PathBuf;
3
4/// The central Error trait for all PDFluent operations.
5pub trait PdfError: std::error::Error {
6    /// Machine-readable error code (bijv. "PASSWORD_REQUIRED")
7    fn code(&self) -> &str;
8
9    /// Menselijk leesbare help tekst met code example
10    fn help(&self) -> Option<String>;
11
12    /// URL naar de error docs
13    fn docs_url(&self) -> String {
14        format!(
15            "https://docs.pdfluent.dev/errors/{}",
16            self.code().to_lowercase().replace('_', "-")
17        )
18    }
19}
20
21/// The central Error enum for all PDFluent operations.
22/// Designed to provide high context and actionable help for developers.
23#[derive(Debug)]
24pub enum Error {
25    FileNotFound {
26        path: PathBuf,
27    },
28    PasswordRequired {
29        path: PathBuf,
30    },
31    CorruptPdf {
32        path: Option<PathBuf>,
33        reason: String,
34    },
35    InvalidPageNumber {
36        requested: usize,
37        total: usize,
38    },
39    FontNotFound {
40        font_name: String,
41    },
42    PermissionDenied {
43        reason: String,
44    },
45    UnsupportedPdfVersion {
46        version: String,
47    },
48    FormFieldNotFound {
49        field_name: String,
50    },
51    SignatureVerificationFailed {
52        reason: String,
53    },
54    RedactionFailed {
55        reason: String,
56    },
57    ConversionFailed {
58        reason: String,
59    },
60    InvalidEncoding {
61        encoding: String,
62    },
63    StreamDecodeFailed {
64        filter: String,
65    },
66    XrefCorrupt {
67        reason: String,
68    },
69    LicenseExpired {
70        expired_since: String,
71    },
72    LicenseInvalid {
73        reason: String,
74    },
75    OutputWriteFailed {
76        path: PathBuf,
77        reason: String,
78    },
79    ImageDecodeFailed {
80        format: String,
81    },
82    EncryptionFailed {
83        reason: String,
84    },
85    ComplianceViolation {
86        standard: String,
87        reason: String,
88    },
89    Io(std::io::Error),
90    UnsupportedFeature {
91        feature: String,
92    },
93}
94
95impl std::error::Error for Error {
96    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
97        match self {
98            Error::Io(err) => Some(err),
99            _ => None,
100        }
101    }
102}
103
104impl From<std::io::Error> for Error {
105    fn from(err: std::io::Error) -> Self {
106        Error::Io(err)
107    }
108}
109
110impl PdfError for Error {
111    fn code(&self) -> &str {
112        match self {
113            Error::FileNotFound { .. } => "FILE_NOT_FOUND",
114            Error::PasswordRequired { .. } => "PASSWORD_REQUIRED",
115            Error::CorruptPdf { .. } => "CORRUPT_PDF",
116            Error::InvalidPageNumber { .. } => "INVALID_PAGE_NUMBER",
117            Error::FontNotFound { .. } => "FONT_NOT_FOUND",
118            Error::PermissionDenied { .. } => "PERMISSION_DENIED",
119            Error::UnsupportedPdfVersion { .. } => "UNSUPPORTED_PDF_VERSION",
120            Error::FormFieldNotFound { .. } => "FORM_FIELD_NOT_FOUND",
121            Error::SignatureVerificationFailed { .. } => "SIGNATURE_VERIFICATION_FAILED",
122            Error::RedactionFailed { .. } => "REDACTION_FAILED",
123            Error::ConversionFailed { .. } => "CONVERSION_FAILED",
124            Error::InvalidEncoding { .. } => "INVALID_ENCODING",
125            Error::StreamDecodeFailed { .. } => "STREAM_DECODE_FAILED",
126            Error::XrefCorrupt { .. } => "XREF_CORRUPT",
127            Error::LicenseExpired { .. } => "LICENSE_EXPIRED",
128            Error::LicenseInvalid { .. } => "LICENSE_INVALID",
129            Error::OutputWriteFailed { .. } => "OUTPUT_WRITE_FAILED",
130            Error::ImageDecodeFailed { .. } => "IMAGE_DECODE_FAILED",
131            Error::EncryptionFailed { .. } => "ENCRYPTION_FAILED",
132            Error::ComplianceViolation { .. } => "COMPLIANCE_VIOLATION",
133            Error::Io(_) => "IO_ERROR",
134            Error::UnsupportedFeature { .. } => "UNSUPPORTED_FEATURE",
135        }
136    }
137
138    fn help(&self) -> Option<String> {
139        match self {
140            Error::FileNotFound { .. } => Some(format!("Check that the file exists and the path is correct.\n        Current directory: {}", std::env::current_dir().unwrap_or_default().display())),
141            Error::PasswordRequired { path } => {
142                let file_name = path.file_name().unwrap_or_default().to_string_lossy();
143                Some(format!("Pass a password when reading the file:\n\n    let doc = pdfluent::read_with(\"{}\", |opts| {{\n        opts.password(\"your-password\")\n    }})?;", file_name))
144            }
145            Error::CorruptPdf { .. } => Some("Try opts.repair(true) to attempt automatic repair.".to_string()),
146            Error::InvalidPageNumber { requested, total } => Some(format!("The document has {} pages. Requested {}, use a 1-based index up to doc.page_count().", total, requested)),
147            Error::FontNotFound { font_name } => Some(format!("Provide a custom font mapping for '{}', or ensure the font is installed on the system.", font_name)),
148            Error::PermissionDenied { .. } => Some("The PDF's permissions do not allow this operation. You may need an owner password.".to_string()),
149            Error::UnsupportedPdfVersion { version } => Some(format!("The SDK currently does not support PDF version {}.", version)),
150            Error::FormFieldNotFound { field_name } => Some(format!("Double-check the field name '{}' using doc.form_fields().", field_name)),
151            Error::SignatureVerificationFailed { .. } => Some("Check the certificate chain, validity period, and document integrity.".to_string()),
152            Error::RedactionFailed { .. } => Some("Ensure coordinates are within page bounds and the document allows redaction.".to_string()),
153            Error::ConversionFailed { .. } => Some("The document could not be converted to the requested format.".to_string()),
154            Error::InvalidEncoding { encoding } => Some(format!("The encoding '{}' is invalid or unsupported.", encoding)),
155            Error::StreamDecodeFailed { filter } => Some(format!("The stream could not be decoded using filter '{}'.", filter)),
156            Error::XrefCorrupt { .. } => Some("The cross-reference table is corrupt. Try opts.repair(true).".to_string()),
157            Error::LicenseExpired { .. } => Some("Please renew your license key at https://pdfluent.dev/pricing".to_string()),
158            Error::LicenseInvalid { .. } => Some("Check your license key or environment variables.".to_string()),
159            Error::OutputWriteFailed { .. } => Some("Ensure the destination path is writable and you have sufficient disk space.".to_string()),
160            Error::ImageDecodeFailed { format } => Some(format!("The image format '{}' could not be decoded.", format)),
161            Error::EncryptionFailed { .. } => Some("Check the encryption parameters and permissions.".to_string()),
162            Error::ComplianceViolation { standard, .. } => Some(format!("The document violates the {} standard. Consider using a compliance repair tool.", standard)),
163            Error::Io(_) => Some("Check the underlying I/O error details.".to_string()),
164            Error::UnsupportedFeature { .. } => Some("This feature is not yet supported by the PDFluent SDK.".to_string()),
165        }
166    }
167}
168
169impl fmt::Display for Error {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        let name = match self {
172            Error::FileNotFound { .. } => "FileNotFound",
173            Error::PasswordRequired { .. } => "PasswordRequired",
174            Error::CorruptPdf { .. } => "CorruptPdf",
175            Error::InvalidPageNumber { .. } => "InvalidPageNumber",
176            Error::FontNotFound { .. } => "FontNotFound",
177            Error::PermissionDenied { .. } => "PermissionDenied",
178            Error::UnsupportedPdfVersion { .. } => "UnsupportedPdfVersion",
179            Error::FormFieldNotFound { .. } => "FormFieldNotFound",
180            Error::SignatureVerificationFailed { .. } => "SignatureVerificationFailed",
181            Error::RedactionFailed { .. } => "RedactionFailed",
182            Error::ConversionFailed { .. } => "ConversionFailed",
183            Error::InvalidEncoding { .. } => "InvalidEncoding",
184            Error::StreamDecodeFailed { .. } => "StreamDecodeFailed",
185            Error::XrefCorrupt { .. } => "XrefCorrupt",
186            Error::LicenseExpired { .. } => "LicenseExpired",
187            Error::LicenseInvalid { .. } => "LicenseInvalid",
188            Error::OutputWriteFailed { .. } => "OutputWriteFailed",
189            Error::ImageDecodeFailed { .. } => "ImageDecodeFailed",
190            Error::EncryptionFailed { .. } => "EncryptionFailed",
191            Error::ComplianceViolation { .. } => "ComplianceViolation",
192            Error::Io(_) => "IoError",
193            Error::UnsupportedFeature { .. } => "UnsupportedFeature",
194        };
195
196        write!(f, "Error: {}\n\n", name)?;
197
198        match self {
199            Error::FileNotFound { path } => {
200                write!(f, "  Could not find the file at: {}\n\n", path.display())?;
201            }
202            Error::PasswordRequired { path } => {
203                write!(
204                    f,
205                    "  This PDF is encrypted and requires a password to open.\n\n"
206                )?;
207                write!(f, "  File: {}\n\n", path.display())?;
208            }
209            Error::CorruptPdf { path, reason } => {
210                if let Some(p) = path {
211                    write!(
212                        f,
213                        "  The PDF file '{}' is corrupt: {}\n\n",
214                        p.display(),
215                        reason
216                    )?;
217                } else {
218                    write!(f, "  The PDF data is corrupt: {}\n\n", reason)?;
219                }
220            }
221            Error::InvalidPageNumber { requested, total } => {
222                write!(
223                    f,
224                    "  Requested page number {}, but the document only has {} pages.\n\n",
225                    requested, total
226                )?;
227            }
228            Error::FontNotFound { font_name } => {
229                write!(
230                    f,
231                    "  The required font '{}' could not be found.\n\n",
232                    font_name
233                )?;
234            }
235            Error::PermissionDenied { reason } => {
236                write!(
237                    f,
238                    "  Operation denied by document permissions: {}\n\n",
239                    reason
240                )?;
241            }
242            Error::UnsupportedPdfVersion { version } => {
243                write!(f, "  PDF version {} is not supported.\n\n", version)?;
244            }
245            Error::FormFieldNotFound { field_name } => {
246                write!(f, "  Could not find form field: '{}'\n\n", field_name)?;
247            }
248            Error::SignatureVerificationFailed { reason } => {
249                write!(f, "  Signature verification failed: {}\n\n", reason)?;
250            }
251            Error::RedactionFailed { reason } => {
252                write!(f, "  Redaction operation failed: {}\n\n", reason)?;
253            }
254            Error::ConversionFailed { reason } => {
255                write!(f, "  Conversion failed: {}\n\n", reason)?;
256            }
257            Error::InvalidEncoding { encoding } => {
258                write!(
259                    f,
260                    "  Invalid or unsupported text encoding: {}\n\n",
261                    encoding
262                )?;
263            }
264            Error::StreamDecodeFailed { filter } => {
265                write!(f, "  Failed to decode stream using filter: {}\n\n", filter)?;
266            }
267            Error::XrefCorrupt { reason } => {
268                write!(f, "  The cross-reference table is corrupt: {}\n\n", reason)?;
269            }
270            Error::LicenseExpired { expired_since } => {
271                write!(
272                    f,
273                    "  Your PDFluent license expired on {}.\n\n",
274                    expired_since
275                )?;
276            }
277            Error::LicenseInvalid { reason } => {
278                write!(f, "  Invalid license key: {}\n\n", reason)?;
279            }
280            Error::OutputWriteFailed { path, reason } => {
281                write!(
282                    f,
283                    "  Failed to write output to '{}': {}\n\n",
284                    path.display(),
285                    reason
286                )?;
287            }
288            Error::ImageDecodeFailed { format } => {
289                write!(f, "  Failed to decode {} image.\n\n", format)?;
290            }
291            Error::EncryptionFailed { reason } => {
292                write!(f, "  Encryption operation failed: {}\n\n", reason)?;
293            }
294            Error::ComplianceViolation { standard, reason } => {
295                write!(
296                    f,
297                    "  Document violates {} compliance: {}\n\n",
298                    standard, reason
299                )?;
300            }
301            Error::Io(err) => {
302                write!(f, "  I/O error occurred: {}\n\n", err)?;
303            }
304            Error::UnsupportedFeature { feature } => {
305                write!(f, "  Unsupported feature: {}\n\n", feature)?;
306            }
307        }
308
309        if let Some(help) = self.help() {
310            write!(f, "  Help: ")?;
311            let mut first = true;
312            for line in help.lines() {
313                if first {
314                    write!(f, "{}\n", line)?;
315                    first = false;
316                } else {
317                    if line.is_empty() {
318                        write!(f, "\n")?;
319                    } else {
320                        write!(f, "        {}\n", line)?;
321                    }
322                }
323            }
324            write!(f, "\n")?;
325        }
326
327        write!(f, "  Docs: {}", self.docs_url())
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn test_file_not_found_message() {
337        let err = Error::FileNotFound {
338            path: PathBuf::from("/tmp/nonexistent.pdf"),
339        };
340        let msg = format!("{}", err);
341        assert!(msg.contains("FileNotFound"));
342        assert!(msg.contains("/tmp/nonexistent.pdf"));
343        assert!(msg.contains("Help:"));
344        assert!(msg.contains("docs.pdfluent.dev"));
345    }
346
347    #[test]
348    fn test_password_required_message() {
349        let err = Error::PasswordRequired {
350            path: PathBuf::from("invoice-2024.pdf"),
351        };
352        let msg = format!("{}", err);
353        assert!(msg.contains("PasswordRequired"));
354        assert!(msg.contains("invoice-2024.pdf"));
355        assert!(msg.contains("Help: Pass a password when reading the file:"));
356        assert!(msg.contains("docs.pdfluent.dev"));
357    }
358}