1use std::fmt;
8use std::path::PathBuf;
9
10pub trait PdfError: std::error::Error {
17 fn code(&self) -> &str;
24
25 fn help(&self) -> Option<String>;
31
32 fn docs_url(&self) -> String {
39 format!(
40 "{ERROR_DOCS_BASE}{}",
41 self.code().to_lowercase().replace('_', "-")
42 )
43 }
44}
45
46const ERROR_DOCS_BASE: &str = "https://pdfluent.com/errors/#";
49
50#[derive(Debug)]
62pub enum Error {
63 FileNotFound {
66 path: PathBuf,
68 },
69 PasswordRequired {
72 path: PathBuf,
74 },
75 CorruptPdf {
79 path: Option<PathBuf>,
81 reason: String,
84 },
85 InvalidPageNumber {
88 requested: usize,
90 total: usize,
92 },
93 FontNotFound {
96 font_name: String,
98 },
99 PermissionDenied {
102 reason: String,
104 },
105 UnsupportedPdfVersion {
108 version: String,
110 },
111 FormFieldNotFound {
114 field_name: String,
117 },
118 SignatureVerificationFailed {
121 reason: String,
123 },
124 RedactionFailed {
128 reason: String,
130 },
131 ConversionFailed {
134 reason: String,
136 },
137 InvalidEncoding {
139 encoding: String,
141 },
142 StreamDecodeFailed {
145 filter: String,
147 },
148 XrefCorrupt {
151 reason: String,
153 },
154 OutputWriteFailed {
157 path: PathBuf,
159 reason: String,
161 },
162 ImageDecodeFailed {
165 format: String,
167 },
168 EncryptionFailed {
171 reason: String,
173 },
174 ComplianceViolation {
177 standard: String,
180 reason: String,
182 },
183 Io(std::io::Error),
186 UnsupportedFeature {
190 feature: String,
193 },
194}
195
196impl std::error::Error for Error {
197 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
198 match self {
199 Error::Io(err) => Some(err),
200 _ => None,
201 }
202 }
203}
204
205impl From<std::io::Error> for Error {
206 fn from(err: std::io::Error) -> Self {
207 Error::Io(err)
208 }
209}
210
211impl PdfError for Error {
212 fn code(&self) -> &str {
213 match self {
214 Error::FileNotFound { .. } => "FILE_NOT_FOUND",
215 Error::PasswordRequired { .. } => "PASSWORD_REQUIRED",
216 Error::CorruptPdf { .. } => "CORRUPT_PDF",
217 Error::InvalidPageNumber { .. } => "INVALID_PAGE_NUMBER",
218 Error::FontNotFound { .. } => "FONT_NOT_FOUND",
219 Error::PermissionDenied { .. } => "PERMISSION_DENIED",
220 Error::UnsupportedPdfVersion { .. } => "UNSUPPORTED_PDF_VERSION",
221 Error::FormFieldNotFound { .. } => "FORM_FIELD_NOT_FOUND",
222 Error::SignatureVerificationFailed { .. } => "SIGNATURE_VERIFICATION_FAILED",
223 Error::RedactionFailed { .. } => "REDACTION_FAILED",
224 Error::ConversionFailed { .. } => "CONVERSION_FAILED",
225 Error::InvalidEncoding { .. } => "INVALID_ENCODING",
226 Error::StreamDecodeFailed { .. } => "STREAM_DECODE_FAILED",
227 Error::XrefCorrupt { .. } => "XREF_CORRUPT",
228 Error::OutputWriteFailed { .. } => "OUTPUT_WRITE_FAILED",
229 Error::ImageDecodeFailed { .. } => "IMAGE_DECODE_FAILED",
230 Error::EncryptionFailed { .. } => "ENCRYPTION_FAILED",
231 Error::ComplianceViolation { .. } => "COMPLIANCE_VIOLATION",
232 Error::Io(_) => "IO_ERROR",
233 Error::UnsupportedFeature { .. } => "UNSUPPORTED_FEATURE",
234 }
235 }
236
237 fn help(&self) -> Option<String> {
238 match self {
239 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())),
240 Error::PasswordRequired { path } => {
241 let file_name = path.file_name().unwrap_or_default().to_string_lossy();
242 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))
243 }
244 Error::CorruptPdf { .. } => Some("Try opts.repair(true) to attempt automatic repair.".to_string()),
245 Error::InvalidPageNumber { requested, total } => Some(format!("The document has {} pages. Requested {}, use a 1-based index up to doc.page_count().", total, requested)),
246 Error::FontNotFound { font_name } => Some(format!("Provide a custom font mapping for '{}', or ensure the font is installed on the system.", font_name)),
247 Error::PermissionDenied { .. } => Some("The PDF's permissions do not allow this operation. You may need an owner password.".to_string()),
248 Error::UnsupportedPdfVersion { version } => Some(format!("The SDK currently does not support PDF version {}.", version)),
249 Error::FormFieldNotFound { field_name } => Some(format!("Double-check the field name '{}' using doc.form_fields().", field_name)),
250 Error::SignatureVerificationFailed { .. } => Some("Check the certificate chain, validity period, and document integrity.".to_string()),
251 Error::RedactionFailed { .. } => Some("Ensure coordinates are within page bounds and the document allows redaction.".to_string()),
252 Error::ConversionFailed { .. } => Some("The document could not be converted to the requested format.".to_string()),
253 Error::InvalidEncoding { encoding } => Some(format!("The encoding '{}' is invalid or unsupported.", encoding)),
254 Error::StreamDecodeFailed { filter } => Some(format!("The stream could not be decoded using filter '{}'.", filter)),
255 Error::XrefCorrupt { .. } => Some("The cross-reference table is corrupt. Try opts.repair(true).".to_string()),
256 Error::OutputWriteFailed { .. } => Some("Ensure the destination path is writable and you have sufficient disk space.".to_string()),
257 Error::ImageDecodeFailed { format } => Some(format!("The image format '{}' could not be decoded.", format)),
258 Error::EncryptionFailed { .. } => Some("Check the encryption parameters and permissions.".to_string()),
259 Error::ComplianceViolation { standard, .. } => Some(format!("The document violates the {} standard. Consider using a compliance repair tool.", standard)),
260 Error::Io(_) => Some("Check the underlying I/O error details.".to_string()),
261 Error::UnsupportedFeature { .. } => Some("This feature is not yet supported by the PDFluent SDK.".to_string()),
262 }
263 }
264}
265
266impl fmt::Display for Error {
267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268 let name = match self {
269 Error::FileNotFound { .. } => "FileNotFound",
270 Error::PasswordRequired { .. } => "PasswordRequired",
271 Error::CorruptPdf { .. } => "CorruptPdf",
272 Error::InvalidPageNumber { .. } => "InvalidPageNumber",
273 Error::FontNotFound { .. } => "FontNotFound",
274 Error::PermissionDenied { .. } => "PermissionDenied",
275 Error::UnsupportedPdfVersion { .. } => "UnsupportedPdfVersion",
276 Error::FormFieldNotFound { .. } => "FormFieldNotFound",
277 Error::SignatureVerificationFailed { .. } => "SignatureVerificationFailed",
278 Error::RedactionFailed { .. } => "RedactionFailed",
279 Error::ConversionFailed { .. } => "ConversionFailed",
280 Error::InvalidEncoding { .. } => "InvalidEncoding",
281 Error::StreamDecodeFailed { .. } => "StreamDecodeFailed",
282 Error::XrefCorrupt { .. } => "XrefCorrupt",
283 Error::OutputWriteFailed { .. } => "OutputWriteFailed",
284 Error::ImageDecodeFailed { .. } => "ImageDecodeFailed",
285 Error::EncryptionFailed { .. } => "EncryptionFailed",
286 Error::ComplianceViolation { .. } => "ComplianceViolation",
287 Error::Io(_) => "IoError",
288 Error::UnsupportedFeature { .. } => "UnsupportedFeature",
289 };
290
291 write!(f, "Error: {}\n\n", name)?;
292
293 match self {
294 Error::FileNotFound { path } => {
295 write!(f, " Could not find the file at: {}\n\n", path.display())?;
296 }
297 Error::PasswordRequired { path } => {
298 write!(
299 f,
300 " This PDF is encrypted and requires a password to open.\n\n"
301 )?;
302 write!(f, " File: {}\n\n", path.display())?;
303 }
304 Error::CorruptPdf { path, reason } => {
305 if let Some(p) = path {
306 write!(
307 f,
308 " The PDF file '{}' is corrupt: {}\n\n",
309 p.display(),
310 reason
311 )?;
312 } else {
313 write!(f, " The PDF data is corrupt: {}\n\n", reason)?;
314 }
315 }
316 Error::InvalidPageNumber { requested, total } => {
317 write!(
318 f,
319 " Requested page number {}, but the document only has {} pages.\n\n",
320 requested, total
321 )?;
322 }
323 Error::FontNotFound { font_name } => {
324 write!(
325 f,
326 " The required font '{}' could not be found.\n\n",
327 font_name
328 )?;
329 }
330 Error::PermissionDenied { reason } => {
331 write!(
332 f,
333 " Operation denied by document permissions: {}\n\n",
334 reason
335 )?;
336 }
337 Error::UnsupportedPdfVersion { version } => {
338 write!(f, " PDF version {} is not supported.\n\n", version)?;
339 }
340 Error::FormFieldNotFound { field_name } => {
341 write!(f, " Could not find form field: '{}'\n\n", field_name)?;
342 }
343 Error::SignatureVerificationFailed { reason } => {
344 write!(f, " Signature verification failed: {}\n\n", reason)?;
345 }
346 Error::RedactionFailed { reason } => {
347 write!(f, " Redaction operation failed: {}\n\n", reason)?;
348 }
349 Error::ConversionFailed { reason } => {
350 write!(f, " Conversion failed: {}\n\n", reason)?;
351 }
352 Error::InvalidEncoding { encoding } => {
353 write!(
354 f,
355 " Invalid or unsupported text encoding: {}\n\n",
356 encoding
357 )?;
358 }
359 Error::StreamDecodeFailed { filter } => {
360 write!(f, " Failed to decode stream using filter: {}\n\n", filter)?;
361 }
362 Error::XrefCorrupt { reason } => {
363 write!(f, " The cross-reference table is corrupt: {}\n\n", reason)?;
364 }
365 Error::OutputWriteFailed { path, reason } => {
366 write!(
367 f,
368 " Failed to write output to '{}': {}\n\n",
369 path.display(),
370 reason
371 )?;
372 }
373 Error::ImageDecodeFailed { format } => {
374 write!(f, " Failed to decode {} image.\n\n", format)?;
375 }
376 Error::EncryptionFailed { reason } => {
377 write!(f, " Encryption operation failed: {}\n\n", reason)?;
378 }
379 Error::ComplianceViolation { standard, reason } => {
380 write!(
381 f,
382 " Document violates {} compliance: {}\n\n",
383 standard, reason
384 )?;
385 }
386 Error::Io(err) => {
387 write!(f, " I/O error occurred: {}\n\n", err)?;
388 }
389 Error::UnsupportedFeature { feature } => {
390 write!(f, " Unsupported feature: {}\n\n", feature)?;
391 }
392 }
393
394 if let Some(help) = self.help() {
395 write!(f, " Help: ")?;
396 let mut first = true;
397 for line in help.lines() {
398 if first {
399 writeln!(f, "{}", line)?;
400 first = false;
401 } else if line.is_empty() {
402 writeln!(f)?;
403 } else {
404 writeln!(f, " {}", line)?;
405 }
406 }
407 writeln!(f)?;
408 }
409
410 write!(f, " Docs: {}", self.docs_url())
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 #[test]
419 fn test_file_not_found_message() {
420 let err = Error::FileNotFound {
421 path: PathBuf::from("/tmp/nonexistent.pdf"),
422 };
423 let msg = format!("{}", err);
424 assert!(msg.contains("FileNotFound"));
425 assert!(msg.contains("/tmp/nonexistent.pdf"));
426 assert!(msg.contains("Help:"));
427 assert!(msg.contains("Docs: https://pdfluent.com/errors/#file-not-found"));
428 }
429
430 #[test]
431 fn test_password_required_message() {
432 let err = Error::PasswordRequired {
433 path: PathBuf::from("invoice-2024.pdf"),
434 };
435 let msg = format!("{}", err);
436 assert!(msg.contains("PasswordRequired"));
437 assert!(msg.contains("invoice-2024.pdf"));
438 assert!(msg.contains("Help: Pass a password when reading the file:"));
439 assert!(msg.contains("Docs: https://pdfluent.com/errors/#password-required"));
440 }
441
442 #[test]
443 fn docs_url_is_the_anchor_of_the_code_on_the_errors_page() {
444 let err = Error::PasswordRequired {
445 path: PathBuf::from("a.pdf"),
446 };
447 assert_eq!(err.code(), "PASSWORD_REQUIRED");
448 assert_eq!(
449 err.docs_url(),
450 "https://pdfluent.com/errors/#password-required"
451 );
452 }
453}