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