1use std::fmt;
2use std::path::PathBuf;
3
4pub trait PdfError: std::error::Error {
6 fn code(&self) -> &str;
8
9 fn help(&self) -> Option<String>;
11
12 fn docs_url(&self) -> String {
14 format!(
15 "https://docs.pdfluent.dev/errors/{}",
16 self.code().to_lowercase().replace('_', "-")
17 )
18 }
19}
20
21#[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}