diff --git a/Cargo.lock b/Cargo.lock index 49b8c25..6119827 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -251,6 +266,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets", +] + [[package]] name = "clang-sys" version = "1.6.1" @@ -339,6 +366,48 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "darling" version = "0.10.2" @@ -374,6 +443,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deranged" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive-getters" version = "0.3.0" @@ -483,9 +561,11 @@ name = "escl-scan" version = "0.1.4" dependencies = [ "log", + "lopdf", "reqwest", "serde", "serde-xml-rs", + "uuid", "zeroconf", ] @@ -505,6 +585,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -583,6 +673,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.0" @@ -736,6 +837,29 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -822,6 +946,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.11" @@ -834,12 +964,46 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "lopdf" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a" +dependencies = [ + "chrono", + "encoding_rs", + "flate2", + "itoa", + "linked-hash-map", + "log", + "md5", + "nom 7.1.3", + "rayon", + "time", + "weezl", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -910,6 +1074,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -1009,6 +1182,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "prettyplease" version = "0.2.15" @@ -1043,6 +1222,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1159,6 +1358,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "2.9.2" @@ -1390,6 +1595,35 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1520,6 +1754,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -1629,6 +1872,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "which" version = "3.1.1" @@ -1681,6 +1930,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/escl-scan/Cargo.toml b/escl-scan/Cargo.toml index 63d9db7..2398650 100644 --- a/escl-scan/Cargo.toml +++ b/escl-scan/Cargo.toml @@ -20,4 +20,6 @@ log = "0.4.*" reqwest = { version = "0.11.16", features = ["blocking"] } serde = { version = "1.0.160", features = ["derive"] } serde-xml-rs = "0.6.0" +lopdf = "0.31.*" +uuid = { version = "1.6.*", features = ["v4"] } zeroconf = "0.12.*" diff --git a/escl-scan/src/scanner.rs b/escl-scan/src/scanner.rs index 03d2968..ea7b581 100644 --- a/escl-scan/src/scanner.rs +++ b/escl-scan/src/scanner.rs @@ -7,13 +7,15 @@ extern crate reqwest; extern crate serde; extern crate serde_xml_rs; +extern crate uuid; use crate::{ scannererror::{ErrorCode, ScannerError}, - structs, + structs::{self}, }; -use reqwest::blocking::Response; -use std::{fmt::Display, fs::File}; +use lopdf::{Bookmark, Document, Object, ObjectId}; +use std::{collections::BTreeMap, fmt::Display, fs, path::Path}; +use uuid::Uuid; #[derive(Clone, Debug)] pub struct Scanner { @@ -147,42 +149,342 @@ impl Scanner { }; let download_url = format!("{}/NextDocument", location); - return self.download_page(&download_url, destination_file); + return self.download_scanned_pages(&scan_settings, &download_url, destination_file); } - fn download_page( + fn download_scanned_pages( &self, + scan_settings: &structs::ScanSettings, download_url: &str, destination_file: &str, ) -> Result<(), ScannerError> { - // We need to try downloadng at least once again, expecting a 404, to make - // sure we got everything. + // We need to try downloadng pages until we get a 404 for the printer to + // consider the scan job done. // This is necessary on my Brother MFC-L2710DW to get it to idle state // again. It will wait for timeout otherwise, even if we got the scanned // page earlier. - let mut page: u16 = 1; + + let mut new_page_idx: u16 = 1; loop { - log::info!("Downloading page {page} to {destination_file}"); - let mut response = match reqwest::blocking::get(download_url) { - Ok(response) => response, - Err(err) => return Err(err.into()), - }; + let tmp_page_path = std::env::temp_dir().join(Uuid::new_v4().to_string()); + log::info!( + "Downloading page {} to {}", + new_page_idx, + tmp_page_path.to_str().expect("File path is printable") + ); + let mut tmp_page_file = fs::File::create(&tmp_page_path)?; + + if let Err(err) = self.download_scanned_page(download_url, &mut tmp_page_file) { + match err.code { + ErrorCode::NoMorePages => { + if new_page_idx == 1 { + log::error!("Scanner has no pages available for download at all"); + return Err(err); + } else { + log::info!("There is no page {new_page_idx}, we're done"); + return Ok(()); + } + } + _ => return Err(err), + } + } + + if let Err(err) = self.process_scanned_page( + scan_settings, + &tmp_page_path, + destination_file, + new_page_idx, + ) { + fs::remove_file(tmp_page_path)?; + return Err(err); + } + + new_page_idx += 1; + } + } + + fn download_scanned_page( + &self, + download_url: &str, + destination_file: &mut fs::File, + ) -> Result<(), ScannerError> { + let mut response = match reqwest::blocking::get(download_url) { + Ok(response) => response, + Err(err) => return Err(err.into()), + }; + + if response.status() == 404 { + return Err(ScannerError { + code: ErrorCode::NoMorePages, + message: String::new(), + }); + } + + if let Err(err) = response.copy_to(destination_file) { + return Err(err.into()); + } + + Ok(()) + } - if response.status() == 404 { - log::info!("There is no page {page}, we're done"); - break; + fn process_scanned_page( + &self, + scan_settings: &structs::ScanSettings, + tmp_page_path: &Path, + destination_file: &str, + page_idx: u16, + ) -> Result<(), ScannerError> { + // This could be more elegant if I managed to (de)serialize the format + // to/from an enum... + if scan_settings.document_format.contains("pdf") { + if Path::new(destination_file).exists() { + log::info!("Appending page to existing document {destination_file}"); + self.merge_scanned_page(Path::new(destination_file), tmp_page_path)?; + } else { + fs::rename(tmp_page_path, &destination_file)?; } + } else { + let page_file_name = self.make_jpg_file_name(&destination_file, page_idx)?; + log::info!("Storing scanned page as {page_file_name}"); + fs::rename(tmp_page_path, &page_file_name)?; + } + + Ok(()) + } - let mut file = match File::create(destination_file) { - Ok(file) => file, + fn make_jpg_file_name( + &self, + destination_file: &str, + new_page_idx: u16, + ) -> Result { + if !Path::new(destination_file).exists() { + log::info!("Destination file does not exist yet"); + return Ok(destination_file.into()); + } + + // We cannot write to destination_file, try numbering pages + if let Some(last_dot_pos) = destination_file.rfind('.') { + let (path_part, ext_part) = destination_file.split_at(last_dot_pos); + + let page_file_name = format!("{path_part}_{new_page_idx}{ext_part}"); + if !Path::new(&page_file_name).exists() { + log::info!("Created page file name \"{page_file_name}\""); + return Ok(page_file_name); + } + + let numbered_path_part = format!("{path_part}_{new_page_idx}"); + for i in 1..u16::MAX { + let fallback_numbered_file_name = format!("{numbered_path_part}_{i}{ext_part}"); + if !Path::new(&fallback_numbered_file_name).exists() { + log::info!("Created fallback file name \"{fallback_numbered_file_name}\""); + return Ok(fallback_numbered_file_name); + } + } + + return Err(ScannerError { + code: ErrorCode::FilesystemError, + message: format!( + "Failed to determine an alternative to existing destination file: \"{}\"", + destination_file.to_string() + ), + }); + } + + Err(ScannerError { + code: ErrorCode::NoFileExtension, + message: destination_file.to_string(), + }) + } + + // Modelled after the lopdf example: https://crates.io/crates/lopdf + // This must be doable by extending the existing document, too! + fn merge_scanned_page(&self, output_path: &Path, page_path: &Path) -> Result<(), ScannerError> { + assert!(output_path.is_file()); + assert!(page_path.is_file()); + + let documents = vec![ + match Document::load(output_path) { + Ok(doc) => doc, Err(err) => return Err(err.into()), - }; + }, + match Document::load(page_path) { + Ok(doc) => doc, + Err(err) => return Err(err.into()), + }, + ]; + + // Define a starting max_id (will be used as start index for object_ids) + let mut max_id = 1; + let mut pagenum = 1; + // Collect all Documents Objects grouped by a map + let mut documents_pages = BTreeMap::new(); + let mut documents_objects = BTreeMap::new(); + let mut document = Document::with_version("1.5"); + + for mut doc in documents { + let mut first = false; + doc.renumber_objects_with(max_id); - if let Err(err) = response.copy_to(&mut file) { - return Err(err.into()); + max_id = doc.max_id + 1; + + documents_pages.extend( + doc.get_pages() + .into_iter() + .map(|(_, object_id)| { + if !first { + let bookmark = Bookmark::new( + String::from(format!("Page_{}", pagenum)), + [0.0, 0.0, 1.0], + 0, + object_id, + ); + document.add_bookmark(bookmark, None); + first = true; + pagenum += 1; + } + + (object_id, doc.get_object(object_id).unwrap().to_owned()) + }) + .collect::>(), + ); + documents_objects.extend(doc.objects); + } + + // Catalog and Pages are mandatory + let mut catalog_object: Option<(ObjectId, Object)> = None; + let mut pages_object: Option<(ObjectId, Object)> = None; + + // Process all objects except "Page" type + for (object_id, object) in documents_objects.iter() { + // We have to ignore "Page" (as are processed later), "Outlines" and "Outline" objects + // All other objects should be collected and inserted into the main Document + match object.type_name().unwrap_or("") { + "Catalog" => { + // Collect a first "Catalog" object and use it for the future "Pages" + catalog_object = Some(( + if let Some((id, _)) = catalog_object { + id + } else { + *object_id + }, + object.clone(), + )); + } + "Pages" => { + // Collect and update a first "Pages" object and use it for the future "Catalog" + // We have also to merge all dictionaries of the old and the new "Pages" object + if let Ok(dictionary) = object.as_dict() { + let mut dictionary = dictionary.clone(); + if let Some((_, ref object)) = pages_object { + if let Ok(old_dictionary) = object.as_dict() { + dictionary.extend(old_dictionary); + } + } + + pages_object = Some(( + if let Some((id, _)) = pages_object { + id + } else { + *object_id + }, + Object::Dictionary(dictionary), + )); + } + } + "Page" => {} // Ignored, processed later and separately + "Outlines" => {} // Ignored, not supported yet + "Outline" => {} // Ignored, not supported yet + _ => { + document.objects.insert(*object_id, object.clone()); + } } + } + + // If no "Pages" object found abort + if pages_object.is_none() { + println!("Pages root not found."); + + return Ok(()); + } + + // Iterate over all "Page" objects and collect into the parent "Pages" created before + for (object_id, object) in documents_pages.iter() { + if let Ok(dictionary) = object.as_dict() { + let mut dictionary = dictionary.clone(); + dictionary.set("Parent", pages_object.as_ref().unwrap().0); + + document + .objects + .insert(*object_id, Object::Dictionary(dictionary)); + } + } + + // If no "Catalog" found abort + if catalog_object.is_none() { + println!("Catalog root not found."); + + return Ok(()); + } + + let catalog_object = catalog_object.unwrap(); + let pages_object = pages_object.unwrap(); + + // Build a new "Pages" with updated fields + if let Ok(dictionary) = pages_object.1.as_dict() { + let mut dictionary = dictionary.clone(); + + // Set new pages count + dictionary.set("Count", documents_pages.len() as u32); + + // Set new "Kids" list (collected from documents pages) for "Pages" + dictionary.set( + "Kids", + documents_pages + .into_iter() + .map(|(object_id, _)| Object::Reference(object_id)) + .collect::>(), + ); + + document + .objects + .insert(pages_object.0, Object::Dictionary(dictionary)); + } + + // Build a new "Catalog" with updated fields + if let Ok(dictionary) = catalog_object.1.as_dict() { + let mut dictionary = dictionary.clone(); + dictionary.set("Pages", pages_object.0); + dictionary.remove(b"Outlines"); // Outlines not supported in merged PDFs + + document + .objects + .insert(catalog_object.0, Object::Dictionary(dictionary)); + } + + document.trailer.set("Root", catalog_object.0); + + // Update the max internal ID as wasn't updated before due to direct objects insertion + document.max_id = document.objects.len() as u32; + + // Reorder all new Document objects + document.renumber_objects(); + + //Set any Bookmarks to the First child if they are not set to a page + document.adjust_zero_pages(); + + //Set all bookmarks to the PDF Object tree then set the Outlines to the Bookmark content map. + if let Some(n) = document.build_outline() { + if let Ok(x) = document.get_object_mut(catalog_object.0) { + if let Object::Dictionary(ref mut dict) = x { + dict.set("Outlines", Object::Reference(n)); + } + } + } - page += 1; + document.compress(); + if let Err(err) = document.save(output_path) { + log::error!("Failed to save merged pdf document: {err:?}"); + return Err(err.into()); } Ok(()) diff --git a/escl-scan/src/scannererror.rs b/escl-scan/src/scannererror.rs index 2e728b6..5bbe44a 100644 --- a/escl-scan/src/scannererror.rs +++ b/escl-scan/src/scannererror.rs @@ -10,7 +10,10 @@ use core::fmt; pub enum ErrorCode { FilesystemError, NetworkError, + NoFileExtension, + NoMorePages, NoScannerFound, + PdfError, ProtocolError, ScannerNotReady, } @@ -26,9 +29,17 @@ impl fmt::Display for ScannerError { let msg = match self.code { ErrorCode::FilesystemError => format!("File System Error: {}", self.message), ErrorCode::NetworkError => format!("Network Error: {}", self.message), + ErrorCode::NoFileExtension => format!( + "Specified output file does not have a file extension: {}", + self.message + ), + ErrorCode::NoMorePages => { + "There are no more scanned pages available for download".to_string() + } ErrorCode::NoScannerFound => { format!("No scanner found where name contains \"{}\"", self.message) } + ErrorCode::PdfError => format!("PDF processing error: {}", self.message), ErrorCode::ProtocolError => format!("eSCL Protocol Error: {}", self.message), ErrorCode::ScannerNotReady => "The scanner is not ready to scan".to_string(), }; @@ -37,6 +48,15 @@ impl fmt::Display for ScannerError { } } +impl From for ScannerError { + fn from(error: lopdf::Error) -> Self { + ScannerError { + code: ErrorCode::PdfError, + message: error.to_string(), + } + } +} + impl From for ScannerError { fn from(error: reqwest::Error) -> Self { ScannerError {