From d15aa2a4edc1431f7c3d58de3c46fdabbad14b66 Mon Sep 17 00:00:00 2001 From: Kilian Schuettler Date: Mon, 10 Feb 2025 12:13:27 +0100 Subject: [PATCH] Added image rendering and notifications --- src-tauri/src/retrieval.rs | 200 ++++++++++++++++++++++++++++++++++++ src/models/TreeViewModel.ts | 15 +++ 2 files changed, 215 insertions(+) create mode 100644 src-tauri/src/retrieval.rs create mode 100644 src/models/TreeViewModel.ts diff --git a/src-tauri/src/retrieval.rs b/src-tauri/src/retrieval.rs new file mode 100644 index 0000000..f99b209 --- /dev/null +++ b/src-tauri/src/retrieval.rs @@ -0,0 +1,200 @@ +use std::{ + collections::VecDeque, + ffi::{OsStr, OsString}, + io::Cursor, + sync::Arc, +}; + +use image::{DynamicImage, GrayImage, RgbImage}; +use pdf::{ + enc::StreamFilter, + file::FileOptions, + object::{ColorSpace, ImageXObject, ObjectWrite, PlainRef, Resolve, Stream}, + primitive::{PdfStream, Primitive}, +}; + +use crate::{CosFile, PathTrace, Step}; + +#[macro_export] +macro_rules! t { + ($result:expr) => {{ + match $result { + Ok(f) => f, + Err(e) => return Err(e.to_string()), + } + }}; +} + +pub fn get_prim_by_steps_with_file( + mut steps: VecDeque, + file: &CosFile, +) -> Result<(Step, Primitive, Vec), String> { + if steps.len() == 0 { + return Err(String::from(format!("{:?} is not a valid path!", steps))); + } + let step = steps.pop_front().unwrap(); + let (mut parent, trace) = resolve_parent(step.clone(), file)?; + + let mut last_jump = trace.last_jump.clone(); + let mut trace = vec![trace]; + + let mut current_prim = &parent; + while !steps.is_empty() { + let step = steps.pop_front().unwrap(); + + current_prim = resolve_step(¤t_prim, &step)?; + if let Primitive::Reference(xref) = current_prim { + last_jump = xref.id.to_string(); + parent = resolve_p_ref(xref.clone(), file)?; + current_prim = &parent; + } + trace.push(PathTrace::new(step.get_key(), last_jump.clone())); + } + Ok((step, current_prim.clone(), trace)) +} + +pub fn resolve_parent(step: Step, file: &CosFile) -> Result<(Primitive, PathTrace), String> { + let parent = match step { + Step::Page(page_num) => return retrieve_page(page_num, file), + Step::Number(obj_num) => resolve_xref(obj_num, file)?, + Step::Trailer => retrieve_trailer(file), + _ => return Err(String::from(format!("{:?} is not a valid path!", step))), + }; + Ok((parent, PathTrace::new(step.get_key(), step.get_key()))) +} + +pub fn resolve_step<'a>(current_prim: &'a Primitive, step: &Step) -> Result<&'a Primitive, String> { + Ok(match step { + Step::Number(index) => match current_prim { + Primitive::Array(prim_array) => { + let i = index.clone() as usize; + if prim_array.len() <= i { + return Err(String::from(format!( + "{} index out of bounds!", + step.get_key() + ))); + } + &prim_array[i] + } + p => { + return Err(String::from(format!( + "{} is not indexed with numbers!", + p.get_debug_name() + ))) + } + }, + Step::String(key) => match current_prim { + Primitive::Dictionary(dict) => match dict.get(key) { + Some(prim) => prim, + None => { + return Err(String::from(format!( + "Key {} does not exist in Dictionary!", + key + ))) + } + }, + Primitive::Stream(stream) => match stream.info.get(key) { + Some(prim) => prim, + None => { + return Err(String::from(format!( + "Key {} does not exist in Info Dictionary!", + key + ))) + } + }, + p => { + return Err(String::from(format!( + "{} has no String paths!", + p.get_debug_name() + ))) + } + }, + _ => return Err(format!("Invalid Step: {}", step.get_key())), + }) +} + +pub fn retrieve_trailer(file: &CosFile) -> Primitive { + let mut updater = FileOptions::uncached().storage(); + file.trailer.to_primitive(&mut updater).unwrap() +} + +pub fn retrieve_page(page_num: u32, file: &CosFile) -> Result<(Primitive, PathTrace), String> { + if page_num <= 0 { + return Err("Page 0 does not exist, use 1-based index!".to_string()); + } + let page_rc = t!(file.get_page(page_num - 1)); + let p_ref = page_rc.get_ref().get_inner(); + Ok(( + resolve_p_ref(p_ref, file)?, + PathTrace::new(format!("Page{}", page_num), p_ref.id.to_string()), + )) +} + +pub fn resolve_xref(id: u64, file: &CosFile) -> Result { + let plain_ref = PlainRef { id, gen: 0 }; + resolve_p_ref(plain_ref, file) +} + +pub fn resolve_p_ref(plain_ref: PlainRef, file: &CosFile) -> Result { + file.resolver() + .resolve(plain_ref) + .map_err(|e| e.to_string()) +} + +pub fn get_prim_by_path_with_file( + path: &str, + file: &CosFile, +) -> Result<(Step, Primitive, Vec), String> { + get_prim_by_steps_with_file(Step::parse(path), file) +} + +pub fn get_image_by_path(path: &str, file: &CosFile) -> Result { + let stream = get_pdf_stream_by_path_with_file(path, file)?; + let resolver = file.resolver(); + let img = t!(ImageXObject::from_stream(stream, &resolver)); + let data = t!(img.image_data(&resolver)); + let color_space = img.color_space.as_ref(); + + match color_space { + Some(ColorSpace::DeviceRGB) | Some(ColorSpace::CalRGB(_)) => { + RgbImage::from_raw(img.width, img.height, data.to_vec()).map(DynamicImage::ImageRgb8) + } + Some(ColorSpace::DeviceGray) | Some(ColorSpace::CalGray(_)) => { + GrayImage::from_raw(img.width, img.height, data.to_vec()).map(DynamicImage::ImageLuma8) + } + _ => return parse_image_lossy(data), + } + .ok_or_else(|| "Failed to decode image".to_string()) +} + +fn parse_image_lossy(data: Arc<[u8]>) -> Result { + let img = image::ImageReader::new(Cursor::new(data)); + match img.decode() { + Ok(img) => Ok(img), + Err(e) => Err(format!("Failed to decode image without color space: {}", e)), + } +} + +pub fn get_pdf_stream_by_path_with_file(path: &str, file: &CosFile) -> Result { + let mut steps = Step::parse(path); + if steps + .pop_back() + .filter(|last| *last == Step::Data) + .is_none() + { + return Err(format!("Path {} does not end with Data", path)); + } + let (_, prim, _) = get_prim_by_steps_with_file(steps, file)?; + + let Primitive::Stream(stream) = prim else { + return Err(format!("Path {} does not point to a stream", path)); + }; + Ok(stream) +} + +pub fn get_stream_data_by_path_with_file(path: &str, file: &CosFile) -> Result, String> { + let stream = get_pdf_stream_by_path_with_file(path, file)?; + let data = + t!(t!(Stream::::from_stream(stream, &file.resolver())).data(&file.resolver())); + Ok(data.to_vec()) +} diff --git a/src/models/TreeViewModel.ts b/src/models/TreeViewModel.ts new file mode 100644 index 0000000..9e80e52 --- /dev/null +++ b/src/models/TreeViewModel.ts @@ -0,0 +1,15 @@ +import type { Trace } from "./Primitive.svelte"; + +export class TreeViewModel { + constructor( + public depth: number, + public key: string, + public ptype: string, + public sub_type: string, + public value: string, + public container: boolean, + public expanded: boolean, + public path: Trace[], + public active: boolean, + ) { } +}