Kilian Schuettler 4b20d20783 Moved tab bar to dropdown in TitleBar.svelte
Made image rendering dynamic
2025-02-11 13:14:33 +01:00

789 lines
23 KiB
Rust

mod render;
mod retrieval;
#[cfg(test)]
mod tests;
extern crate pdf;
use crate::pdf::object::Resolve;
use crate::render::Renderer;
use base64;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use image::{ImageFormat, RgbImage};
use lazy_static::lazy_static;
use pdf::file::{File, FileOptions, NoLog, ObjectCache, StreamCache};
use pdf::object::{Object, PlainRef};
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::io::Cursor;
use std::ops::DerefMut;
use std::path::Path;
use std::sync::{Arc, RwLock};
use tauri::{Manager, State};
use uuid::Uuid;
type CosFile = File<Vec<u8>, ObjectCache, StreamCache, NoLog>;
#[derive(Serialize, Debug, Clone)]
pub struct XRefTableModel {
pub size: usize,
pub entries: Vec<XRefEntryModel>,
}
#[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<PageModel>,
}
#[derive(Serialize, Debug, Clone)]
pub struct PrimitiveModel {
pub key: String,
pub ptype: String,
pub sub_type: String,
pub value: String,
pub children: Vec<PrimitiveModel>,
pub trace: Vec<PathTrace>,
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<PathTrace>,
pub active: bool,
}
#[derive(Serialize, Debug, Clone)]
pub struct PathTrace {
pub key: String,
pub last_jump: String,
}
impl PathTrace {
fn new(key: String, last_jump: String) -> PathTrace {
PathTrace { key, last_jump }
}
}
#[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<TreeViewRequest>,
expand: bool,
}
impl TreeViewRequest {
fn step(&self) -> Result<Step, String> {
Step::parse_step(&self.key)
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ContentsModel {
parts: Vec<Vec<String>>,
}
#[tauri::command]
fn get_all_files(session: State<Session>) -> Vec<PdfFile> {
session
.get_all_files()
.iter()
.map(|s| s.pdf_file.clone())
.collect()
}
#[tauri::command]
fn get_all_file_ids(session: State<Session>) -> Vec<String> {
session.get_all_file_ids()
}
#[tauri::command]
fn close_file(id: &str, session: State<Session>) {
session.handle_close(id);
}
#[tauri::command]
fn get_file_by_id(id: &str, session: State<Session>) -> Result<PdfFile, String> {
session.get_file(id).map(|file| file.pdf_file.clone())
}
#[tauri::command]
fn upload(path: &str, session: State<Session>) -> Result<String, String> {
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<PdfFile, String> {
fn parse_title_from_path(path: &str) -> Option<String> {
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)
}
#[tauri::command]
fn get_contents(id: &str, path: &str, session: State<Session>) -> Result<ContentsModel, String> {
let file = session.get_file(id)?;
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));
if let Some(contents) = page.contents {
let mut parts = vec![];
for part in contents.parts {
let data = &t!(part.data(&resolver));
let ops = t!(pdf::content::parse_ops(&data, &resolver));
let part = t!(pdf::content::display_ops(&ops));
parts.push(part);
}
return Ok(ContentsModel { parts });
}
Err(String::from("Error occurred"))
}
#[tauri::command]
fn get_stream_data_as_string(
id: &str,
path: &str,
session: State<Session>,
) -> Result<String, String> {
let file = session.get_file(id)?;
let data = get_stream_data_by_path_with_file(path, &file.cos_file)?;
Ok(String::from_utf8_lossy(&data).into_owned())
}
fn encode_b64(img: RgbImage) -> Result<String, String> {
let mut writer = Cursor::new(Vec::new());
match img.write_to(&mut writer, ImageFormat::Jpeg) {
Ok(_) => Ok(format!(
"data:image/{};base64,{}",
"jpeg",
BASE64_STANDARD.encode(writer.into_inner())
)),
Err(e) => Err(e.to_string()),
}
}
#[tauri::command]
async fn get_stream_data_as_image(id: &str, path: &str, session: State<'_, Session>,) -> Result<String, String> {
use base64::prelude::*;
let file = session.get_file(id)?;
let img = retrieval::get_image_by_path(path, &file.cos_file)?;
encode_b64(img.into_rgb8())
}
#[tauri::command]
async fn get_page_by_num(id: &str, num: u32, session: State<'_, Session>) -> Result<String, String> {
let file = session.get_file(id)?;
let mut renderer = Renderer::new(&file.cos_file, 150);
let img = renderer.render(num)?;
encode_b64(img.into_rgb8())
}
#[tauri::command]
fn get_prim_by_path(
id: &str,
path: &str,
session: State<Session>,
) -> Result<PrimitiveModel, String> {
let file = session.get_file(id)?;
get_prim_model_by_path_with_file(path, &file.cos_file)
}
fn get_prim_model_by_path_with_file(path: &str, file: &CosFile) -> Result<PrimitiveModel, String> {
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<Step>, file: &CosFile) -> Result<PrimitiveModel, String> {
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<TreeViewRequest>,
session: State<Session>,
) -> Result<Vec<PrimitiveTreeView>, String> {
let file = session.get_file(id)?;
let results = paths
.into_iter()
.map(|path| get_prim_tree_by_path_with_file(path, &file.cos_file))
.collect::<Result<Vec<_>, String>>()?
.into_iter()
.flatten()
.collect();
Ok(results)
}
fn get_prim_tree_by_path_with_file(
node: TreeViewRequest,
file: &CosFile,
) -> Result<Vec<PrimitiveTreeView>, 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()?;
let 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(), x_ref.id.to_string()));
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<Step, String> {
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::<u64>().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::<u32>()
.map_err(|_| format!("Invalid page number in {}", path))?,
)
} else {
Step::String(path.to_string())
}
}
},
})
}
fn parse(path: &str) -> VecDeque<Step> {
let mut steps = VecDeque::new();
if path.starts_with("/") {
steps.push_back(Step::Trailer);
}
let split_path = path.split("/").collect::<VecDeque<&str>>();
split_path
.iter()
.filter_map(|s| Step::parse_step(s).ok())
.collect::<VecDeque<Step>>()
}
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<PathTrace>) -> Vec<PathTrace> {
let mut new_path = path.clone();
let last_jump = new_path.last().unwrap().last_jump.clone();
new_path.push(PathTrace::new(key, last_jump));
new_path
}
impl PrimitiveModel {
fn from_primitive(key: String, primitive: &Primitive, path: Vec<PathTrace>) -> 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<PathTrace>) -> 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<Primitive>) -> 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<PathTrace>,
) -> PrimitiveModel {
let mut model = PrimitiveModel::from_primitive(
trace.last().unwrap().key.clone(),
primitive,
trace.clone(),
);
model.add_children(primitive, &trace);
model
}
fn add_children(&mut self, primitive: &Primitive, path: &Vec<PathTrace>) {
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<PathTrace>,
) -> &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<PrimitiveModel> {
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<PrimitiveTreeView> {
let mut views: Vec<PrimitiveTreeView> = 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<Session>) -> Result<XRefTableModel, String> {
let file = session.get_file(id)?;
get_xref_table_model_with_file(&file.cos_file)
}
fn get_xref_table_model_with_file(file: &CosFile) -> Result<XRefTableModel, String> {
let resolver = file.resolver();
let x_ref_table = file.get_xref();
let mut models: Vec<XRefEntryModel> = 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<HashMap<String, Arc<SessionFile>>>,
}
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<Arc<SessionFile>, String> {
let lock = self.files.read().unwrap();
lock.get(id)
.cloned()
.ok_or(format!(" File {} not found!", id))
}
fn get_all_files(&self) -> Vec<Arc<SessionFile>> {
self.files
.read()
.unwrap()
.values()
.map(|f| f.clone())
.collect()
}
fn get_all_file_ids(&self) -> Vec<String> {
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(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,
get_stream_data_as_string,
get_stream_data_as_image,
get_page_by_num
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}