mod render; mod retrieval; #[cfg(test)] mod tests; extern crate pdf; use crate::pdf::object::Resolve; use crate::render::Renderer; use image::DynamicImage; use lazy_static::lazy_static; use pdf::file::{File, FileOptions, NoLog, ObjectCache, StreamCache}; use pdf::object::{Object, ObjectWrite, Page, PageRc, PlainRef, Stream, Updater}; use pdf::primitive::{Dictionary, Primitive}; use pdf::xref::XRef; use regex::Regex; use retrieval::{get_prim_by_path_with_file, get_stream_data_by_path_with_file}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, VecDeque}; use std::ops::Deref; use std::path::Path; use std::sync::{Arc, RwLock}; use tauri::ipc::{InvokeResponseBody, Response}; use tauri::{Manager, State}; use uuid::Uuid; use pdf::backend::Backend; type CosFile = File, ObjectCache, StreamCache, NoLog>; #[derive(Serialize, Debug, Clone)] pub struct XRefTableModel { pub size: usize, pub entries: Vec, } #[derive(Serialize, Debug, Clone)] pub struct XRefEntryModel { pub obj_num: u64, pub gen_num: u64, pub obj_type: String, pub offset: u64, } #[derive(Serialize, Debug, Clone)] pub struct PdfFile { pub id: String, pub name: String, pub path: String, pub page_count: u32, pub xref_entries: usize, pub pages: Vec, } #[derive(Serialize, Debug, Clone)] pub struct PrimitiveModel { pub key: String, pub ptype: String, pub sub_type: String, pub value: String, pub children: Vec, pub trace: Vec, pub expanded: bool, } #[derive(Serialize, Debug, Clone)] pub struct PrimitiveTreeView { pub depth: usize, pub key: String, pub ptype: String, pub sub_type: String, pub value: String, pub container: bool, pub expanded: bool, pub path: Vec, pub active: bool, } #[derive(Serialize, Debug, Clone)] pub struct PathTrace { pub key: String, #[serde(skip_serializing)] pub last_ref: Option, pub last_jump: String, } impl PathTrace { fn new(key: String, last_ref: Option) -> PathTrace { PathTrace { key, last_jump: last_ref .map(|r| r.id.to_string()) .unwrap_or("Trailer".to_string()), last_ref: last_ref, } } fn trailer() -> PathTrace { PathTrace { key: "Trailer".to_string(), last_ref: None, last_jump: "Trailer".to_string(), } } } #[derive(Serialize, Debug, Clone)] pub struct PageModel { key: String, obj_num: u64, page_num: u64, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct TreeViewRequest { key: String, children: Vec, expand: bool, } impl TreeViewRequest { fn step(&self) -> Result { Step::parse_step(&self.key) } } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct ContentsModel { parts: Vec>, } #[tauri::command] fn get_all_files(session: State) -> Vec { session .get_all_files() .iter() .map(|s| s.read().unwrap().pdf_file.clone()) .collect() } #[tauri::command] fn get_changes(id: &str, session: State) -> Result, String> { let lock = session.get_file(id)?; let file = lock.read().unwrap(); Ok(file.cos_file.changes().iter() .map(|(obj, (prim, gen))| PrimitiveModel::from_primitive_until_refs(prim, PathTrace::new(obj.to_string(), Some(PlainRef{id: *obj, gen: *gen})))) .collect()) } #[tauri::command] fn clear_changes(id: &str, session: State) -> Result<(), String> { let lock = session.get_file(id)?; let mut file = lock.write().unwrap(); file.cos_file.clear_changes(); Ok(()) } #[tauri::command] fn get_all_file_ids(session: State) -> Vec { session.get_all_file_ids() } #[tauri::command] fn close_file(id: &str, session: State) { session.handle_close(id); } #[tauri::command] fn get_file_by_id(id: &str, session: State) -> Result { let lock = session.get_file(id)?; let file = lock.read().unwrap(); Ok(file.pdf_file.clone()) } #[tauri::command] fn upload(path: &str, session: State) -> Result { let file = t!(FileOptions::cached().open(path)); let pdf_file = to_pdf_file(path, &file)?; let id = pdf_file.id.clone(); session.handle_upload(pdf_file, file)?; Ok(id) } fn to_pdf_file(path: &str, file: &CosFile) -> Result { fn parse_title_from_path(path: &str) -> Option { Path::new(path) .file_name() .and_then(|f| f.to_str().map(|s| s.to_string())) } let file_name = if let Some(ref info) = file.trailer.info_dict { info.title .as_ref() .map(|p| p.to_string_lossy()) .unwrap_or(parse_title_from_path(path).unwrap_or_else(|| "Not found".to_string())) } else { "Not found".to_string() }; let pages = file .pages() .enumerate() .map(|(i, page_ref)| PageModel { key: format!("Page {}", i + 1), obj_num: page_ref.unwrap().get_ref().get_inner().id, page_num: (i + 1) as u64, }) .collect(); let pdf_file = PdfFile { id: Uuid::new_v4().to_string(), name: file_name.to_string().into(), path: path.to_string().into(), page_count: file.num_pages(), xref_entries: file.get_xref().len(), pages: pages, }; Ok(pdf_file) } fn read_contents(page: &Page, resolver: &impl Resolve) -> Result { if let Some(ref contents) = page.contents { let mut parts = vec![]; for part in &contents.parts { let data = &t!(part.data(resolver)); let string = String::from_utf8_lossy(data); let part = string .split("\n") .map(|s| s.to_string()) .collect::>(); parts.push(part); } return Ok(ContentsModel { parts }); } Err(String::from("Error occurred")) } #[tauri::command] async fn get_contents(id: &str, path: &str, session: State<'_, Session>) -> Result { let lock = session.get_file(id)?; let file = lock.read().unwrap(); let (_, page_prim, _) = get_prim_by_path_with_file(path, &file.cos_file)?; let resolver = file.cos_file.resolver(); let page = t!(pdf::object::Page::from_primitive(page_prim, &resolver)); read_contents(&page, &resolver) } #[tauri::command] async fn update_contents( id: &str, path: &str, session: State<'_, Session>, contents: ContentsModel, ) -> Result<(), String> { let lock = session.get_file(id)?; let mut file = lock.write().unwrap(); update_contents_with_file(path, contents, &mut file.cos_file) } fn update_contents_with_file(path: &str, contents: ContentsModel, file: &mut CosFile) -> Result<(), String> { let mut parts = vec![]; for part in contents.parts { let stream_ref = Stream::new((), part.join("\n").as_bytes()); parts.push(stream_ref); } let new_contents = pdf::content::Content{parts}; if let Some(Step::Page(num)) = Step::parse(path).pop_front() { let page_rc: PageRc = t!(file.get_page(num - 1)); let page_ref = page_rc.get_ref().get_inner().clone(); let mut page = page_rc.deref().clone(); page.contents = Some(new_contents); let _ = t!(file.update(page_ref, page)); return Ok(()); } Err(String::from("Path does not lead to a page!")) } #[tauri::command] async fn get_stream_data_as_string( id: &str, path: &str, session: State<'_, Session>, ) -> Result { let lock = session.get_file(id)?; let file = lock.read().unwrap(); let data = get_stream_data_by_path_with_file(path, &file.cos_file)?; Ok(String::from_utf8_lossy(&data).into_owned()) } #[tauri::command] async fn update_stream_data_as_string( id: &str, path: &str, new_data: &str, session: State<'_, Session>, ) -> Result<(), String> { let lock = session.get_file(id)?; let mut file = lock.write().unwrap(); let (_, _, trace) = get_prim_by_path_with_file(path, &file.cos_file)?; let i = trace.len() - 1; let plain_ref = trace[i].last_ref.expect("No trace to update"); let stream = Stream::new((), new_data.as_bytes()); let _ = t!(file.cos_file.update(plain_ref, stream)); Ok(()) } fn format_img_as_response(img: DynamicImage) -> Result { use std::mem; let w: u32 = img.width(); let h: u32 = img.height(); let mut data: Vec = img.into_rgba8().into_raw(); let w_bytes = w.to_le_bytes(); // or to_be_bytes() depending on endianness let h_bytes = h.to_le_bytes(); let old_capacity = data.capacity(); let ptr = data.as_mut_ptr(); let len = data.len(); mem::forget(data); // Create a new Vec with the two u32s in front let merged = unsafe { let mut new_vec = Vec::from_raw_parts(ptr, len, old_capacity); // Insert u32 bytes at the front new_vec.splice(0..0, w_bytes.iter().copied()); new_vec.splice(4..4, h_bytes.iter().copied()); new_vec }; Ok(Response::new(InvokeResponseBody::from(merged))) } #[tauri::command] async fn get_stream_data_as_image( id: &str, path: &str, session: State<'_, Session>, ) -> Result { let lock = session.get_file(id)?; let file = lock.read().unwrap(); let img = retrieval::get_image_by_path(path, &file.cos_file)?; format_img_as_response(img) } #[tauri::command] async fn get_page_by_num( id: &str, num: u32, session: State<'_, Session>, ) -> Result { render_page_by_path(id, &format!("Page{}", num), session).await } #[tauri::command] async fn render_page_by_path( id: &str, path: &str, session: State<'_, Session>, ) -> Result { let lock = session.get_file(id)?; let file = lock.read().unwrap(); let page = get_page_by_path(path, &file.cos_file)?; let mut renderer = Renderer::new(&file.cos_file, 150); let img = renderer.render(&page)?; format_img_as_response(img) } fn get_page_by_path(path: &str, file: &CosFile) -> Result { let resolver = file.resolver(); let (_, page_prim, _) = get_prim_by_path_with_file(path, file)?; let page = t!(pdf::object::Page::from_primitive(page_prim, &resolver)); Ok(page) } #[tauri::command] fn get_prim_by_path( id: &str, path: &str, session: State, ) -> Result { let lock = session.get_file(id)?; let file = lock.read().unwrap(); get_prim_model_by_path_with_file(path, &file.cos_file) } fn get_prim_model_by_path_with_file(path: &str, file: &CosFile) -> Result { let steps = Step::parse(path); if steps.len() == 0 { return Err(String::from(format!("{:?} is not a valid path!", steps))); } if steps.back().unwrap() == &Step::Data { return handle_data_step(steps, file); } let (_, prim, trace) = retrieval::get_prim_by_steps_with_file(steps, file)?; Ok(PrimitiveModel::from_primitive_with_children(&prim, trace)) } fn handle_data_step(mut steps: VecDeque, file: &CosFile) -> Result { let _data = steps.pop_back(); let (_, prim, trace) = retrieval::get_prim_by_steps_with_file(steps, file)?; if let Primitive::Stream(stream) = prim { let sub_type = match stream.info.get("Subtype") { Some(element) => match element { Primitive::Name(name) => name.as_str().to_string(), _ => String::from("-"), }, _ => String::from("-"), }; return Ok(PrimitiveModel::as_data(sub_type, trace)); } Err(String::from("Parent of Data is not a stream")) } #[tauri::command] fn get_prim_tree_by_path( id: &str, paths: Vec, session: State, ) -> Result, String> { let lock = session.get_file(id)?; let file = lock.read().unwrap(); let results = paths .into_iter() .map(|path| get_prim_tree_by_path_with_file(path, &file.cos_file)) .collect::, String>>()? .into_iter() .flatten() .collect(); Ok(results) } fn get_prim_tree_by_path_with_file( node: TreeViewRequest, file: &CosFile, ) -> Result, String> { let step = node.step()?; let (parent, trace) = retrieval::resolve_parent(step.clone(), file)?; let trace = vec![trace]; let mut parent_model: PrimitiveModel; if node.expand { parent_model = PrimitiveModel::from_primitive_with_children(&parent, trace); for child in node.children.iter() { expand(child, &mut parent_model, &parent, file)?; } } else { parent_model = PrimitiveModel::from_primitive(step.get_key(), &parent, trace); } Ok(PrimitiveTreeView::flatten(0, parent_model)) } fn expand( node: &TreeViewRequest, parent_model: &mut PrimitiveModel, parent: &Primitive, file: &CosFile, ) -> Result<(), String> { if !node.expand { return Ok(()); } let step = node.step()?; if let Ok(prim) = retrieval::resolve_step(parent, &step) { if let Primitive::Reference(x_ref) = prim { let jump = retrieval::resolve_xref(x_ref.id, file)?; let mut jump_trace = parent_model.trace.clone(); jump_trace.push(PathTrace::new(step.get_key(), Some(x_ref.clone()))); let mut to_expand = parent_model.get_child(step.get_key()).unwrap(); to_expand.add_children(&jump, &jump_trace); expand_children(node, file, &jump, &mut to_expand)?; } else { let mut to_expand = parent_model.get_child(step.get_key()).unwrap(); to_expand.add_children(prim, &to_expand.trace.clone()); expand_children(node, file, prim, &mut to_expand)?; } } Ok(()) } fn expand_children( node: &TreeViewRequest, file: &CosFile, prim: &Primitive, mut expanded: &mut PrimitiveModel, ) -> Result<(), String> { for child in node.children.iter() { expand(child, &mut expanded, prim, file)?; } Ok(()) } #[derive(Debug, PartialEq, Clone)] pub enum Step { String(String), Page(u32), Number(u64), Trailer, Data, } impl Step { fn parse_step(path: &str) -> Result { lazy_static! { static ref PAGE_RE: Regex = Regex::new(r"^Page(\d+)$").unwrap(); } if path.len() == 0 { return Err(String::from("Path is empty")); } Ok(match &path.parse::().ok() { Some(i) => Step::Number(*i), None => match &path[..] { "Data" => Step::Data, "Trailer" => Step::Trailer, "/" => Step::Trailer, _ => { if let Some(caps) = PAGE_RE.captures(path) { Step::Page( caps[1] .parse::() .map_err(|_| format!("Invalid page number in {}", path))?, ) } else { Step::String(path.to_string()) } } }, }) } fn parse(path: &str) -> VecDeque { let mut steps = VecDeque::new(); if path.starts_with("/") { steps.push_back(Step::Trailer); } let split_path = path.split("/").collect::>(); split_path .iter() .filter_map(|s| Step::parse_step(s).ok()) .collect::>() } fn get_key(&self) -> String { match self { Step::String(s) => s.clone(), Step::Number(i) => i.to_string(), Step::Trailer => "Trailer".to_string(), Step::Page(n) => format!("Page{}", n), Step::Data => "Data".into(), } } } fn append_path(key: String, path: &Vec) -> Vec { let mut new_path = path.clone(); let last_jump = new_path.last().unwrap().last_ref.clone(); new_path.push(PathTrace::new(key, last_jump)); new_path } impl PrimitiveModel { fn from_primitive(key: String, primitive: &Primitive, path: Vec) -> PrimitiveModel { let value: String = match primitive { Primitive::Null => "Null".to_string(), Primitive::Integer(i) => i.to_string(), Primitive::Number(f) => f.to_string(), Primitive::Boolean(b) => b.to_string(), Primitive::String(s) => s.to_string_lossy(), Primitive::Stream(_) => "-".to_string(), Primitive::Dictionary(dict) => PrimitiveModel::format_dict_content(dict), Primitive::Array(arr) => PrimitiveModel::format_arr_content(arr), Primitive::Reference(pref) => { format!("Obj Nr: {} Gen Nr: {}", pref.id, pref.gen) } Primitive::Name(name) => name.clone().as_str().to_string(), }; let sub_type: String = match primitive { Primitive::Dictionary(d) => d .get("Type") .and_then(|value| match value { Primitive::Name(name) => Some(name.clone().as_str().to_string()), _ => None, }) .unwrap_or(String::from("-")), _ => String::from("-"), }; PrimitiveModel { key: key, ptype: primitive.get_debug_name().into(), sub_type: sub_type, value: value, children: Vec::new(), trace: path, expanded: false, } } fn as_data(sub_type: String, trace: Vec) -> PrimitiveModel { PrimitiveModel { key: "Data".to_string(), ptype: "Stream Data".to_string(), sub_type: sub_type, value: "".to_string(), children: Vec::new(), trace: append_path("Data".to_string(), &trace), expanded: false, } } fn format_dict_content(dict: &Dictionary) -> String { let mut result = String::from("{"); let mut count = 0; for (key, value) in dict.iter() { result.push_str(&format!( "{}: {}", key, PrimitiveModel::format_prim_short(value) )); count += 1; if count < 4 { result.push_str(", "); } else { result.push_str(",..."); break; } } result.push_str("}"); result } fn format_arr_content(arr: &Vec) -> String { if arr.len() == 0 { return "[]".to_string(); } let mut result = String::from("["); let contents = if arr.len() > 4 { &arr[0..4] } else { &arr[..] }; for i in 0..contents.len() { let prim = contents.get(i).unwrap(); result.push_str(&PrimitiveModel::format_prim_short(prim)); if i != contents.len() - 1 { result.push_str(", "); } } if arr.len() > 4 { result.push_str(",..."); } result.push_str("]"); result } fn format_prim_short(prim: &Primitive) -> String { match prim { Primitive::Integer(i) => format!("{}", i), Primitive::Number(n) => format!("{}", n), Primitive::Boolean(b) => format!("{}", b), Primitive::String(s) => s.to_string().unwrap_or(String::from("-")), Primitive::Name(n) => n.as_str().to_string(), _ => prim.get_debug_name().to_string(), } } fn from_primitive_with_children( primitive: &Primitive, trace: Vec, ) -> PrimitiveModel { let mut model = PrimitiveModel::from_primitive( trace.last().unwrap().key.clone(), primitive, trace.clone(), ); model.add_children(primitive, &trace); model } fn from_primitive_until_refs(primitive: &Primitive, trace: PathTrace) -> PrimitiveModel { let mut parent_model = PrimitiveModel::from_primitive( trace.key.clone(), primitive, vec![trace.clone()], ); let mut model = parent_model; model.add_children(primitive, &vec!(trace)); model } fn add_children(&mut self, primitive: &Primitive, path: &Vec) { self.expanded = true; match primitive { Primitive::Dictionary(dict) => dict.iter().for_each(|(name, value)| { self.add_child( name.clone().as_str().to_string(), value, append_path(name.clone().as_str().to_string(), &path), ); }), Primitive::Array(arr) => arr.iter().enumerate().for_each(|(i, obj)| { self.add_child(i.to_string(), obj, append_path(i.to_string(), &path)); }), Primitive::Stream(stream) => { stream.info.iter().for_each(|(name, value)| { self.add_child( name.clone().as_str().to_string(), value, append_path(name.clone().as_str().to_string(), &path), ); }); self.children.push(PrimitiveModel { key: "Data".to_string(), ptype: "Stream Data".to_string(), sub_type: "-".to_string(), value: "".to_string(), children: vec![], trace: append_path("Data".to_string(), &path), expanded: false, }); } _ => (), }; } fn add_child( &mut self, key: String, child: &Primitive, path: Vec, ) -> &PrimitiveModel { let child_model = Self::from_primitive(key, child, path); self.children.push(child_model); &self.children[self.children.len() - 1] } fn get_child(&mut self, key: String) -> Option<&mut PrimitiveModel> { self.children.iter_mut().find(|child| child.key == key) } fn is_container(&self) -> bool { self.ptype == "Dictionary" || self.ptype == "Array" || self.ptype == "Stream" || self.ptype == "Reference" } fn drain_children(&mut self) -> Vec { self.children.drain(..).collect() } } impl PrimitiveTreeView { fn from_primitive(depth: usize, primitive: PrimitiveModel) -> PrimitiveTreeView { let is_container = primitive.is_container(); PrimitiveTreeView { depth: depth, key: primitive.key, ptype: primitive.ptype, sub_type: primitive.sub_type, value: primitive.value, container: is_container, expanded: primitive.expanded, path: primitive.trace, active: true, } } fn flatten(depth: usize, mut primitive: PrimitiveModel) -> Vec { let mut views: Vec = Vec::new(); let children = primitive.drain_children(); views.push(PrimitiveTreeView::from_primitive(depth, primitive)); children.into_iter().for_each(|child| { views.extend(PrimitiveTreeView::flatten(depth + 1, child.clone())); }); views } } #[tauri::command] fn get_xref_table(id: &str, session: State) -> Result { let lock = session.get_file(id)?; let file = lock.read().unwrap(); get_xref_table_model_with_file(&file.cos_file) } fn get_xref_table_model_with_file(file: &CosFile) -> Result { let resolver = file.resolver(); let x_ref_table = file.get_xref(); let mut models: Vec = Vec::new(); for (i, x_ref) in x_ref_table.iter_real().enumerate() { models.push(match x_ref { XRef::Raw { pos, gen_nr } => { let prim: Primitive = resolver .resolve(PlainRef { id: i as u64, gen: *gen_nr, }) .unwrap(); XRefEntryModel { obj_num: i as u64, gen_num: *gen_nr, obj_type: prim.get_debug_name().to_string().into(), offset: *pos as u64, } } XRef::Stream { stream_id, index } => XRefEntryModel { obj_num: i as u64, gen_num: 0, obj_type: "Stream".into(), offset: *index as u64, }, XRef::Free { next_obj_nr, gen_nr, } => XRefEntryModel { obj_num: i as u64, gen_num: *gen_nr as u64, obj_type: "Free".into(), offset: *next_obj_nr as u64, }, XRef::Promised => XRefEntryModel { obj_num: i as u64, gen_num: 0, obj_type: "Promised".into(), offset: 0, }, XRef::Invalid => XRefEntryModel { obj_num: i as u64, gen_num: 0, obj_type: "Invalid".into(), offset: 0, }, }); } Ok(XRefTableModel { size: x_ref_table.len(), entries: models, }) } struct Session { files: RwLock>>>, } struct SessionFile { pdf_file: PdfFile, cos_file: CosFile, } impl Session { fn load() -> Session { Session { files: RwLock::new(HashMap::new()), } } fn get_file(&self, id: &str) -> Result>, String> { let lock = self.files.read().unwrap(); lock.get(id) .cloned() .ok_or(format!(" File {} not found!", id)) } fn get_all_files(&self) -> Vec>> { self.files.read().unwrap().values().cloned().collect() } fn get_all_file_ids(&self) -> Vec { self.files .read() .unwrap() .keys() .map(|f| f.clone()) .collect() } fn handle_upload(&self, pdf_file: PdfFile, cos_file: CosFile) -> Result<(), String> { if let Ok(mut files) = self.files.write() { return match files.insert( pdf_file.id.clone(), Arc::new(RwLock::new(SessionFile { pdf_file: pdf_file, cos_file: cos_file, })), ) { Some(_) => Err("File could not be uploaded!".to_string()), None => Ok(()), }; }; Err("Lock could not be acquired!".to_string()) } fn handle_close(&self, id: &str) { self.files.write().unwrap().remove(id); } } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_opener::init()) .setup(|app| { app.manage(Session::load()); Ok(()) }) .invoke_handler(tauri::generate_handler![ upload, get_all_files, get_all_file_ids, get_file_by_id, close_file, get_prim_by_path, get_prim_tree_by_path, get_xref_table, get_contents, update_contents, get_stream_data_as_string, update_stream_data_as_string, get_stream_data_as_image, get_page_by_num, get_changes, clear_changes ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }