Compare commits

..

No commits in common. "7e71cb947bd5b515e64f5aa3e217a76124568a61" and "30a0f1beb7fde5f9cac156e9cb57df2dbaa60fca" have entirely different histories.

44 changed files with 1201 additions and 1745 deletions

View File

@ -14,18 +14,15 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@geoffcox/svelte-splitter": "^1.0.1", "@geoffcox/svelte-splitter": "^1.0.1",
"@monaco-editor/loader": "^1.4.0",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2", "@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-fs": "~2", "@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"flowbite-svelte": "^0.47.4", "flowbite-svelte": "^0.47.4",
"flowbite-svelte-icons": "^2.0.2", "flowbite-svelte-icons": "^2.0.2",
"monaco-editor": "^0.52.2",
"paths": "^0.1.1", "paths": "^0.1.1",
"svelte-split-pane": "^0.1.2", "svelte-split-pane": "^0.1.2",
"svelte-splitpanes": "^8.0.9", "svelte-splitpanes": "^8.0.9"
"vite-plugin-monaco-editor": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-static": "^3.0.6", "@sveltejs/adapter-static": "^3.0.6",
@ -41,6 +38,6 @@
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"vite": "^6.0.11" "vite": "^6.0.3"
} }
} }

View File

@ -1,15 +1,15 @@
//! Models of PDF types //! Models of PDF types
use std::collections::{HashMap, VecDeque};
use datasize::DataSize; use datasize::DataSize;
use std::collections::HashMap;
use crate as pdf; use crate as pdf;
use crate::content::deep_clone_op; use crate::content::deep_clone_op;
use crate::content::{parse_ops, serialize_ops, Content, FormXObject, Matrix, Op};
use crate::enc::StreamFilter;
use crate::error::*;
use crate::font::Font;
use crate::object::*; use crate::object::*;
use crate::error::*;
use crate::content::{Content, FormXObject, Matrix, parse_ops, serialize_ops, Op};
use crate::font::Font;
use crate::enc::StreamFilter;
/// Node in a page tree - type is either `Page` or `PageTree` /// Node in a page tree - type is either `Page` or `PageTree`
#[derive(Debug, Clone, DataSize)] #[derive(Debug, Clone, DataSize)]
@ -24,10 +24,7 @@ impl Object for PagesNode {
match dict.require("PagesNode", "Type")?.as_name()? { match dict.require("PagesNode", "Type")?.as_name()? {
"Page" => Ok(PagesNode::Leaf(t!(Page::from_dict(dict, resolve)))), "Page" => Ok(PagesNode::Leaf(t!(Page::from_dict(dict, resolve)))),
"Pages" => Ok(PagesNode::Tree(t!(PageTree::from_dict(dict, resolve)))), "Pages" => Ok(PagesNode::Tree(t!(PageTree::from_dict(dict, resolve)))),
other => Err(PdfError::WrongDictionaryType { other => Err(PdfError::WrongDictionaryType {expected: "Page or Pages".into(), found: other.into()}),
expected: "Page or Pages".into(),
found: other.into(),
}),
} }
} }
} }
@ -63,7 +60,7 @@ impl Deref for PageRc {
fn deref(&self) -> &Page { fn deref(&self) -> &Page {
match *self.0 { match *self.0 {
PagesNode::Leaf(ref page) => page, PagesNode::Leaf(ref page) => page,
_ => unreachable!(), _ => unreachable!()
} }
} }
} }
@ -79,11 +76,8 @@ impl Object for PageRc {
fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<PageRc> { fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<PageRc> {
let node = t!(RcRef::from_primitive(p, resolve)); let node = t!(RcRef::from_primitive(p, resolve));
match *node { match *node {
PagesNode::Tree(_) => Err(PdfError::WrongDictionaryType { PagesNode::Tree(_) => Err(PdfError::WrongDictionaryType {expected: "Page".into(), found: "Pages".into()}),
expected: "Page".into(), PagesNode::Leaf(_) => Ok(PageRc(node))
found: "Pages".into(),
}),
PagesNode::Leaf(_) => Ok(PageRc(node)),
} }
} }
} }
@ -102,7 +96,7 @@ impl Deref for PagesRc {
fn deref(&self) -> &PageTree { fn deref(&self) -> &PageTree {
match *self.0 { match *self.0 {
PagesNode::Tree(ref tree) => tree, PagesNode::Tree(ref tree) => tree,
_ => unreachable!(), _ => unreachable!()
} }
} }
} }
@ -115,11 +109,8 @@ impl Object for PagesRc {
fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<PagesRc> { fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<PagesRc> {
let node = t!(RcRef::from_primitive(p, resolve)); let node = t!(RcRef::from_primitive(p, resolve));
match *node { match *node {
PagesNode::Leaf(_) => Err(PdfError::WrongDictionaryType { PagesNode::Leaf(_) => Err(PdfError::WrongDictionaryType {expected: "Pages".into(), found: "Page".into()}),
expected: "Pages".into(), PagesNode::Tree(_) => Ok(PagesRc(node))
found: "Page".into(),
}),
PagesNode::Tree(_) => Ok(PagesRc(node)),
} }
} }
} }
@ -150,6 +141,7 @@ pub struct Catalog {
// ViewerPreferences: dict // ViewerPreferences: dict
// PageLayout: name // PageLayout: name
// PageMode: name // PageMode: name
#[pdf(key="Outlines")] #[pdf(key="Outlines")]
pub outlines: Option<Outlines>, pub outlines: Option<Outlines>,
// Threads: array // Threads: array
@ -166,6 +158,7 @@ pub struct Catalog {
#[pdf(key="StructTreeRoot")] #[pdf(key="StructTreeRoot")]
pub struct_tree_root: Option<StructTreeRoot>, pub struct_tree_root: Option<StructTreeRoot>,
// MarkInfo: dict // MarkInfo: dict
// Lang: text string // Lang: text string
// SpiderInfo: dict // SpiderInfo: dict
@ -308,14 +301,13 @@ pub struct Page {
pub other: Dictionary, pub other: Dictionary,
} }
fn inherit<'a, T: 'a, F>(mut parent: &'a PageTree, f: F) -> Result<Option<T>> fn inherit<'a, T: 'a, F>(mut parent: &'a PageTree, f: F) -> Result<Option<T>>
where where F: Fn(&'a PageTree) -> Option<T>
F: Fn(&'a PageTree) -> Option<T>,
{ {
loop { loop {
match (&parent.parent, f(parent)) { match (&parent.parent, f(parent)) {
(_, Some(t)) => return Ok(Some(t)), (_, Some(t)) => return Ok(Some(t)),
(Some(ref p), None) => parent = p, (Some(ref p), None) => parent = p,
(None, None) => return Ok(None), (None, None) => return Ok(None)
} }
} }
} }
@ -340,12 +332,8 @@ impl Page {
pub fn media_box(&self) -> Result<Rectangle> { pub fn media_box(&self) -> Result<Rectangle> {
match self.media_box { match self.media_box {
Some(b) => Ok(b), Some(b) => Ok(b),
None => { None => inherit(&self.parent, |pt| pt.media_box)?
inherit(&self.parent, |pt| pt.media_box)?.ok_or_else(|| PdfError::MissingEntry { .ok_or_else(|| PdfError::MissingEntry { typ: "Page", field: "MediaBox".into() })
typ: "Page",
field: "MediaBox".into(),
})
}
} }
} }
pub fn crop_box(&self) -> Result<Rectangle> { pub fn crop_box(&self) -> Result<Rectangle> {
@ -353,24 +341,21 @@ impl Page {
Some(b) => Ok(b), Some(b) => Ok(b),
None => match inherit(&self.parent, |pt| pt.crop_box)? { None => match inherit(&self.parent, |pt| pt.crop_box)? {
Some(b) => Ok(b), Some(b) => Ok(b),
None => self.media_box(), None => self.media_box()
}, }
} }
} }
pub fn resources(&self) -> Result<&MaybeRef<Resources>> { pub fn resources(&self) -> Result<&MaybeRef<Resources>> {
match self.resources { match self.resources {
Some(ref r) => Ok(r), Some(ref r) => Ok(r),
None => inherit(&self.parent, |pt| pt.resources.as_ref())?.ok_or_else(|| { None => inherit(&self.parent, |pt| pt.resources.as_ref())?
PdfError::MissingEntry { .ok_or_else(|| PdfError::MissingEntry { typ: "Page", field: "Resources".into() })
typ: "Page",
field: "Resources".into(),
}
}),
} }
} }
} }
impl SubType<PagesNode> for Page {} impl SubType<PagesNode> for Page {}
#[derive(Object, DataSize, Debug, ObjectWrite)] #[derive(Object, DataSize, Debug, ObjectWrite)]
pub struct PageLabel { pub struct PageLabel {
#[pdf(key="S")] #[pdf(key="S")]
@ -380,7 +365,7 @@ pub struct PageLabel {
pub prefix: Option<PdfString>, pub prefix: Option<PdfString>,
#[pdf(key="St")] #[pdf(key="St")]
pub start: Option<usize>, pub start: Option<usize>
} }
#[derive(Object, ObjectWrite, Debug, DataSize, Default, DeepClone, Clone)] #[derive(Object, ObjectWrite, Debug, DataSize, Default, DeepClone, Clone)]
@ -405,6 +390,7 @@ pub struct Resources {
pub properties: HashMap<Name, MaybeRef<Dictionary>>, pub properties: HashMap<Name, MaybeRef<Dictionary>>,
} }
#[derive(Debug, Object, ObjectWrite, DataSize, Clone, DeepClone)] #[derive(Debug, Object, ObjectWrite, DataSize, Clone, DeepClone)]
pub struct PatternDict { pub struct PatternDict {
#[pdf(key="PaintType")] #[pdf(key="PaintType")]
@ -446,9 +432,7 @@ impl Object for Pattern {
fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<Self> { fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<Self> {
let p = p.resolve(resolve)?; let p = p.resolve(resolve)?;
match p { match p {
Primitive::Dictionary(dict) => { Primitive::Dictionary(dict) => Ok(Pattern::Dict(PatternDict::from_dict(dict, resolve)?)),
Ok(Pattern::Dict(PatternDict::from_dict(dict, resolve)?))
}
Primitive::Stream(s) => { Primitive::Stream(s) => {
let stream: Stream<PatternDict> = Stream::from_stream(s, resolve)?; let stream: Stream<PatternDict> = Stream::from_stream(s, resolve)?;
let data = stream.data(resolve)?; let data = stream.data(resolve)?;
@ -456,10 +440,7 @@ impl Object for Pattern {
let dict = stream.info.info; let dict = stream.info.info;
Ok(Pattern::Stream(dict, ops)) Ok(Pattern::Stream(dict, ops))
} }
p => Err(PdfError::UnexpectedPrimitive { p => Err(PdfError::UnexpectedPrimitive { expected: "Dictionary or Stream", found: p.get_debug_name() })
expected: "Dictionary or Stream",
found: p.get_debug_name(),
}),
} }
} }
} }
@ -482,10 +463,7 @@ impl DeepClone for Pattern {
Pattern::Stream(ref dict, ref ops) => { Pattern::Stream(ref dict, ref ops) => {
let old_resources = cloner.get(dict.resources)?; let old_resources = cloner.get(dict.resources)?;
let mut resources = Resources::default(); let mut resources = Resources::default();
let ops: Vec<Op> = ops let ops: Vec<Op> = ops.iter().map(|op| deep_clone_op(op, cloner, &old_resources, &mut resources)).collect::<Result<Vec<_>>>()?;
.iter()
.map(|op| deep_clone_op(op, cloner, &old_resources, &mut resources))
.collect::<Result<Vec<_>>>()?;
let dict = PatternDict { let dict = PatternDict {
resources: cloner.create(resources)?.get_ref(), resources: cloner.create(resources)?.get_ref(),
.. *dict .. *dict
@ -500,13 +478,13 @@ impl DeepClone for Pattern {
pub enum LineCap { pub enum LineCap {
Butt = 0, Butt = 0,
Round = 1, Round = 1,
Square = 2, Square = 2
} }
#[derive(Object, ObjectWrite, DeepClone, Debug, DataSize, Copy, Clone)] #[derive(Object, ObjectWrite, DeepClone, Debug, DataSize, Copy, Clone)]
pub enum LineJoin { pub enum LineJoin {
Miter = 0, Miter = 0,
Round = 1, Round = 1,
Bevel = 2, Bevel = 2
} }
#[derive(Object, ObjectWrite, DeepClone, Debug, DataSize, Clone)] #[derive(Object, ObjectWrite, DeepClone, Debug, DataSize, Clone)]
@ -553,12 +531,14 @@ pub struct GraphicsStateParameters {
// FL // FL
// SM // SM
// SA // SA
#[pdf(key="BM")] #[pdf(key="BM")]
pub blend_mode: Option<Primitive>, pub blend_mode: Option<Primitive>,
#[pdf(key="SMask")] #[pdf(key="SMask")]
pub smask: Option<Primitive>, pub smask: Option<Primitive>,
#[pdf(key="CA")] #[pdf(key="CA")]
pub stroke_alpha: Option<f32>, pub stroke_alpha: Option<f32>,
@ -572,7 +552,7 @@ pub struct GraphicsStateParameters {
pub text_knockout: Option<bool>, pub text_knockout: Option<bool>,
#[pdf(other)] #[pdf(other)]
_other: Dictionary, _other: Dictionary
} }
#[derive(Object, Debug, DataSize, DeepClone)] #[derive(Object, Debug, DataSize, DeepClone)]
@ -601,7 +581,7 @@ pub type PostScriptXObject = Stream<PostScriptDict>;
#[derive(Debug, DataSize, Clone, DeepClone)] #[derive(Debug, DataSize, Clone, DeepClone)]
pub struct ImageXObject { pub struct ImageXObject {
pub inner: Stream<ImageDict>, pub inner: Stream<ImageDict>
} }
impl Object for ImageXObject { impl Object for ImageXObject {
fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<Self> { fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<Self> {
@ -627,7 +607,7 @@ pub enum ImageFormat {
Jp2k, Jp2k,
Jbig2, Jbig2,
CittFax, CittFax,
Png, Png
} }
impl ImageXObject { impl ImageXObject {
@ -637,38 +617,32 @@ impl ImageXObject {
} }
/// Decode everything except for the final image encoding (jpeg, jbig2, jp2k, ...) /// Decode everything except for the final image encoding (jpeg, jbig2, jp2k, ...)
pub fn raw_image_data( pub fn raw_image_data(&self, resolve: &impl Resolve) -> Result<(Arc<[u8]>, Option<&StreamFilter>)> {
&self,
resolve: &impl Resolve,
) -> Result<(Arc<[u8]>, Option<&StreamFilter>)> {
match self.inner.inner_data { match self.inner.inner_data {
StreamData::Generated(_) => Ok((self.inner.data(resolve)?, None)), StreamData::Generated(_) => Ok((self.inner.data(resolve)?, None)),
StreamData::Original(ref file_range, id) => { StreamData::Original(ref file_range, id) => {
let filters = self.inner.filters.as_slice(); let filters = self.inner.filters.as_slice();
// decode all non image filters // decode all non image filters
let end = filters let end = filters.iter().rposition(|f| match f {
.iter()
.rposition(|f| match f {
StreamFilter::ASCIIHexDecode => false, StreamFilter::ASCIIHexDecode => false,
StreamFilter::ASCII85Decode => false, StreamFilter::ASCII85Decode => false,
StreamFilter::LZWDecode(_) => false, StreamFilter::LZWDecode(_) => false,
StreamFilter::RunLengthDecode => false, StreamFilter::RunLengthDecode => false,
StreamFilter::Crypt => true, StreamFilter::Crypt => true,
_ => true, _ => true
}) }).unwrap_or(filters.len());
.unwrap_or(filters.len());
let (normal_filters, image_filters) = filters.split_at(end); let (normal_filters, image_filters) = filters.split_at(end);
let data = resolve.get_data_or_decode(id, file_range.clone(), normal_filters)?; let data = resolve.get_data_or_decode(id, file_range.clone(), normal_filters)?;
match image_filters { match image_filters {
[] => Ok((data, None)), [] => Ok((data, None)),
[StreamFilter::DCTDecode(_)] [StreamFilter::DCTDecode(_)] |
| [StreamFilter::CCITTFaxDecode(_)] [StreamFilter::CCITTFaxDecode(_)] |
| [StreamFilter::JPXDecode] [StreamFilter::JPXDecode] |
| [StreamFilter::FlateDecode(_)] [StreamFilter::FlateDecode(_)] |
| [StreamFilter::JBIG2Decode(_)] => Ok((data, Some(&image_filters[0]))), [StreamFilter::JBIG2Decode(_)] => Ok((data, Some(&image_filters[0]))),
_ => bail!("??? filters={:?}", image_filters), _ => bail!("??? filters={:?}", image_filters)
} }
} }
} }
@ -678,16 +652,12 @@ impl ImageXObject {
let (data, filter) = self.raw_image_data(resolve)?; let (data, filter) = self.raw_image_data(resolve)?;
let filter = match filter { let filter = match filter {
Some(f) => f, Some(f) => f,
None => return Ok(data), None => return Ok(data)
}; };
let mut data = match filter { let mut data = match filter {
StreamFilter::CCITTFaxDecode(ref params) => { StreamFilter::CCITTFaxDecode(ref params) => {
if self.inner.info.width != params.columns { if self.inner.info.width != params.columns {
bail!( bail!("image width mismatch {} != {}", self.inner.info.width, params.columns);
"image width mismatch {} != {}",
self.inner.info.width,
params.columns
);
} }
let mut data = fax_decode(&data, params)?; let mut data = fax_decode(&data, params)?;
if params.rows == 0 { if params.rows == 0 {
@ -701,9 +671,9 @@ impl ImageXObject {
StreamFilter::JBIG2Decode(ref p) => { StreamFilter::JBIG2Decode(ref p) => {
let global_data = p.globals.as_ref().map(|s| s.data(resolve)).transpose()?; let global_data = p.globals.as_ref().map(|s| s.data(resolve)).transpose()?;
jbig2_decode(&data, global_data.as_deref().unwrap_or_default())? jbig2_decode(&data, global_data.as_deref().unwrap_or_default())?
} },
StreamFilter::FlateDecode(ref p) => flate_decode(&data, p)?, StreamFilter::FlateDecode(ref p) => flate_decode(&data, p)?,
_ => unreachable!(), _ => unreachable!()
}; };
if let Some(ref decode) = self.decode { if let Some(ref decode) = self.decode {
if decode == &[1.0, 0.0] && self.bits_per_component == Some(1) { if decode == &[1.0, 0.0] && self.bits_per_component == Some(1) {
@ -719,7 +689,7 @@ impl ImageXObject {
pub struct PostScriptDict { pub struct PostScriptDict {
// TODO // TODO
#[pdf(other)] #[pdf(other)]
pub other: Dictionary, pub other: Dictionary
} }
#[derive(Object, Debug, Clone, DataSize, DeepClone, ObjectWrite, Default)] #[derive(Object, Debug, Clone, DataSize, DeepClone, ObjectWrite, Default)]
@ -737,10 +707,12 @@ pub struct ImageDict {
#[pdf(key="BitsPerComponent")] #[pdf(key="BitsPerComponent")]
pub bits_per_component: Option<i32>, pub bits_per_component: Option<i32>,
// Note: only allowed values are 1, 2, 4, 8, 16. Enum? // Note: only allowed values are 1, 2, 4, 8, 16. Enum?
#[pdf(key="Intent")] #[pdf(key="Intent")]
pub intent: Option<RenderingIntent>, pub intent: Option<RenderingIntent>,
// Note: default: "the current rendering intent in the graphics state" - I don't think this // Note: default: "the current rendering intent in the graphics state" - I don't think this
// ought to have a default then // ought to have a default then
#[pdf(key="ImageMask", default="false")] #[pdf(key="ImageMask", default="false")]
pub image_mask: bool, pub image_mask: bool,
@ -775,10 +747,12 @@ pub struct ImageDict {
// OPI: dict // OPI: dict
// Metadata: stream // Metadata: stream
// OC: dict // OC: dict
#[pdf(other)] #[pdf(other)]
pub other: Dictionary, pub other: Dictionary
} }
#[derive(Object, Debug, Copy, Clone, DataSize, DeepClone, ObjectWrite)] #[derive(Object, Debug, Copy, Clone, DataSize, DeepClone, ObjectWrite)]
pub enum RenderingIntent { pub enum RenderingIntent {
AbsoluteColorimetric, AbsoluteColorimetric,
@ -793,7 +767,7 @@ impl RenderingIntent {
"RelativeColorimetric" => Some(RenderingIntent::RelativeColorimetric), "RelativeColorimetric" => Some(RenderingIntent::RelativeColorimetric),
"Perceptual" => Some(RenderingIntent::Perceptual), "Perceptual" => Some(RenderingIntent::Perceptual),
"Saturation" => Some(RenderingIntent::Saturation), "Saturation" => Some(RenderingIntent::Saturation),
_ => None, _ => None
} }
} }
pub fn to_str(self) -> &'static str { pub fn to_str(self) -> &'static str {
@ -852,6 +826,7 @@ pub struct FormDict {
pub other: Dictionary, pub other: Dictionary,
} }
#[derive(Object, ObjectWrite, Debug, Clone, DataSize)] #[derive(Object, ObjectWrite, Debug, Clone, DataSize)]
pub struct InteractiveFormDictionary { pub struct InteractiveFormDictionary {
#[pdf(key="Fields")] #[pdf(key="Fields")]
@ -907,7 +882,7 @@ pub struct SeedValueDictionary {
#[pdf(key="DigestMethod")] #[pdf(key="DigestMethod")]
pub digest_method: Vec<PdfString>, pub digest_method: Vec<PdfString>,
#[pdf(other)] #[pdf(other)]
pub other: Dictionary, pub other: Dictionary
} }
#[derive(Object, ObjectWrite, Debug)] #[derive(Object, ObjectWrite, Debug)]
@ -946,7 +921,7 @@ pub struct SignatureDictionary {
#[pdf(key="Prop_AuthType")] #[pdf(key="Prop_AuthType")]
pub prop_auth_type: Name, pub prop_auth_type: Name,
#[pdf(other)] #[pdf(other)]
pub other: Dictionary, pub other: Dictionary
} }
#[derive(Object, ObjectWrite, Debug)] #[derive(Object, ObjectWrite, Debug)]
@ -961,9 +936,10 @@ pub struct SignatureReferenceDictionary {
#[pdf(key="DigestMethod")] #[pdf(key="DigestMethod")]
pub digest_method: Option<Name>, pub digest_method: Option<Name>,
#[pdf(other)] #[pdf(other)]
pub other: Dictionary, pub other: Dictionary
} }
#[derive(Object, ObjectWrite, Debug, Clone, DataSize)] #[derive(Object, ObjectWrite, Debug, Clone, DataSize)]
#[pdf(Type="Annot?")] #[pdf(Type="Annot?")]
pub struct Annot { pub struct Annot {
@ -1055,7 +1031,7 @@ pub struct FieldDictionary {
pub subtype: Option<Name>, pub subtype: Option<Name>,
#[pdf(other)] #[pdf(other)]
pub other: Dictionary, pub other: Dictionary
} }
#[derive(Object, ObjectWrite, Debug, DataSize, Clone, DeepClone)] #[derive(Object, ObjectWrite, Debug, DataSize, Clone, DeepClone)]
@ -1073,21 +1049,14 @@ pub struct AppearanceStreams {
#[derive(Clone, Debug, DeepClone)] #[derive(Clone, Debug, DeepClone)]
pub enum AppearanceStreamEntry { pub enum AppearanceStreamEntry {
Single(FormXObject), Single(FormXObject),
Dict(HashMap<Name, AppearanceStreamEntry>), Dict(HashMap<Name, AppearanceStreamEntry>)
} }
impl Object for AppearanceStreamEntry { impl Object for AppearanceStreamEntry {
fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<Self> { fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<Self> {
match p.resolve(resolve)? { match p.resolve(resolve)? {
p @ Primitive::Dictionary(_) => { p @ Primitive::Dictionary(_) => Object::from_primitive(p, resolve).map(AppearanceStreamEntry::Dict),
Object::from_primitive(p, resolve).map(AppearanceStreamEntry::Dict) p @ Primitive::Stream(_) => Object::from_primitive(p, resolve).map(AppearanceStreamEntry::Single),
} p => Err(PdfError::UnexpectedPrimitive {expected: "Dict or Stream", found: p.get_debug_name()})
p @ Primitive::Stream(_) => {
Object::from_primitive(p, resolve).map(AppearanceStreamEntry::Single)
}
p => Err(PdfError::UnexpectedPrimitive {
expected: "Dict or Stream",
found: p.get_debug_name(),
}),
} }
} }
} }
@ -1105,7 +1074,7 @@ impl DataSize for AppearanceStreamEntry {
fn estimate_heap_size(&self) -> usize { fn estimate_heap_size(&self) -> usize {
match self { match self {
AppearanceStreamEntry::Dict(d) => d.estimate_heap_size(), AppearanceStreamEntry::Dict(d) => d.estimate_heap_size(),
AppearanceStreamEntry::Single(s) => s.estimate_heap_size(), AppearanceStreamEntry::Single(s) => s.estimate_heap_size()
} }
} }
} }
@ -1121,7 +1090,7 @@ pub enum Counter {
#[pdf(name="a")] #[pdf(name="a")]
AlphaUpper, AlphaUpper,
#[pdf(name="A")] #[pdf(name="A")]
AlphaLower, AlphaLower
} }
#[derive(Debug, DataSize)] #[derive(Debug, DataSize)]
@ -1129,7 +1098,8 @@ pub enum NameTreeNode<T> {
/// ///
Intermediate (Vec<Ref<NameTree<T>>>), Intermediate (Vec<Ref<NameTree<T>>>),
/// ///
Leaf(Vec<(PdfString, T)>), Leaf (Vec<(PdfString, T)>)
} }
/// Note: The PDF concept of 'root' node is an intermediate or leaf node which has no 'Limits' /// Note: The PDF concept of 'root' node is an intermediate or leaf node which has no 'Limits'
/// entry. Hence, `limits`, /// entry. Hence, `limits`,
@ -1139,11 +1109,7 @@ pub struct NameTree<T> {
pub node: NameTreeNode<T>, pub node: NameTreeNode<T>,
} }
impl<T: Object+DataSize> NameTree<T> { impl<T: Object+DataSize> NameTree<T> {
pub fn walk( pub fn walk(&self, r: &impl Resolve, callback: &mut dyn FnMut(&PdfString, &T)) -> Result<(), PdfError> {
&self,
r: &impl Resolve,
callback: &mut dyn FnMut(&PdfString, &T),
) -> Result<(), PdfError> {
match self.node { match self.node {
NameTreeNode::Leaf(ref items) => { NameTreeNode::Leaf(ref items) => {
for (name, val) in items { for (name, val) in items {
@ -1176,7 +1142,7 @@ impl<T: Object> Object for NameTree<T> {
Some((min, max)) Some((min, max))
} }
None => None, None => None
}; };
let kids = dict.remove("Kids"); let kids = dict.remove("Kids");
@ -1184,15 +1150,12 @@ impl<T: Object> Object for NameTree<T> {
// If no `kids`, try `names`. Else there is an error. // If no `kids`, try `names`. Else there is an error.
Ok(match (kids, names) { Ok(match (kids, names) {
(Some(kids), _) => { (Some(kids), _) => {
let kids = t!(kids let kids = t!(kids.resolve(resolve)?.into_array()?.iter().map(|kid|
.resolve(resolve)? Ref::<NameTree<T>>::from_primitive(kid.clone(), resolve)
.into_array()? ).collect::<Result<Vec<_>>>());
.iter()
.map(|kid| Ref::<NameTree<T>>::from_primitive(kid.clone(), resolve))
.collect::<Result<Vec<_>>>());
NameTree { NameTree {
limits, limits,
node: NameTreeNode::Intermediate(kids), node: NameTreeNode::Intermediate (kids)
} }
} }
(None, Some(names)) => { (None, Some(names)) => {
@ -1212,7 +1175,7 @@ impl<T: Object> Object for NameTree<T> {
warn!("Neither Kids nor Names present in NameTree node."); warn!("Neither Kids nor Names present in NameTree node.");
NameTree { NameTree {
limits, limits,
node: NameTreeNode::Intermediate(vec![]), node: NameTreeNode::Intermediate(vec![])
} }
} }
}) })
@ -1251,22 +1214,19 @@ impl<T: Object> Object for NumberTree<T> {
Some((min, max)) Some((min, max))
} }
None => None, None => None
}; };
let kids = dict.remove("Kids"); let kids = dict.remove("Kids");
let nums = dict.remove("Nums"); let nums = dict.remove("Nums");
match (kids, nums) { match (kids, nums) {
(Some(kids), _) => { (Some(kids), _) => {
let kids = t!(kids let kids = t!(kids.resolve(resolve)?.into_array()?.iter().map(|kid|
.resolve(resolve)? Ref::<NumberTree<T>>::from_primitive(kid.clone(), resolve)
.into_array()? ).collect::<Result<Vec<_>>>());
.iter()
.map(|kid| Ref::<NumberTree<T>>::from_primitive(kid.clone(), resolve))
.collect::<Result<Vec<_>>>());
Ok(NumberTree { Ok(NumberTree {
limits, limits,
node: NumberTreeNode::Intermediate(kids), node: NumberTreeNode::Intermediate (kids)
}) })
} }
(None, Some(nums)) => { (None, Some(nums)) => {
@ -1279,14 +1239,14 @@ impl<T: Object> Object for NumberTree<T> {
} }
Ok(NumberTree { Ok(NumberTree {
limits, limits,
node: NumberTreeNode::Leaf(items), node: NumberTreeNode::Leaf(items)
}) })
} }
(None, None) => { (None, None) => {
warn!("Neither Kids nor Names present in NumberTree node."); warn!("Neither Kids nor Names present in NumberTree node.");
Ok(NumberTree { Ok(NumberTree {
limits, limits,
node: NumberTreeNode::Intermediate(vec![]), node: NumberTreeNode::Intermediate(vec![])
}) })
} }
} }
@ -1308,21 +1268,14 @@ impl<T: ObjectWrite> ObjectWrite for NumberTree<T> {
dict.insert("Nums", nums); dict.insert("Nums", nums);
} }
NumberTreeNode::Intermediate(ref kids) => { NumberTreeNode::Intermediate(ref kids) => {
dict.insert( dict.insert("Kids", kids.iter().map(|r| r.get_inner().into()).collect_vec());
"Kids",
kids.iter().map(|r| r.get_inner().into()).collect_vec(),
);
} }
} }
Ok(dict.into()) Ok(dict.into())
} }
} }
impl<T: Object+DataSize> NumberTree<T> { impl<T: Object+DataSize> NumberTree<T> {
pub fn walk( pub fn walk(&self, r: &impl Resolve, callback: &mut dyn FnMut(i32, &T)) -> Result<(), PdfError> {
&self,
r: &impl Resolve,
callback: &mut dyn FnMut(i32, &T),
) -> Result<(), PdfError> {
match self.node { match self.node {
NumberTreeNode::Leaf(ref items) => { NumberTreeNode::Leaf(ref items) => {
for &(idx, ref val) in items { for &(idx, ref val) in items {
@ -1355,23 +1308,13 @@ pub struct LageLabel {
#[derive(Debug, Clone, DataSize)] #[derive(Debug, Clone, DataSize)]
pub enum DestView { pub enum DestView {
// left, top, zoom // left, top, zoom
XYZ { XYZ { left: Option<f32>, top: Option<f32>, zoom: f32 },
left: Option<f32>,
top: Option<f32>,
zoom: f32,
},
Fit, Fit,
FitH { FitH { top: f32 },
top: f32, FitV { left: f32 },
},
FitV {
left: f32,
},
FitR(Rectangle), FitR(Rectangle),
FitB, FitB,
FitBH { FitBH { top: f32 }
top: f32,
},
} }
#[derive(Debug, Clone, DataSize)] #[derive(Debug, Clone, DataSize)]
@ -1383,17 +1326,17 @@ pub enum MaybeNamedDest {
#[derive(Debug, Clone, DataSize)] #[derive(Debug, Clone, DataSize)]
pub struct Dest { pub struct Dest {
pub page: Option<Ref<Page>>, pub page: Option<Ref<Page>>,
pub view: DestView, pub view: DestView
} }
impl Object for Dest { impl Object for Dest {
fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<Self> { fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<Self> {
let p = match p { let p = match p {
Primitive::Reference(r) => resolve.resolve(r)?, Primitive::Reference(r) => resolve.resolve(r)?,
p => p, p => p
}; };
let p = match p { let p = match p {
Primitive::Dictionary(mut dict) => dict.require("Dest", "D")?, Primitive::Dictionary(mut dict) => dict.require("Dest", "D")?,
p => p, p => p
}; };
let array = t!(p.as_array(), p); let array = t!(p.as_array(), p);
Dest::from_array(array, resolve) Dest::from_array(array, resolve)
@ -1409,43 +1352,28 @@ impl Dest {
Primitive::Null => None, Primitive::Null => None,
Primitive::Integer(n) => Some(n as f32), Primitive::Integer(n) => Some(n as f32),
Primitive::Number(f) => Some(f), Primitive::Number(f) => Some(f),
ref p => { ref p => return Err(PdfError::UnexpectedPrimitive { expected: "Number | Integer | Null", found: p.get_debug_name() }),
return Err(PdfError::UnexpectedPrimitive {
expected: "Number | Integer | Null",
found: p.get_debug_name(),
})
}
}, },
top: match *try_opt!(array.get(3)) { top: match *try_opt!(array.get(3)) {
Primitive::Null => None, Primitive::Null => None,
Primitive::Integer(n) => Some(n as f32), Primitive::Integer(n) => Some(n as f32),
Primitive::Number(f) => Some(f), Primitive::Number(f) => Some(f),
ref p => { ref p => return Err(PdfError::UnexpectedPrimitive { expected: "Number | Integer | Null", found: p.get_debug_name() }),
return Err(PdfError::UnexpectedPrimitive {
expected: "Number | Integer | Null",
found: p.get_debug_name(),
})
}
}, },
zoom: match array.get(4) { zoom: match array.get(4) {
Some(Primitive::Null) => 0.0, Some(Primitive::Null) => 0.0,
Some(&Primitive::Integer(n)) => n as f32, Some(&Primitive::Integer(n)) => n as f32,
Some(&Primitive::Number(f)) => f, Some(&Primitive::Number(f)) => f,
Some(p) => { Some(p) => return Err(PdfError::UnexpectedPrimitive { expected: "Number | Integer | Null", found: p.get_debug_name() }),
return Err(PdfError::UnexpectedPrimitive {
expected: "Number | Integer | Null",
found: p.get_debug_name(),
})
}
None => 0.0, None => 0.0,
}, },
}, },
"Fit" => DestView::Fit, "Fit" => DestView::Fit,
"FitH" => DestView::FitH { "FitH" => DestView::FitH {
top: try_opt!(array.get(2)).as_number()?, top: try_opt!(array.get(2)).as_number()?
}, },
"FitV" => DestView::FitV { "FitV" => DestView::FitV {
left: try_opt!(array.get(2)).as_number()?, left: try_opt!(array.get(2)).as_number()?
}, },
"FitR" => DestView::FitR(Rectangle { "FitR" => DestView::FitR(Rectangle {
left: try_opt!(array.get(2)).as_number()?, left: try_opt!(array.get(2)).as_number()?,
@ -1455,28 +1383,26 @@ impl Dest {
}), }),
"FitB" => DestView::FitB, "FitB" => DestView::FitB,
"FitBH" => DestView::FitBH { "FitBH" => DestView::FitBH {
top: try_opt!(array.get(2)).as_number()?, top: try_opt!(array.get(2)).as_number()?
}, },
name => { name => return Err(PdfError::UnknownVariant { id: "Dest", name: name.into() })
return Err(PdfError::UnknownVariant {
id: "Dest",
name: name.into(),
})
}
}; };
Ok(Dest { page, view }) Ok(Dest {
page,
view
})
} }
} }
impl Object for MaybeNamedDest { impl Object for MaybeNamedDest {
fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<Self> { fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<Self> {
let p = match p { let p = match p {
Primitive::Reference(r) => resolve.resolve(r)?, Primitive::Reference(r) => resolve.resolve(r)?,
p => p, p => p
}; };
let p = match p { let p = match p {
Primitive::Dictionary(mut dict) => dict.require("Dest", "D")?, Primitive::Dictionary(mut dict) => dict.require("Dest", "D")?,
Primitive::String(s) => return Ok(MaybeNamedDest::Named(s)), Primitive::String(s) => return Ok(MaybeNamedDest::Named(s)),
p => p, p => p
}; };
let array = t!(p.as_array(), p); let array = t!(p.as_array(), p);
Dest::from_array(array, resolve).map(MaybeNamedDest::Direct) Dest::from_array(array, resolve).map(MaybeNamedDest::Direct)
@ -1486,7 +1412,7 @@ impl ObjectWrite for MaybeNamedDest {
fn to_primitive(&self, update: &mut impl Updater) -> Result<Primitive> { fn to_primitive(&self, update: &mut impl Updater) -> Result<Primitive> {
match self { match self {
MaybeNamedDest::Named(s) => Ok(Primitive::String(s.clone())), MaybeNamedDest::Named(s) => Ok(Primitive::String(s.clone())),
MaybeNamedDest::Direct(d) => d.to_primitive(update), MaybeNamedDest::Direct(d) => d.to_primitive(update)
} }
} }
} }
@ -1600,28 +1526,13 @@ impl<T: DataSize> DataSize for Files<T> {
const STATIC_HEAP_SIZE: usize = 5 * Option::<T>::STATIC_HEAP_SIZE; const STATIC_HEAP_SIZE: usize = 5 * Option::<T>::STATIC_HEAP_SIZE;
fn estimate_heap_size(&self) -> usize { fn estimate_heap_size(&self) -> usize {
self.f.as_ref().map(|t| t.estimate_heap_size()).unwrap_or(0) self.f.as_ref().map(|t| t.estimate_heap_size()).unwrap_or(0) +
+ self self.uf.as_ref().map(|t| t.estimate_heap_size()).unwrap_or(0) +
.uf self.dos.as_ref().map(|t| t.estimate_heap_size()).unwrap_or(0) +
.as_ref() self.mac.as_ref().map(|t| t.estimate_heap_size()).unwrap_or(0) +
.map(|t| t.estimate_heap_size()) self.unix.as_ref().map(|t| t.estimate_heap_size()).unwrap_or(0)
.unwrap_or(0)
+ self
.dos
.as_ref()
.map(|t| t.estimate_heap_size())
.unwrap_or(0)
+ self
.mac
.as_ref()
.map(|t| t.estimate_heap_size())
.unwrap_or(0)
+ self
.unix
.as_ref()
.map(|t| t.estimate_heap_size())
.unwrap_or(0)
} }
} }
/// PDF Embedded File Stream. /// PDF Embedded File Stream.
@ -1691,7 +1602,7 @@ pub struct OutlineItem {
#[derive(Clone, Debug, DataSize)] #[derive(Clone, Debug, DataSize)]
pub enum Action { pub enum Action {
Goto(MaybeNamedDest), Goto(MaybeNamedDest),
Other(Dictionary), Other(Dictionary)
} }
impl Object for Action { impl Object for Action {
fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<Self> { fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result<Self> {
@ -1699,13 +1610,10 @@ impl Object for Action {
let s = try_opt!(d.get("S")).as_name()?; let s = try_opt!(d.get("S")).as_name()?;
match s { match s {
"GoTo" => { "GoTo" => {
let dest = t!(MaybeNamedDest::from_primitive( let dest = t!(MaybeNamedDest::from_primitive(try_opt!(d.remove("D")), resolve));
try_opt!(d.remove("D")),
resolve
));
Ok(Action::Goto(dest)) Ok(Action::Goto(dest))
} }
_ => Ok(Action::Other(d)), _ => Ok(Action::Other(d))
} }
} }
} }
@ -1717,7 +1625,7 @@ impl ObjectWrite for Action {
dict.insert("D", dest.to_primitive(update)?); dict.insert("D", dest.to_primitive(update)?);
Ok(Primitive::Dictionary(dict)) Ok(Primitive::Dictionary(dict))
} }
Action::Other(dict) => Ok(Primitive::Dictionary(dict.clone())), Action::Other(dict) => Ok(Primitive::Dictionary(dict.clone()))
} }
} }
} }
@ -1733,6 +1641,7 @@ pub struct Outlines {
#[pdf(key="Last")] #[pdf(key="Last")]
pub last: Option<Ref<OutlineItem>>, pub last: Option<Ref<OutlineItem>>,
} }
/// ISO 32000-2:2020(E) 7.9.5 Rectangles (Pg 134) /// ISO 32000-2:2020(E) 7.9.5 Rectangles (Pg 134)
@ -1764,24 +1673,21 @@ impl Object for Rectangle {
left: arr[0].as_number()?, left: arr[0].as_number()?,
bottom: arr[1].as_number()?, bottom: arr[1].as_number()?,
right: arr[2].as_number()?, right: arr[2].as_number()?,
top: arr[3].as_number()?, top: arr[3].as_number()?
}) })
} }
} }
impl ObjectWrite for Rectangle { impl ObjectWrite for Rectangle {
fn to_primitive(&self, update: &mut impl Updater) -> Result<Primitive> { fn to_primitive(&self, update: &mut impl Updater) -> Result<Primitive> {
Primitive::array::<f32, _, _, _>( Primitive::array::<f32, _, _, _>([self.left, self.bottom, self.right, self.top].iter(), update)
[self.left, self.bottom, self.right, self.top].iter(),
update,
)
} }
} }
// Stuff from chapter 10 of the PDF 1.7 ref // Stuff from chapter 10 of the PDF 1.7 ref
#[derive(Object, ObjectWrite, Debug, DataSize)] #[derive(Object, ObjectWrite, Debug, DataSize)]
pub struct MarkInformation { pub struct MarkInformation { // TODO no /Type
// TODO no /Type
/// indicating whether the document conforms to Tagged PDF conventions /// indicating whether the document conforms to Tagged PDF conventions
#[pdf(key="Marked", default="false")] #[pdf(key="Marked", default="false")]
pub marked: bool, pub marked: bool,

View File

@ -1,14 +1,16 @@
use std::borrow::Cow;
use std::ops::{Deref, Range, RangeFrom};
/// Lexing an input file, in the sense of breaking it up into substrings based on delimiters and /// Lexing an input file, in the sense of breaking it up into substrings based on delimiters and
/// whitespace. /// whitespace.
use std::str::FromStr; use std::str::FromStr;
use std::ops::{Range, Deref, RangeFrom};
use std::borrow::Cow;
use crate::error::*; use crate::error::*;
use crate::primitive::Name; use crate::primitive::Name;
mod str; mod str;
pub use self::str::{HexStringLexer, StringLexer}; pub use self::str::{StringLexer, HexStringLexer};
/// `Lexer` has functionality to jump around and traverse the PDF lexemes of a string in any direction. /// `Lexer` has functionality to jump around and traverse the PDF lexemes of a string in any direction.
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
@ -24,7 +26,7 @@ pub struct Lexer<'a> {
fn boundary_rev(data: &[u8], pos: usize, condition: impl Fn(u8) -> bool) -> usize { fn boundary_rev(data: &[u8], pos: usize, condition: impl Fn(u8) -> bool) -> usize {
match data[.. pos].iter().rposition(|&b| !condition(b)) { match data[.. pos].iter().rposition(|&b| !condition(b)) {
Some(start) => start + 1, Some(start) => start + 1,
None => 0, None => 0
} }
} }
@ -33,7 +35,7 @@ fn boundary_rev(data: &[u8], pos: usize, condition: impl Fn(u8) -> bool) -> usiz
fn boundary(data: &[u8], pos: usize, condition: impl Fn(u8) -> bool) -> usize { fn boundary(data: &[u8], pos: usize, condition: impl Fn(u8) -> bool) -> usize {
match data[pos ..].iter().position(|&b| !condition(b)) { match data[pos ..].iter().position(|&b| !condition(b)) {
Some(start) => pos + start, Some(start) => pos + start,
None => data.len(), None => data.len()
} }
} }
@ -50,14 +52,14 @@ impl<'a> Lexer<'a> {
Lexer { Lexer {
pos: 0, pos: 0,
buf, buf,
file_offset: 0, file_offset: 0
} }
} }
pub fn with_offset(buf: &'a [u8], file_offset: usize) -> Lexer<'a> { pub fn with_offset(buf: &'a [u8], file_offset: usize) -> Lexer<'a> {
Lexer { Lexer {
pos: 0, pos: 0,
buf, buf,
file_offset, file_offset
} }
} }
@ -110,6 +112,7 @@ impl<'a> Lexer<'a> {
Err(PdfError::EOF) => Ok(self.new_substr(self.pos..self.pos)), Err(PdfError::EOF) => Ok(self.new_substr(self.pos..self.pos)),
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
/// Returns `Ok` if the next lexeme matches `expected` - else `Err`. /// Returns `Ok` if the next lexeme matches `expected` - else `Err`.
@ -121,7 +124,7 @@ impl<'a> Lexer<'a> {
Err(PdfError::UnexpectedLexeme { Err(PdfError::UnexpectedLexeme {
pos: self.pos, pos: self.pos,
lexeme: word.to_string(), lexeme: word.to_string(),
expected, expected
}) })
} }
} }
@ -210,9 +213,7 @@ impl<'a> Lexer<'a> {
#[inline] #[inline]
pub fn next_as<T>(&mut self) -> Result<T> pub fn next_as<T>(&mut self) -> Result<T>
where where T: FromStr, T::Err: std::error::Error + Send + Sync + 'static
T: FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
{ {
self.next().and_then(|word| word.to::<T>()) self.next().and_then(|word| word.to::<T>())
} }
@ -266,12 +267,14 @@ impl<'a> Lexer<'a> {
#[allow(dead_code)] #[allow(dead_code)]
pub fn seek_newline(&mut self) -> Substr{ pub fn seek_newline(&mut self) -> Substr{
let start = self.pos; let start = self.pos;
while self.buf[self.pos] != b'\n' && self.incr_pos() {} while self.buf[self.pos] != b'\n'
&& self.incr_pos() { }
self.incr_pos(); self.incr_pos();
self.new_substr(start..self.pos) self.new_substr(start..self.pos)
} }
// TODO: seek_substr and seek_substr_back should use next() or back()? // TODO: seek_substr and seek_substr_back should use next() or back()?
/// Moves pos to after the found `substr`. Returns Substr with traversed text if `substr` is found. /// Moves pos to after the found `substr`. Returns Substr with traversed text if `substr` is found.
#[allow(dead_code)] #[allow(dead_code)]
@ -282,7 +285,7 @@ impl<'a> Lexer<'a> {
let mut matched = 0; let mut matched = 0;
loop { loop {
if self.pos >= self.buf.len() { if self.pos >= self.buf.len() {
return None; return None
} }
if self.buf[self.pos] == substr[matched] { if self.buf[self.pos] == substr[matched] {
matched += 1; matched += 1;
@ -303,17 +306,12 @@ impl<'a> Lexer<'a> {
/// Substr if found. /// Substr if found.
pub fn seek_substr_back(&mut self, substr: &[u8]) -> Result<Substr<'a>> { pub fn seek_substr_back(&mut self, substr: &[u8]) -> Result<Substr<'a>> {
let end = self.pos; let end = self.pos;
match self.buf[..end] match self.buf[.. end].windows(substr.len()).rposition(|w| w == substr) {
.windows(substr.len())
.rposition(|w| w == substr)
{
Some(start) => { Some(start) => {
self.pos = start + substr.len(); self.pos = start + substr.len();
Ok(self.new_substr(self.pos .. end)) Ok(self.new_substr(self.pos .. end))
} }
None => Err(PdfError::NotFound { None => Err(PdfError::NotFound {word: String::from_utf8_lossy(substr).into() })
word: String::from_utf8_lossy(substr).into(),
}),
} }
} }
@ -340,9 +338,7 @@ impl<'a> Lexer<'a> {
/// for debugging /// for debugging
pub fn ctx(&self) -> Cow<str> { pub fn ctx(&self) -> Cow<str> {
String::from_utf8_lossy( String::from_utf8_lossy(&self.buf[self.pos.saturating_sub(40)..self.buf.len().min(self.pos+40)])
&self.buf[self.pos.saturating_sub(40)..self.buf.len().min(self.pos + 40)],
)
} }
#[inline] #[inline]
@ -356,21 +352,18 @@ impl<'a> Lexer<'a> {
} }
#[inline] #[inline]
fn is_whitespace(&self, pos: usize) -> bool { fn is_whitespace(&self, pos: usize) -> bool {
self.buf self.buf.get(pos).map(|&b| is_whitespace(b)).unwrap_or(false)
.get(pos)
.map(|&b| is_whitespace(b))
.unwrap_or(false)
} }
#[inline] #[inline]
fn is_delimiter(&self, pos: usize) -> bool { fn is_delimiter(&self, pos: usize) -> bool {
self.buf self.buf.get(pos).map(|b| b"()<>[]{}/%".contains(b)).unwrap_or(false)
.get(pos)
.map(|b| b"()<>[]{}/%".contains(b))
.unwrap_or(false)
} }
} }
/// A slice from some original string - a lexeme. /// A slice from some original string - a lexeme.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct Substr<'a> { pub struct Substr<'a> {
@ -379,10 +372,7 @@ pub struct Substr<'a> {
} }
impl<'a> Substr<'a> { impl<'a> Substr<'a> {
pub fn new<T: AsRef<[u8]> + ?Sized>(data: &'a T, file_offset: usize) -> Self { pub fn new<T: AsRef<[u8]> + ?Sized>(data: &'a T, file_offset: usize) -> Self {
Substr { Substr { slice: data.as_ref(), file_offset }
slice: data.as_ref(),
file_offset,
}
} }
// to: &S -> U. Possibly expensive conversion. // to: &S -> U. Possibly expensive conversion.
// as: &S -> &U. Cheap borrow conversion // as: &S -> &U. Cheap borrow conversion
@ -399,13 +389,9 @@ impl<'a> Substr<'a> {
self.slice.to_vec() self.slice.to_vec()
} }
pub fn to<T>(&self) -> Result<T> pub fn to<T>(&self) -> Result<T>
where where T: FromStr, T::Err: std::error::Error + Send + Sync + 'static
T: FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
{ {
std::str::from_utf8(self.slice)? std::str::from_utf8(self.slice)?.parse::<T>().map_err(|e| PdfError::Parse { source: e.into() })
.parse::<T>()
.map_err(|e| PdfError::Parse { source: e.into() })
} }
pub fn is_integer(&self) -> bool { pub fn is_integer(&self) -> bool {
if self.slice.len() == 0 { if self.slice.len() == 0 {
@ -447,7 +433,7 @@ impl<'a> Substr<'a> {
let end = self.slice.len() - slice.len() + len; let end = self.slice.len() - slice.len() + len;
Some(Substr { Some(Substr {
file_offset: self.file_offset, file_offset: self.file_offset,
slice: &self.slice[..end], slice: &self.slice[..end]
}) })
} else { } else {
Some(*self) Some(*self)
@ -505,7 +491,6 @@ mod tests {
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
use std::io::{BufWriter, Write}; use std::io::{BufWriter, Write};
use std::u32::MAX;
#[test] #[test]
fn test_boundary_rev() { fn test_boundary_rev() {
@ -537,19 +522,14 @@ mod tests {
fn test_lexed() { fn test_lexed() {
let file_data = fs::read("/home/kschuettler/Dokumente/TestFiles/18 - EVIDIS - Corrosao Irritacao ocular aguda.pdf").expect("File not found!"); let file_data = fs::read("/home/kschuettler/Dokumente/TestFiles/18 - EVIDIS - Corrosao Irritacao ocular aguda.pdf").expect("File not found!");
println!("{}", file_data.len()); println!("{}", file_data.len());
let mut lexer = Lexer::new(&file_data[0..]); let mut lexer = Lexer::new(&*file_data);
let file = File::create("/tmp/pdf.txt").unwrap(); let file = File::create("/tmp/pdf.txt").unwrap();
let mut writer = BufWriter::new(file); let mut writer = BufWriter::new(file);
let mut depth = false; let mut depth = false;
let mut stream = false; let mut stream = false;
let mut dict = 0u32; let mut dict = 0;
let lex_count = MAX;
let mut lex_count_left = lex_count;
while let Ok(s) = lexer.next() { while let Ok(s) = lexer.next() {
if lex_count_left == 0 {
break;
}
if stream && s.to_string().as_str() == "endstream" { if stream && s.to_string().as_str() == "endstream" {
stream = false; stream = false;
writer writer
@ -559,7 +539,6 @@ mod tests {
} else if stream { } else if stream {
continue; continue;
} }
lex_count_left -= 1;
match s.to_string().as_str() { match s.to_string().as_str() {
"obj" => depth = true, "obj" => depth = true,

2
src-tauri/Cargo.lock generated
View File

@ -2767,9 +2767,7 @@ dependencies = [
name = "pdf-forge" name = "pdf-forge"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"lazy_static",
"pdf", "pdf",
"regex",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",

View File

@ -26,6 +26,4 @@ pdf = { path = "../src-pdfrs/pdf", features = ["cache"] }
tauri-plugin-fs = "2" tauri-plugin-fs = "2"
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
uuid = { version = "1.12.0", features = ["v4"] } uuid = { version = "1.12.0", features = ["v4"] }
regex = "1.10.3"
lazy_static = "1.4.0"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,18 +1,15 @@
#[cfg(test)]
mod tests;
extern crate pdf; extern crate pdf;
use crate::pdf::object::Resolve; use crate::pdf::object::Resolve;
use lazy_static::lazy_static; use pdf::content::Op;
use pdf::file::{File, FileOptions, NoLog, ObjectCache, StreamCache}; use pdf::file::{File, FileOptions, NoLog, ObjectCache, StreamCache};
use pdf::object::{Object, ObjectWrite, PlainRef, Stream}; use pdf::object::{InfoDict, Object, ObjectWrite, PlainRef};
use pdf::primitive::Primitive; use pdf::primitive::Primitive;
use pdf::xref::XRef; use pdf::xref::XRef;
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::fmt::format;
use std::ops::DerefMut; use std::ops::DerefMut;
use std::path::Path; use std::path::Path;
use std::sync::{Mutex, MutexGuard}; use std::sync::{Mutex, MutexGuard};
@ -51,6 +48,7 @@ pub struct PdfFile {
pub pages: Vec<PageModel>, pub pages: Vec<PageModel>,
} }
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub struct PrimitiveModel { pub struct PrimitiveModel {
pub key: String, pub key: String,
@ -58,25 +56,25 @@ pub struct PrimitiveModel {
pub sub_type: String, pub sub_type: String,
pub value: String, pub value: String,
pub children: Vec<PrimitiveModel>, pub children: Vec<PrimitiveModel>,
pub trace: Vec<PathTrace>, pub detail_path: Vec<DetailPathStep>,
} }
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub struct PathTrace { pub struct DetailPathStep {
pub key: String, pub key: String,
pub last_jump: String, pub last_jump: String,
} }
impl PathTrace { impl DetailPathStep {
fn new(key: String, last_jump: String) -> PathTrace { fn new(key: String, last_jump: String) -> DetailPathStep {
PathTrace { key, last_jump } DetailPathStep { key, last_jump }
} }
} }
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub struct PageModel { pub struct PageModel {
key: String, key: String,
obj_num: u64, id: u64,
page_num: u64,
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct TreeViewNode { pub struct TreeViewNode {
@ -85,14 +83,14 @@ pub struct TreeViewNode {
} }
impl TreeViewNode { impl TreeViewNode {
fn step(&self) -> Result<Step, String> { fn step(&self) -> Step {
Step::parse_step(&self.key) Step::parse_step(&self.key)
} }
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ContentsModel { pub struct ContentsModel {
parts: Vec<Vec<String>>, parts: Vec<Vec<String>>
} }
#[tauri::command] #[tauri::command]
@ -143,30 +141,22 @@ fn upload(path: &str, session: State<Mutex<Session>>) -> Result<String, String>
} }
fn to_pdf_file(path: &str, file: &CosFile) -> Result<PdfFile, String> { fn to_pdf_file(path: &str, file: &CosFile) -> Result<PdfFile, String> {
fn parse_title_from_path(path: &str) -> Option<String> { fn parse_title_from_path(path: &str) -> Option<String> {
Path::new(path) Path::new(path).file_name()
.file_name()
.and_then(|f| f.to_str().map(|s| s.to_string())) .and_then(|f| f.to_str().map(|s| s.to_string()))
} }
let file_name = if let Some(ref info) = file.trailer.info_dict { let file_name = if let Some(ref info) = file.trailer.info_dict {
info.title info.title.as_ref().map(|p| p.to_string_lossy())
.as_ref() .unwrap_or( parse_title_from_path(path)
.map(|p| p.to_string_lossy()) .unwrap_or_else(|| "Not found".to_string()))
.unwrap_or(parse_title_from_path(path).unwrap_or_else(|| "Not found".to_string()))
} else { } else {
"Not found".to_string() "Not found".to_string()
}; };
let pages = file
.pages() let pages = file.pages().enumerate().map(|(i, page_ref)| PageModel { key: format!("Page {}", i + 1), id: page_ref.unwrap().get_ref().get_inner().id }).collect();
.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 { let pdf_file = PdfFile {
id: Uuid::new_v4().to_string(), id: Uuid::new_v4().to_string(),
@ -180,17 +170,13 @@ fn to_pdf_file(path: &str, file: &CosFile) -> Result<PdfFile, String> {
} }
#[tauri::command] #[tauri::command]
fn get_contents( fn get_contents(id: &str, path: &str, session: State<Mutex<Session>>) -> Result<ContentsModel, String> {
id: &str,
path: &str,
session: State<Mutex<Session>>,
) -> Result<ContentsModel, String> {
let session_guard = session let session_guard = session
.lock() .lock()
.map_err(|_| "Failed to lock the session mutex.".to_string())?; .map_err(|_| "Failed to lock the session mutex.".to_string())?;
let file = get_file_from_state(id, &session_guard)?; let file = get_file_from_state(path, &session_guard)?;
let (_, page_prim, _) = get_prim_by_path_with_file(path, &file.cos_file)?; let (_, page_prim, _) = get_prim_by_path_with_file(id, &file.cos_file)?;
let resolver = file.cos_file.resolver(); let resolver = file.cos_file.resolver();
let page = t!(pdf::object::Page::from_primitive(page_prim, &resolver)); let page = t!(pdf::object::Page::from_primitive(page_prim, &resolver));
@ -201,40 +187,12 @@ fn get_contents(
let ops = t!(pdf::content::parse_ops(&data, &resolver)); let ops = t!(pdf::content::parse_ops(&data, &resolver));
let part = t!(pdf::content::display_ops(&ops)); let part = t!(pdf::content::display_ops(&ops));
parts.push(part); parts.push(part);
} };
return Ok(ContentsModel {parts}); return Ok(ContentsModel {parts});
} }
Err(String::from("Error occurred")) Err(String::from("Error occurred"))
} }
#[tauri::command]
fn get_stream_data(id: &str, path: &str, session: State<Mutex<Session>>) -> Result<String, String> {
let session_guard = session
.lock()
.map_err(|_| "Failed to lock the session mutex.".to_string())?;
let file = get_file_from_state(id, &session_guard)?;
get_stream_data_by_path_with_file(path, &file.cos_file)
}
fn get_stream_data_by_path_with_file(path: &str, file: &CosFile) -> Result<String, String> {
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));
};
let resolver = file.resolver();
let data = t!(t!(Stream::<Primitive>::from_stream(stream, &resolver)).data(&resolver));
Ok(String::from_utf8_lossy(&data).into_owned())
}
#[tauri::command] #[tauri::command]
fn get_prim_by_path( fn get_prim_by_path(
id: &str, id: &str,
@ -248,56 +206,44 @@ fn get_prim_by_path(
get_prim_model_by_path_with_file(path, &file.cos_file) 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> { fn get_prim_model_by_path_with_file(path: &str, file: &CosFile) -> Result<PrimitiveModel, String> {
let (step, prim, trace) = get_prim_by_path_with_file(path, file)?; let (key, prim, detail_path) = get_prim_by_path_with_file(path, file)?;
Ok(PrimitiveModel::from_primitive_with_children(&prim, trace)) Ok(PrimitiveModel::from_primitive_with_children(
key,
&prim,
detail_path
))
} }
fn get_prim_by_path_with_file( fn get_prim_by_path_with_file(path: &str, file: &CosFile) -> Result<(String, Primitive, Vec<DetailPathStep>), String> {
path: &str, let mut steps = Step::parse(path);
file: &CosFile,
) -> Result<(Step, Primitive, Vec<PathTrace>), String> {
get_prim_by_steps_with_file(Step::parse(path), file)
}
fn get_prim_by_steps_with_file(
mut steps: VecDeque<Step>,
file: &CosFile,
) -> Result<(Step, Primitive, Vec<PathTrace>), String> {
if steps.len() == 0 { if steps.len() == 0 {
return Err(String::from(format!("{:?} is not a valid path!", steps))); return Err(String::from(format!("{} is not a valid path!", path)));
} }
let step = steps.pop_front().unwrap(); let mut step = steps.pop_front().unwrap();
let (mut parent, trace) = resolve_parent(step.clone(), file)?; let mut parent = match step {
Step::Number(obj_num) => resolve_xref(obj_num, file)?,
Step::Trailer => retrieve_trailer(file),
_ => return Err(String::from(format!("{} is not a valid path!", path))),
};
let mut last_jump = trace.last_jump.clone(); let mut detail_path = vec![DetailPathStep::new(step.get_key(), step.get_key())];
let mut trace = vec![trace]; let mut last_jump = step.get_key();
let mut current_prim = &parent; let mut current_prim = &parent;
while !steps.is_empty() { while !steps.is_empty() {
let step = steps.pop_front().unwrap(); step = steps.pop_front().unwrap();
current_prim = resolve_step(&current_prim, &step)?; current_prim = resolve_step(&current_prim, &step)?;
if let Primitive::Reference(xref) = current_prim { if let Primitive::Reference(xref) = current_prim {
last_jump = xref.id.to_string(); last_jump = xref.id.to_string();
parent = resolve_p_ref(xref.clone(), file)?; parent = resolve_xref(xref.id, file)?;
current_prim = &parent; current_prim = &parent;
} }
trace.push(PathTrace::new(step.get_key(), last_jump.clone())); detail_path.push(DetailPathStep::new(step.get_key(), last_jump.clone()));
} }
Ok((step, current_prim.clone(), trace)) Ok((step.get_key(), current_prim.clone(), detail_path))
}
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())))
} }
#[tauri::command] #[tauri::command]
@ -318,12 +264,15 @@ fn get_prim_tree_by_path_with_file(
node: TreeViewNode, node: TreeViewNode,
file: &CosFile, file: &CosFile,
) -> Result<PrimitiveModel, String> { ) -> Result<PrimitiveModel, String> {
let step = node.step()?; let step = node.step();
let (parent, trace) = resolve_parent(step.clone(), file)?; let parent = match step {
Step::Number(obj_num) => resolve_xref(obj_num, file)?,
let path = vec![trace]; Step::Trailer => retrieve_trailer(file),
let mut parent_model = PrimitiveModel::from_primitive_with_children(&parent, path); _ => return Err(String::from(format!("{:?} is not a valid path!", node))),
};
let path = vec![DetailPathStep::new(step.get_key(), step.get_key())];
let mut parent_model = PrimitiveModel::from_primitive_with_children(step.get_key(), &parent, path);
for child in node.children.iter() { for child in node.children.iter() {
expand(child, &mut parent_model, &parent, file)?; expand(child, &mut parent_model, &parent, file)?;
} }
@ -337,20 +286,17 @@ fn expand(
parent: &Primitive, parent: &Primitive,
file: &CosFile, file: &CosFile,
) -> Result<(), String> { ) -> Result<(), String> {
let step = node.step()?; let step = node.step();
let prim = resolve_step(parent, &step)?; let prim = resolve_step(parent, &step)?;
if let Primitive::Reference(x_ref) = prim { if let Primitive::Reference(x_ref) = prim {
let jump = resolve_xref(x_ref.id, file)?; let jump = resolve_xref(x_ref.id, file)?;
// parent_model.ptype = format!("{}-Reference", jump.get_debug_name()); // parent_model.ptype = format!("{}-Reference", jump.get_debug_name());
let mut to_expand = parent_model.get_child(step.get_key()).unwrap(); let mut to_expand = parent_model.get_child(step.get_key()).unwrap();
to_expand.add_children( to_expand.add_children(&jump, append_path_with_jump(step.get_key(), x_ref.id.to_string(), &to_expand.detail_path));
&jump,
append_path_with_jump(step.get_key(), x_ref.id.to_string(), &to_expand.trace),
);
expand_children(node, file, &jump, &mut to_expand)?; expand_children(node, file, &jump, &mut to_expand)?;
} else { } else {
let mut to_expand = parent_model.get_child(step.get_key()).unwrap(); let mut to_expand = parent_model.get_child(step.get_key()).unwrap();
to_expand.add_children(prim, append_path(step.get_key(), &to_expand.trace)); to_expand.add_children(prim, append_path(step.get_key(), &to_expand.detail_path));
expand_children(node, file, prim, &mut to_expand)?; expand_children(node, file, prim, &mut to_expand)?;
} }
Ok(()) Ok(())
@ -415,6 +361,7 @@ fn resolve_step<'a>(current_prim: &'a Primitive, step: &Step) -> Result<&'a Prim
))) )))
} }
}, },
Step::Data => return Err("Not implemented!".to_string()),
_ => return Err(format!("Invalid Step: {}", step.get_key())), _ => return Err(format!("Invalid Step: {}", step.get_key())),
}) })
} }
@ -424,71 +371,53 @@ fn retrieve_trailer(file: &CosFile) -> Primitive {
file.trailer.to_primitive(&mut updater).unwrap() file.trailer.to_primitive(&mut updater).unwrap()
} }
fn retrieve_page(page_num: u32, file: &CosFile) -> Result<(Primitive, PathTrace), String> { #[derive(Debug)]
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()),
))
}
#[derive(Debug, PartialEq, Clone)]
pub enum Step { pub enum Step {
String(String), String(String),
Page(u32),
Number(u64), Number(u64),
Trailer, Trailer,
Data, Data,
} }
impl Step { impl Step {
fn parse_step(path: &str) -> Result<Step, String> { fn parse_step(path: &str) -> Step {
lazy_static! { match &path.parse::<u64>().ok() {
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), Some(i) => Step::Number(*i),
None => match &path[..] { None => match &path[..] {
"Data" => Step::Data, "Data" => Step::Data,
"Trailer" => Step::Trailer,
"/" => Step::Trailer, "/" => Step::Trailer,
_ => { _ => Step::String(path.to_string().clone()),
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> { fn parse(path: &str) -> VecDeque<Step> {
let mut steps = VecDeque::new(); let mut steps = VecDeque::new();
if path.starts_with("/") { if path.starts_with("/") {
steps.push_back(Step::Trailer); steps.push_back(Step::Trailer);
} }
let split_path = path.split("/").collect::<VecDeque<&str>>(); let split_path = path.split("/").collect::<VecDeque<&str>>();
split_path for path_component in split_path {
.iter() if path_component.len() == 0 {
.filter_map(|s| Step::parse_step(s).ok()) continue;
.collect::<VecDeque<Step>>() }
let step = match &path_component.parse::<u64>().ok() {
Some(i) => Step::Number(*i),
None => match path_component {
"Data" => Step::Data,
_ => Step::String(path_component.to_string().clone()),
},
};
steps.push_back(step);
}
steps
} }
fn get_key(&self) -> String { fn get_key(&self) -> String {
match self { match self {
Step::String(s) => s.clone(), Step::String(s) => s.clone(),
Step::Number(i) => i.to_string(), Step::Number(i) => i.to_string(),
Step::Trailer => "Trailer".to_string(), Step::Trailer => "/".to_string(),
Step::Page(n) => format!("Page{}", n),
Step::Data => "Data".into(), Step::Data => "Data".into(),
} }
} }
@ -496,10 +425,6 @@ impl Step {
fn resolve_xref(id: u64, file: &CosFile) -> Result<Primitive, String> { fn resolve_xref(id: u64, file: &CosFile) -> Result<Primitive, String> {
let plain_ref = PlainRef { id, gen: 0 }; let plain_ref = PlainRef { id, gen: 0 };
resolve_p_ref(plain_ref, file)
}
fn resolve_p_ref(plain_ref: PlainRef, file: &CosFile) -> Result<Primitive, String> {
file.resolver() file.resolver()
.resolve(plain_ref) .resolve(plain_ref)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
@ -515,27 +440,28 @@ fn get_file_from_state<'a>(
.ok_or_else(|| format!("File with id {} does not exist!", id)) .ok_or_else(|| format!("File with id {} does not exist!", id))
} }
fn append_path_with_jump(key: String, last_jump: String, path: &Vec<PathTrace>) -> Vec<PathTrace> { fn append_path_with_jump(key: String, last_jump: String, path: &Vec<DetailPathStep>) -> Vec<DetailPathStep> {
let mut new_path = path.clone(); let mut new_path = path.clone();
new_path.push(PathTrace::new(key, last_jump)); new_path.push(DetailPathStep::new(key, last_jump));
new_path new_path
} }
fn append_path(key: String, path: &Vec<PathTrace>) -> Vec<PathTrace> { fn append_path(key: String, path: &Vec<DetailPathStep>) -> Vec<DetailPathStep> {
let mut new_path = path.clone(); let mut new_path = path.clone();
let last_jump = new_path.last().unwrap().last_jump.clone(); let last_jump = new_path.last().unwrap().last_jump.clone();
new_path.push(PathTrace::new(key, last_jump)); new_path.push(DetailPathStep::new(key, last_jump));
new_path new_path
} }
impl PrimitiveModel { impl PrimitiveModel {
fn from_primitive(key: String, primitive: &Primitive, path: Vec<PathTrace>) -> PrimitiveModel { fn from_primitive(key: String, primitive: &Primitive, path: Vec<DetailPathStep>) -> PrimitiveModel {
let value: String = match primitive { let value: String = match primitive {
Primitive::Null => "Null".to_string(), Primitive::Null => "Null".to_string(),
Primitive::Integer(i) => i.to_string(), Primitive::Integer(i) => i.to_string(),
Primitive::Number(f) => f.to_string(), Primitive::Number(f) => f.to_string(),
Primitive::Boolean(b) => b.to_string(), Primitive::Boolean(b) => b.to_string(),
Primitive::String(s) => s.to_string_lossy(), Primitive::String(s) => s.to_string().unwrap_or(String::new()),
Primitive::Stream(_) => "-".to_string(), Primitive::Stream(_) => "-".to_string(),
Primitive::Dictionary(_) => "-".to_string(), Primitive::Dictionary(_) => "-".to_string(),
Primitive::Array(arr) =>PrimitiveModel::format_arr_content(arr), Primitive::Array(arr) =>PrimitiveModel::format_arr_content(arr),
@ -550,10 +476,10 @@ impl PrimitiveModel {
.get("Type") .get("Type")
.and_then(|value| match value { .and_then(|value| match value {
Primitive::Name(name) => Some(name.clone().as_str().to_string()), Primitive::Name(name) => Some(name.clone().as_str().to_string()),
_ => None, _ => None
}) })
.unwrap_or(String::from("-")), .unwrap_or(String::from("-")),
_ => String::from("-"), _ => String::from("-")
}; };
PrimitiveModel { PrimitiveModel {
key: key, key: key,
@ -561,7 +487,7 @@ impl PrimitiveModel {
sub_type: sub_type, sub_type: sub_type,
value: value, value: value,
children: Vec::new(), children: Vec::new(),
trace: path, detail_path: path,
} }
} }
@ -593,24 +519,16 @@ impl PrimitiveModel {
result result
} }
fn from_primitive_with_children(primitive: &Primitive, path: Vec<PathTrace>) -> PrimitiveModel { fn from_primitive_with_children(key: String, primitive: &Primitive, path: Vec<DetailPathStep>) -> PrimitiveModel {
let mut model = PrimitiveModel::from_primitive( let mut model = PrimitiveModel::from_primitive(key, primitive, path.clone());
path.last().unwrap().key.clone(),
primitive,
path.clone(),
);
model.add_children(primitive, path); model.add_children(primitive, path);
model model
} }
fn add_children(&mut self, primitive: &Primitive, path: Vec<PathTrace>) { fn add_children(&mut self, primitive: &Primitive, path: Vec<DetailPathStep>) {
match primitive { match primitive {
Primitive::Dictionary(dict) => dict.iter().for_each(|(name, value)| { Primitive::Dictionary(dict) => dict.iter().for_each(|(name, value)| {
self.add_child( self.add_child(name.clone().as_str().to_string(), value, append_path(name.clone().as_str().to_string(), &path));
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)| { Primitive::Array(arr) => arr.iter().enumerate().for_each(|(i, obj)| {
self.add_child(i.to_string(), obj, append_path(i.to_string(), &path)); self.add_child(i.to_string(), obj, append_path(i.to_string(), &path));
@ -622,26 +540,17 @@ impl PrimitiveModel {
sub_type: "-".to_string(), sub_type: "-".to_string(),
value: "".to_string(), value: "".to_string(),
children: vec![], children: vec![],
trace: append_path("Data".to_string(), &path), detail_path: append_path("Data".to_string(), &path),
}); });
stream.info.iter().for_each(|(name, value)| { stream.info.iter().for_each(|(name, value)| {
self.add_child( self.add_child(name.clone().as_str().to_string(), value, append_path(name.clone().as_str().to_string(), &path));
name.clone().as_str().to_string(),
value,
append_path(name.clone().as_str().to_string(), &path),
);
}) })
} }
_ => (), _ => (),
}; };
} }
fn add_child( fn add_child(&mut self, key: String, child: &Primitive, path: Vec<DetailPathStep>) -> &PrimitiveModel {
&mut self,
key: String,
child: &Primitive,
path: Vec<PathTrace>,
) -> &PrimitiveModel {
let child_model = Self::from_primitive(key, child, path); let child_model = Self::from_primitive(key, child, path);
self.children.push(child_model); self.children.push(child_model);
&self.children[self.children.len() - 1] &self.children[self.children.len() - 1]
@ -767,9 +676,9 @@ pub fn run() {
get_prim_by_path, get_prim_by_path,
get_prim_tree_by_path, get_prim_tree_by_path,
get_xref_table, get_xref_table,
get_contents, get_contents
get_stream_data
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -4,9 +4,7 @@ extern crate pdf;
mod tests { mod tests {
use crate::{ use crate::{
get_prim_by_path_with_file, get_prim_model_by_path_with_file, get_prim_by_path_with_file, get_prim_model_by_path_with_file, get_prim_tree_by_path_with_file, get_xref_table_model_with_file, to_pdf_file, DetailPathStep, PrimitiveModel, TreeViewNode
get_prim_tree_by_path_with_file, get_stream_data_by_path_with_file,
get_xref_table_model_with_file, to_pdf_file, PathTrace, PrimitiveModel, TreeViewNode,
}; };
use pdf::content::{display_ops, serialize_ops, Op}; use pdf::content::{display_ops, serialize_ops, Op};
@ -103,7 +101,7 @@ mod tests {
fn print_node(node: PrimitiveModel, depth: usize) { fn print_node(node: PrimitiveModel, depth: usize) {
let spaces = " ".repeat(depth); let spaces = " ".repeat(depth);
println!("{:?}", node.trace); println!("{:?}", node.detail_path);
println!("{}{} | {} | {}", spaces, node.key, node.ptype, node.value); println!("{}{} | {} | {}", spaces, node.key, node.ptype, node.value);
for child in node.children { for child in node.children {
print_node(child, depth + 1); print_node(child, depth + 1);
@ -115,17 +113,14 @@ mod tests {
FileOptions::cached().open(FILE_PATH).unwrap(), FileOptions::cached().open(FILE_PATH).unwrap(),
"Loading file" "Loading file"
); );
let mut file2 = timed!(FileOptions::uncached().storage(), "Loading storage"); let mut file2 = timed!(
FileOptions::uncached().storage(),
"Loading storage"
);
let trail = timed!(
file.trailer.to_primitive(&mut file2).unwrap(), let trail = timed!(file.trailer.to_primitive(&mut file2).unwrap(), "writing trailer");
"writing trailer" let trail_model = PrimitiveModel::from_primitive_with_children("Trailer".to_string(), &trail, vec![DetailPathStep::new("/".to_string(), "/".to_string())]);
);
let trail_model = PrimitiveModel::from_primitive_with_children(
"Trailer".to_string(),
&trail,
vec![PathTrace::new("/".to_string(), "/".to_string())],
);
print_node(trail_model, 5); print_node(trail_model, 5);
println!("{:?}", file.trailer.info_dict); println!("{:?}", file.trailer.info_dict);
} }
@ -144,6 +139,7 @@ mod tests {
} }
#[test] #[test]
fn test_read_contents() { fn test_read_contents() {
let file = timed!( let file = timed!(
FileOptions::cached().open(FILE_PATH).unwrap(), FileOptions::cached().open(FILE_PATH).unwrap(),
"Loading file" "Loading file"
@ -152,10 +148,7 @@ mod tests {
let (_, page2_prim, _) = get_prim_by_path_with_file("1", &file).unwrap(); let (_, page2_prim, _) = get_prim_by_path_with_file("1", &file).unwrap();
let resolver = file.resolver(); let resolver = file.resolver();
let page2 = Page::from_primitive(page2_prim, &resolver).unwrap(); let page2 = Page::from_primitive(page2_prim, &resolver).unwrap();
let mut ops: Vec<Op> = timed!( let mut ops: Vec<Op> = timed!(page2.contents.unwrap().operations(&resolver).unwrap(), "parse ops");
page2.contents.unwrap().operations(&resolver).unwrap(),
"parse ops"
);
let serialized = timed!(serialize_ops(&mut ops).unwrap(), "serializing"); let serialized = timed!(serialize_ops(&mut ops).unwrap(), "serializing");
let display = timed!(display_ops(&mut ops).unwrap(), "displaying"); let display = timed!(display_ops(&mut ops).unwrap(), "displaying");
println!("Serialized -----------------------------------------------------------------"); println!("Serialized -----------------------------------------------------------------");
@ -164,22 +157,6 @@ mod tests {
for (line, s) in display.iter().enumerate() { for (line, s) in display.iter().enumerate() {
println!("{}: {}", line, s); println!("{}: {}", line, s);
} }
}
#[test]
fn test_read_stream() {
let file = timed!(
FileOptions::cached().open(FILE_PATH).unwrap(),
"Loading file"
);
let prim = timed!(
get_prim_model_by_path_with_file("1/Contents/1", &file).unwrap(),
"get prim"
);
print_node(prim, 3);
let content1 = timed!(
get_stream_data_by_path_with_file("1/Contents/1/Data", &file).unwrap(),
"get content 1"
);
println!("{}", content1);
} }
} }

View File

@ -19,14 +19,14 @@ body {
color: var(--font-color); color: var(--font-color);
border-color: var(--secondary-color) border-color: var(--secondary-color)
} }
::before, ::after {
::before,
::after {
border-color: var(--secondary-color); border-color: var(--secondary-color);
} }
.full-container { .full-container {
position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import WelcomeScreen from "./WelcomeScreen.svelte"; import WelcomeScreen from "./WelcomeScreen.svelte";
import { Pane, Splitpanes } from "svelte-splitpanes"; import {Pane, Splitpanes} from 'svelte-splitpanes';
import {open} from "@tauri-apps/plugin-dialog"; import {open} from "@tauri-apps/plugin-dialog";
import {invoke} from "@tauri-apps/api/core"; import {invoke} from "@tauri-apps/api/core";
import type PdfFile from "../models/PdfFile"; import type PdfFile from "../models/PdfFile";
@ -14,7 +14,6 @@
const footerHeight: number = 30; const footerHeight: number = 30;
const titleBarHeight: number = 30; const titleBarHeight: number = 30;
const tabBarHeight: number = 30;
let files: PdfFile[] = $state([]); let files: PdfFile[] = $state([]);
let innerHeight: number = $state(1060); let innerHeight: number = $state(1060);
let errorMessage: string = $state(""); let errorMessage: string = $state("");
@ -22,27 +21,23 @@
let xrefTableShowing: boolean = $state(false); let xrefTableShowing: boolean = $state(false);
let treeShowing: boolean = $state(true); let treeShowing: boolean = $state(true);
let pagesShowing: boolean = $state(false); let pagesShowing: boolean = $state(false);
let fileViewHeight: number = $derived( let fileViewHeight: number = $derived(Math.max(innerHeight - footerHeight - titleBarHeight, 0));
Math.max(innerHeight - footerHeight - tabBarHeight - titleBarHeight, 0),
);
let fStates: Map<string, FileViewState> = new Map<string, FileViewState>(); let fStates: Map<string, FileViewState> = new Map<string, FileViewState>();
let fState: FileViewState | undefined = $state(); let fState: FileViewState | undefined = $state();
let selected_file = $derived(fState ? fState.file : undefined); let selected_file = $derived(fState ? fState.file : undefined);
initialLoadAllFiles(); initialLoadAllFiles()
function initialLoadAllFiles() { function initialLoadAllFiles() {
invoke<PdfFile[]>("get_all_files") invoke<PdfFile[]>("get_all_files").then(result_list => {
.then((result_list) => {
files = result_list; files = result_list;
createFileStates(files); createFileStates(files);
if (files.length > 0) { if (files.length > 0) {
selectFile(files[files.length - 1]); selectFile(files[files.length - 1])
} }
}) }).catch(error => {
.catch((error) => { errorMessage = error
errorMessage = error; console.error("File retrieval failed: " + error)
console.error("File retrieval failed: " + error);
}); });
} }
@ -51,7 +46,7 @@
if (fStates.has(file.id)) { if (fStates.has(file.id)) {
continue; continue;
} }
let fState = new FileViewState(file); let fState = new FileViewState(file)
fStates.set(file.id, fState); fStates.set(file.id, fState);
} }
} }
@ -60,31 +55,27 @@
let file_path = await open({ let file_path = await open({
multiple: false, multiple: false,
directory: false, directory: false,
filters: [{ name: "pdf", extensions: ["pdf"] }], filters: [{name: 'pdf', extensions: ['pdf']}]
}); })
if (file_path === null || Array.isArray(file_path)) { if (file_path === null || Array.isArray(file_path)) {
return; return;
} }
invoke<String>("upload", { path: file_path }) invoke<String>("upload", {path: file_path}).then(result => {
.then((result) => { invoke<PdfFile[]>("get_all_files").then(result_list => {
invoke<PdfFile[]>("get_all_files")
.then((result_list) => {
files = result_list; files = result_list;
createFileStates(files); createFileStates(files);
let file = files.find((file) => file.id === result); let file = files.find(file => file.id === result);
if (file) { if (file) {
selectFile(file); selectFile(file);
} }
}) }).catch(error => {
.catch((error) => { errorMessage = error
errorMessage = error; console.error("Fetch all files failed with: " + error)
console.error("Fetch all files failed with: " + error);
}); });
}) }).catch(error => {
.catch((error) => { errorMessage = error
errorMessage = error; console.error("File upload failed with: " + error)
console.error("File upload failed with: " + error);
}); });
} }
@ -93,44 +84,29 @@
} }
function closeFile(file: PdfFile) { function closeFile(file: PdfFile) {
invoke("close_file", { id: file.id }) invoke("close_file", {id: file.id}).then(_ => {
.then((_) => { files = files.filter(f => f.id != file.id)
files = files.filter((f) => f.id != file.id);
if (file === selected_file) { if (file === selected_file) {
fState = undefined; fState = undefined;
} }
}) }).catch(err => console.error(err));
.catch((err) => console.error(err));
} }
</script>
</script>
<svelte:window bind:innerHeight/> <svelte:window bind:innerHeight/>
<main style="height: {innerHeight}px; overflow: hidden;"> <main style="height: {innerHeight}px">
<div style="height: {titleBarHeight}px"> <div style="height: {titleBarHeight}px">
<TitleBar></TitleBar> <TitleBar></TitleBar>
</div> </div>
<div class="fileview_container" style="height: {fileViewHeight + 30}px"> <div style="height: {fileViewHeight}px">
<Splitpanes theme="forge-movable" dblClickSplitter={false}> <Splitpanes theme="forge-movable" dblClickSplitter={false}>
<Pane size={2.5} minSize={1.5} maxSize={4}> <Pane size={2.5} minSize={1.5} maxSize={4}>
<ToolbarLeft bind:tree={treeShowing} bind:pages={pagesShowing} <ToolbarLeft bind:tree={treeShowing} bind:pages={pagesShowing}></ToolbarLeft>
></ToolbarLeft>
</Pane> </Pane>
<Pane> <Pane>
<TabBar <TabBar {files} {selected_file} closeTab={closeFile} openTab={upload} selectTab={selectFile}></TabBar>
{files} {#if (fState)}
{selected_file} <FileView {treeShowing} {xrefTableShowing} {pagesShowing} {fState} height={fileViewHeight}></FileView>
closeTab={closeFile}
openTab={upload}
selectTab={selectFile}
></TabBar>
{#if fState}
<FileView
{treeShowing}
{xrefTableShowing}
{pagesShowing}
{fState}
height={fileViewHeight}
></FileView>
{:else} {:else}
<WelcomeScreen {upload}></WelcomeScreen> <WelcomeScreen {upload}></WelcomeScreen>
{/if} {/if}
@ -143,6 +119,7 @@
<Footer {fState} {footerHeight} ></Footer> <Footer {fState} {footerHeight} ></Footer>
</main> </main>
<style global> <style global>
/* Resiable layout */ /* Resiable layout */
:global(.splitpanes.forge-movable) :global(.splitpanes__pane) { :global(.splitpanes.forge-movable) :global(.splitpanes__pane) {
@ -156,8 +133,7 @@
flex-shrink: 0; flex-shrink: 0;
} }
:global(.splitpanes.forge-movable) :global(.splitpanes__splitter:before), :global(.splitpanes.forge-movable) :global(.splitpanes__splitter:before), :global(.splitpanes.forge-movable) :global(.splitpanes__splitter:after) {
:global(.splitpanes.forge-movable) :global(.splitpanes__splitter:after) {
content: ""; content: "";
position: absolute; position: absolute;
top: 50%; top: 50%;
@ -167,103 +143,61 @@
transition: background-color 0.3s; transition: background-color 0.3s;
} }
:global(.splitpanes.forge-movable) :global(.splitpanes.forge-movable) :global(.splitpanes__splitter:hover:before), :global(.splitpanes.forge-movable) :global(.splitpanes__splitter:hover:after) {
:global(.splitpanes__splitter:hover:before),
:global(.splitpanes.forge-movable)
:global(.splitpanes__splitter:hover:after) {
background-color: var(--boundary-color); background-color: var(--boundary-color);
} }
:global(.splitpanes.forge-movable) :global(.splitpanes.forge-movable) :global(.splitpanes__splitter:first-child) {
:global(.splitpanes__splitter:first-child) {
cursor: auto; cursor: auto;
} }
:global(.forge-movable.splitpanes) :global(.forge-movable.splitpanes) :global(.splitpanes) :global(.splitpanes__splitter) {
:global(.splitpanes)
:global(.splitpanes__splitter) {
z-index: 1; z-index: 1;
} }
:global(.forge-movable.splitpanes--vertical) :global(.forge-movable.splitpanes--vertical) > :global(.splitpanes__splitter),
> :global(.splitpanes__splitter), :global(.forge-movable) :global(.splitpanes--vertical) > :global(.splitpanes__splitter) {
:global(.forge-movable)
:global(.splitpanes--vertical)
> :global(.splitpanes__splitter) {
width: 2px; width: 2px;
border-left: 1px solid var(--boundary-color); border-left: 1px solid var(--boundary-color);
cursor: col-resize; cursor: col-resize;
} }
:global(.forge-movable.splitpanes--vertical) :global(.forge-movable.splitpanes--vertical) > :global(.splitpanes__splitter:before), :global(.forge-movable.splitpanes--vertical) > :global(.splitpanes__splitter:after), :global(.forge-movable) :global(.splitpanes--vertical) > :global(.splitpanes__splitter:before), :global(.forge-movable) :global(.splitpanes--vertical) > :global(.splitpanes__splitter:after) {
> :global(.splitpanes__splitter:before),
:global(.forge-movable.splitpanes--vertical)
> :global(.splitpanes__splitter:after),
:global(.forge-movable)
:global(.splitpanes--vertical)
> :global(.splitpanes__splitter:before),
:global(.forge-movable)
:global(.splitpanes--vertical)
> :global(.splitpanes__splitter:after) {
transform: translateY(-50%); transform: translateY(-50%);
width: 1px; width: 1px;
height: 40px; height: 40px;
} }
:global(.forge-movable.splitpanes--vertical) :global(.forge-movable.splitpanes--vertical) > :global(.splitpanes__splitter:before),
> :global(.splitpanes__splitter:before), :global(.forge-movable) :global(.splitpanes--vertical) > :global(.splitpanes__splitter:before) {
:global(.forge-movable)
:global(.splitpanes--vertical)
> :global(.splitpanes__splitter:before) {
margin-left: -2px; margin-left: -2px;
} }
:global(.forge-movable.splitpanes--vertical) :global(.forge-movable.splitpanes--vertical) > :global(.splitpanes__splitter:after),
> :global(.splitpanes__splitter:after), :global(.forge-movable) :global(.splitpanes--vertical) > :global(.splitpanes__splitter:after) {
:global(.forge-movable)
:global(.splitpanes--vertical)
> :global(.splitpanes__splitter:after) {
margin-left: 1px; margin-left: 1px;
} }
:global(.forge-movable.splitpanes--horizontal) :global(.forge-movable.splitpanes--horizontal) > :global(.splitpanes__splitter),
> :global(.splitpanes__splitter), :global(.forge-movable) :global(.splitpanes--horizontal) > :global(.splitpanes__splitter) {
:global(.forge-movable)
:global(.splitpanes--horizontal)
> :global(.splitpanes__splitter) {
height: 2px; height: 2px;
border-top: 1px solid var(--boundary-color); border-top: 1px solid var(--boundary-color);
cursor: row-resize; cursor: row-resize;
} }
:global(.forge-movable.splitpanes--horizontal) :global(.forge-movable.splitpanes--horizontal) > :global(.splitpanes__splitter:before), :global(.forge-movable.splitpanes--horizontal) > :global(.splitpanes__splitter:after), :global(.forge-movable) :global(.splitpanes--horizontal) > :global(.splitpanes__splitter:before), :global(.forge-movable) :global(.splitpanes--horizontal) > :global(.splitpanes__splitter:after) {
> :global(.splitpanes__splitter:before),
:global(.forge-movable.splitpanes--horizontal)
> :global(.splitpanes__splitter:after),
:global(.forge-movable)
:global(.splitpanes--horizontal)
> :global(.splitpanes__splitter:before),
:global(.forge-movable)
:global(.splitpanes--horizontal)
> :global(.splitpanes__splitter:after) {
transform: translateX(-50%); transform: translateX(-50%);
width: 40px; width: 40px;
height: 3px; height: 3px;
} }
:global(.forge-movable.splitpanes--horizontal) :global(.forge-movable.splitpanes--horizontal) > :global(.splitpanes__splitter:before),
> :global(.splitpanes__splitter:before), :global(.forge-movable) :global(.splitpanes--horizontal) > :global(.splitpanes__splitter:before) {
:global(.forge-movable)
:global(.splitpanes--horizontal)
> :global(.splitpanes__splitter:before) {
margin-top: -2px; margin-top: -2px;
} }
:global(.forge-movable.splitpanes--horizontal) :global(.forge-movable.splitpanes--horizontal) > :global(.splitpanes__splitter:after),
> :global(.splitpanes__splitter:after), :global(.forge-movable) :global(.splitpanes--horizontal) > :global(.splitpanes__splitter:after) {
:global(.forge-movable)
:global(.splitpanes--horizontal)
> :global(.splitpanes__splitter:after) {
margin-top: 1px; margin-top: 1px;
} }
@ -277,6 +211,7 @@
position: relative; position: relative;
} }
:global(div.splitpanes--horizontal.splitpanes--dragging) { :global(div.splitpanes--horizontal.splitpanes--dragging) {
cursor: row-resize; cursor: row-resize;
} }

View File

@ -1,62 +1,36 @@
<script lang="ts"> <script lang="ts">
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import type ContentModel from "../models/ContentModel.svelte"; import type ContentModel from "../models/ContentModel.svelte";
import { onMount } from "svelte";
import * as monaco from "monaco-editor";
import type FileViewState from "../models/FileViewState.svelte";
let { fState, height }: { fState: FileViewState; height: number } = let {id, path, h}: {id: string, path: string, h: number} = $props();
$props();
let h = $derived(height);
let path = $derived(fState.prim?.getFirstJump()?.toString());
let id = $derived(fState.file.id);
let contents: ContentModel | undefined = $state(undefined); let contents: ContentModel | undefined = $state(undefined);
let editorContainer: HTMLElement;
let editor: monaco.editor.IStandaloneCodeEditor;
onMount(() => {
editor = monaco.editor.create(editorContainer, {
value: "",
language: "plaintext",
theme: "vs-dark",
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
automaticLayout: true,
});
return () => {
editor.dispose();
};
});
$effect( () => { $effect( () => {
loadContents(path, id); loadContents(path, id)
});
$inspect(contents);
function loadContents(path: string | undefined, id: string) {
console.log("Loading contents for", path, id);
if (!path || !id) return;
invoke<ContentModel>("get_contents", { id, path })
.then((result) => {
console.log("Contents loaded", result);
contents = result;
if (contents && editor) {
const text = contents.parts
.map((part) => part.join("\n"))
.join(
"\n\n%-------------------% EOF %-------------------%\n\n",
);
editor.setValue(text);
}
}) })
.catch((err) => console.error(err));
function loadContents(id: string, path: string) {
invoke<ContentModel>("get_contents", {id: id, path: path})
.then(result => contents = result)
.catch(err => console.error(err));
} }
</script> </script>
{#if contents}
<div class="overflow-auto">
<div class="whitespace-nowrap" style="height: {h}px">
{#each contents.parts as part }
<div class="part">
{#each part as line}
<div class="line">{line}</div>
{/each}
<div bind:this={editorContainer} style="height: {h}px; width: 100%;"></div> </div>
{/each}
</div>
</div>
{:else}
{"Loading id: " + id + " Path: " + path}
{/if}
<style lang="postcss"> <style lang="postcss">
</style> </style>

View File

@ -4,34 +4,20 @@
import PrimitiveView from "./PrimitiveView.svelte"; import PrimitiveView from "./PrimitiveView.svelte";
import TreeView from "./TreeView.svelte"; import TreeView from "./TreeView.svelte";
import type FileViewState from "../models/FileViewState.svelte"; import type FileViewState from "../models/FileViewState.svelte";
import { createEventDispatcher, onMount } from "svelte"; import {onMount} from 'svelte';
import PageList from "./PageList.svelte"; import PageList from "./PageList.svelte";
import ContentsView from "./ContentsView.svelte";
import TreeViewNode from "../models/TreeViewNode.svelte";
import TreeViewState from "../models/TreeViewState.svelte";
import type { PathSelectedEvent } from "../events/PathSelectedEvent";
let { let {treeShowing, xrefTableShowing, pagesShowing, fState, height}: {
treeShowing, treeShowing: boolean,
xrefTableShowing, xrefTableShowing: boolean,
pagesShowing, pagesShowing: boolean,
fState, fState: FileViewState,
height, height: number
}: { } = $props()
treeShowing: boolean;
xrefTableShowing: boolean;
pagesShowing: boolean;
fState: FileViewState;
height: number;
} = $props();
let width: number = $state(0);
let xRefTableWidth = $derived(xrefTableShowing ? 281 : 0);
let splitPanesWidth: number = $derived(width - xRefTableWidth);
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
// Check for "Alt + Left Arrow" // Check for "Alt + Left Arrow"
if (event.altKey && event.key === "ArrowLeft") { if (event.altKey && event.key === 'ArrowLeft') {
fState.popPath(); fState.popPath();
} }
} }
@ -44,87 +30,39 @@
} }
onMount(() => { onMount(() => {
window.addEventListener("keydown", handleKeydown); window.addEventListener('keydown', handleKeydown);
window.addEventListener("mousedown", handleMouseButton); window.addEventListener('mousedown', handleMouseButton);
return () => { return () => {
window.removeEventListener("keydown", handleKeydown); window.removeEventListener('keydown', handleKeydown);
window.removeEventListener("mousedown", handleMouseButton); window.removeEventListener('mousedown', handleMouseButton);
}; };
}); });
function pathSelectedHandler(event: PathSelectedEvent) {
console.log("Path selected", event.detail.path);
fState.selectPath(event.detail.path);
}
</script> </script>
<div bind:clientWidth={width} class="file-view-container"> <Splitpanes theme="forge-movable">
<div <Pane size={treeShowing || pagesShowing ? 15 : 0} minSize={treeShowing || pagesShowing ? 1 : 0} maxSize={treeShowing || pagesShowing ? 100 : 0}>
class="flex flex-row" <Splitpanes theme="forge-movable" horizontal>
style="width: {splitPanesWidth}px; height: {height}px" <Pane size={treeShowing ? pagesShowing ? 50 : 100 : 0} minSize={treeShowing ? 2 : 0} maxSize={treeShowing ? 100 : 0}>
> <TreeView {fState}></TreeView>
<Splitpanes theme="forge-movable" pushOtherPanes={false}>
<Pane
size={treeShowing || pagesShowing ? 15 : 0}
minSize={treeShowing || pagesShowing ? 2 : 0}
maxSize={treeShowing || pagesShowing ? 100 : 0}
>
{#if pagesShowing}
<PageList {fState} h={height}></PageList>
{:else if treeShowing}
<TreeView
bind:pathSelectedHandler
file_id={fState.file.id}
root={TreeViewNode.TRAILER}
></TreeView>
{/if}
</Pane> </Pane>
<Pane size={pagesShowing ? treeShowing ? 50 : 100 : 0} minSize={pagesShowing ? 2 : 0} maxSize={pagesShowing ? 100 : 0}>
<PageList {fState}></PageList>
</Pane>
</Splitpanes>
</Pane>
<Pane minSize={1}> <Pane minSize={1}>
<div> <div>
<PrimitiveView {fState} {height}></PrimitiveView> <PrimitiveView {fState} {height}></PrimitiveView>
</div> </div>
</Pane> </Pane>
{#if fState.prim?.isPage()}
<Pane minSize={1}> <Pane size={xrefTableShowing ? 16 : 0} minSize={xrefTableShowing ? 1 : 0} maxSize={xrefTableShowing ? 100 : 0}>
<div class="overflow-hidden"> <XRefTable {fState}></XRefTable>
<ContentsView {fState} {height}></ContentsView>
</div>
</Pane> </Pane>
{/if}
</Splitpanes> </Splitpanes>
</div>
{#if xrefTableShowing}
<div class="xref-modal" class:visible={xrefTableShowing}>
<XRefTable {fState} {height}></XRefTable>
</div>
{/if}
</div>
<style lang="postcss"> <style lang="postcss">
.file-view-container {
position: relative;
height: 100%;
width: 100%;
}
.xref-modal {
position: absolute;
border-left: 1px solid var(--border-color);
top: 0;
right: 0;
bottom: 0;
width: 280px;
background: var(--background-color);
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
z-index: 1000;
overflow-y: auto;
transform: translateX(100%);
transition: transform 0.2s ease-in-out;
}
.xref-modal.visible {
transform: translateX(0);
}
</style> </style>

View File

@ -1,18 +1,15 @@
<script lang="ts"> <script lang="ts">
import type FileViewState from "../models/FileViewState.svelte"; import type FileViewState from "../models/FileViewState.svelte";
import type { Trace } from "../models/Primitive.svelte"; import type { DetailPath } from "../models/Primitive.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte"; import PrimitiveIcon from "./PrimitiveIcon.svelte";
import { CaretRightOutline } from "flowbite-svelte-icons";
class Path { class Path {
public value: string; public value: string;
public jump?: string; public jump?: string;
public index: number;
constructor(value: string, index: number, jump?: string) { constructor(value: string, jump?: string) {
this.value = value; this.value = value;
this.jump = jump; this.jump = jump;
this.index = index;
} }
} }
@ -21,59 +18,57 @@
footerHeight, footerHeight,
}: { fState: FileViewState | undefined; footerHeight: number } = $props(); }: { fState: FileViewState | undefined; footerHeight: number } = $props();
let elements: Path[] | undefined = $derived( let elements: Path[] | undefined = $derived(
fState && fState.prim ? toElements(fState.prim.trace) : undefined, fState && fState.prim ? toElements(fState.prim.detail_path) : undefined,
); );
function toElements(path: Trace[]): Path[] { $inspect(elements);
$inspect(fState?.prim?.detail_path);
function toElements(path: DetailPath[]): Path[] {
if (path.length == 0) { if (path.length == 0) {
return [new Path("Trailer", 0)]; return [{ value: "Trailer" } as Path];
} }
let display: Path[] = []; let display: Path[] = [];
let lastJump: string | undefined; if (path[0].key === "/") {
for (let i = 0; i < path.length; i++) { display.push(new Path("Trailer"));
const pathElement = path[i]; } else {
display.push(new Path(path[0].key, path[0].last_jump));
}
let lastJump = path[0].last_jump;
for (const pathElement of path.slice(1, path.length)) {
if (pathElement.last_jump !== lastJump) { if (pathElement.last_jump !== lastJump) {
lastJump = pathElement.last_jump; lastJump = pathElement.last_jump;
display.push(new Path(pathElement.key, i, lastJump)); display.push(new Path(pathElement.key, lastJump));
} else { } else {
display.push(new Path(pathElement.key, i, undefined)); display.push(new Path(pathElement.key, undefined));
} }
} }
return display; return display;
} }
function selectPath(path: Path) {
if (fState) {
let newPath = fState.copyPath().slice(0, path.index + 1);
fState.selectPath(newPath);
}
}
</script> </script>
<div class="footer" style="height: {footerHeight}px"> <div
class="bg-forge-prim border border-forge-bound"
style="height: {footerHeight}px"
>
<div class="m-1 flex flex-row">
{#if elements} {#if elements}
{#each elements as path} {#each elements as path}
<button <button class="m-1 flex flex-row mt-auto mb-auto">
class="flex flex-row items-center ml-2"
onclick={() => selectPath(path)}
>
{#if path.jump} {#if path.jump}
<div class="m-auto" style="height: {footerHeight}px">
<PrimitiveIcon ptype={"Reference"}></PrimitiveIcon> <PrimitiveIcon ptype={"Reference"}></PrimitiveIcon>
{:else} </div>
<CaretRightOutline class="text-forge-sec" />
{/if} {/if}
<p class="text-xs ml-1">{path.value}</p> <p class="text-xs ml-1 c">{path.value}</p>
</button> </button>
{/each} {/each}
{/if} {/if}
</div> </div>
</div>
<style lang="postcss"> <style lang="postcss">
.footer {
@apply bg-forge-prim border border-forge-bound flex flex-row items-center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
</style> </style>

View File

@ -3,16 +3,19 @@
import PrimitiveIcon from "./PrimitiveIcon.svelte"; import PrimitiveIcon from "./PrimitiveIcon.svelte";
import type PageModel from "../models/PageModel"; import type PageModel from "../models/PageModel";
let { fState, h }: { fState: FileViewState; h: number } = $props(); let {fState}: { fState: FileViewState } = $props();
let h: number = $state(100);
let selected: PageModel | undefined = $state(undefined); let selected: PageModel | undefined = $state(undefined);
function handlePageSelect(page: PageModel) { function handlePageSelect(page: PageModel) {
selected = page; selected = page
fState.selectPath(["Page" + page.page_num]); fState.selectPath([page.id]);
} }
</script> </script>
<div bind:clientHeight={h} class="full-container">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<div class="w-[251px]">
<table> <table>
<thead> <thead>
<tr> <tr>
@ -21,15 +24,11 @@
</tr> </tr>
</thead> </thead>
</table> </table>
<div class="overflow-y-auto" style="height: {h - 25}px"> <div class="overflow-y-auto" style="height: {h-55}px">
<table> <table>
<tbody> <tbody>
{#each fState.file.pages as page} {#each fState.file.pages as page}
<tr <tr class:selected={page === selected} class="hover:bg-forge-sec" ondblclick={() => handlePageSelect(page)}>
class:selected={page === selected}
class="hover:bg-forge-sec"
ondblclick={() => handlePageSelect(page)}
>
<td class="page-cell t-data"> <td class="page-cell t-data">
<div class="key-field"> <div class="key-field">
<PrimitiveIcon ptype={"Reference"}/> <PrimitiveIcon ptype={"Reference"}/>
@ -38,20 +37,21 @@
</p> </p>
</div> </div>
</td> </td>
<td class="ref-cell t-data">{page.obj_num}</td> <td class="ref-cell t-data">{page.id}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div>
</div>
<style lang="postcss"> <style lang="postcss">
.key-field { .key-field {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 3px; gap: 3px
} }
.selected { .selected {
@ -64,7 +64,7 @@
} }
.t-data { .t-data {
@apply border border-forge-sec text-xs text-forge-text text-left; @apply border border-forge-sec text-sm text-forge-text text-left;
text-align: left; text-align: left;
cursor: default; cursor: default;
} }
@ -78,4 +78,5 @@
@apply min-w-[150px] w-[150px] min-h-[20px] h-[20px] p-1 m-0; @apply min-w-[150px] w-[150px] min-h-[20px] h-[20px] p-1 m-0;
user-select: none; user-select: none;
} }
</style> </style>

View File

@ -6,10 +6,10 @@
let { fState, height }: { fState: FileViewState; height: number } = let { fState, height }: { fState: FileViewState; height: number } =
$props(); $props();
let bodyHeight = $derived(height - 24); let bodyHeight = $derived(height - 54);
let selected: Primitive | undefined = $state(undefined);
let prim = $derived(fState.prim); let prim = $derived(fState.prim);
let showContents = $state(false); let showContents = $state(false);
$inspect(fState.highlightedPrim);
function handlePrimSelect(prim: Primitive) { function handlePrimSelect(prim: Primitive) {
if (prim.isContainer()) { if (prim.isContainer()) {
if (!prim) { if (!prim) {
@ -30,10 +30,8 @@
<table> <table>
<thead> <thead>
<tr> <tr>
<td class="page-cell t-header border-forge-prim">Key</td <td class="page-cell t-header border-forge-prim">Key</td>
> <td class="ref-cell t-header border-forge-prim">Type</td>
<td class="ref-cell t-header border-forge-prim">Type</td
>
<td class="cell t-header border-forge-sec">Value</td> <td class="cell t-header border-forge-sec">Value</td>
</tr> </tr>
</thead> </thead>
@ -43,10 +41,9 @@
<tbody> <tbody>
{#each prim.children as entry} {#each prim.children as entry}
<tr <tr
class:selected={entry.key === class:selected={entry === selected}
fState.highlightedPrim?.key}
class="hover:bg-forge-sec" class="hover:bg-forge-sec"
onclick={() => (fState.highlightedPrim = entry)} onclick={() => (selected = entry)}
ondblclick={() => handlePrimSelect(entry)} ondblclick={() => handlePrimSelect(entry)}
> >
<td class="page-cell t-data"> <td class="page-cell t-data">
@ -63,6 +60,21 @@
{/each} {/each}
</tbody> </tbody>
</table> </table>
<p class="flex flex-row">
Dict Type: {prim.sub_type}
</p>
{#if prim.isPage()}
<button onclick={() => (showContents = true)}
>View contents</button
>
{#if showContents}
<ContentsView
id={fState.file.id}
path={prim.getLastJump().toString()}
h={500}
></ContentsView>
{/if}
{/if}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,41 +1,25 @@
<script lang="ts"> <script lang="ts">
import {BookOpenSolid, CodeBranchSolid} from "flowbite-svelte-icons"; import {BookOpenSolid, CodeBranchSolid} from "flowbite-svelte-icons";
let { let {tree = $bindable(false), pages = $bindable(false)}: { tree: boolean, pages: boolean } = $props();
tree = $bindable(false),
pages = $bindable(false),
}: { tree: boolean; pages: boolean } = $props();
function toggleTree() { function toggleTree() {
tree = !tree; tree = !tree;
if (tree) {
pages = false;
}
} }
function togglePages() { function togglePages() {
pages = !pages; pages = !pages;
if (pages) {
tree = false;
}
} }
</script> </script>
<div class="grid grid-cols-1"> <div class="grid grid-cols-1">
<button <button class={ tree ? "tool-button active" : "tool-button" } onclick={toggleTree} >
class={tree ? "tool-button active" : "tool-button"}
onclick={toggleTree}
>
<div class="justify-center flex m-0"> <div class="justify-center flex m-0">
<CodeBranchSolid class="rotate-180 scale-x-[-1]"/> <CodeBranchSolid class="rotate-180 scale-x-[-1]"/>
</div> </div>
<b class="button-title">Tree</b> <b class="button-title">Tree</b>
</button> </button>
<button <button id="#page" class={ pages ? "tool-button active" : "tool-button" } onclick={togglePages}>
id="#page"
class={pages ? "tool-button active" : "tool-button"}
onclick={togglePages}
>
<div class="justify-center flex "> <div class="justify-center flex ">
<BookOpenSolid/> <BookOpenSolid/>
</div> </div>
@ -43,6 +27,7 @@
</button> </button>
</div> </div>
<style lang="postcss"> <style lang="postcss">
.button-title { .button-title {
@apply text-forge-text_sec text-xs; @apply text-forge-text_sec text-xs;

View File

@ -2,21 +2,12 @@
import type Primitive from "../models/Primitive.svelte"; import type Primitive from "../models/Primitive.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte"; import PrimitiveIcon from "./PrimitiveIcon.svelte";
import {CaretRightOutline, CaretDownOutline} from "flowbite-svelte-icons"; import {CaretRightOutline, CaretDownOutline} from "flowbite-svelte-icons";
import type TreeViewState from "../models/TreeViewState.svelte"; import type FileViewState from "../models/FileViewState.svelte";
import { PathSelectedEvent } from "../events/PathSelectedEvent"; import {arraysAreEqual} from "../utils.js";
import { createEventDispatcher } from "svelte";
let {prim, path, fState}: { prim: Primitive | undefined, path: string[], fState: FileViewState } = $props();
let active = $derived(arraysAreEqual(path, fState.path))
let {
parent_view,
parent_path,
treeState,
pathSelectedHandler = $bindable(),
}: {
parent_view: Primitive;
parent_path: string[];
treeState: TreeViewState;
pathSelectedHandler: any;
} = $props();
function copyPathAndAppend(key: string): string[] { function copyPathAndAppend(key: string): string[] {
const _path = copyPath(); const _path = copyPath();
@ -27,61 +18,47 @@
function copyPath(): string[] { function copyPath(): string[] {
const _path: string[] = []; const _path: string[] = [];
for (let item of parent_path) { for (let item of path) {
_path.push(item); _path.push(item);
} }
return _path; return _path;
} }
function selectItem(child: Primitive) { function selectItem(child: Primitive) {
if (child.isContainer()) {
let _path = copyPathAndAppend(child.key); let _path = copyPathAndAppend(child.key);
console.log("Selecting from tree", _path); fState.expandTree(_path);
treeState.expandTree(_path); fState.selectPath(_path);
} else {
const event = new PathSelectedEvent(_path); let _path = copyPath();
pathSelectedHandler(event); fState.selectPath(_path);
}
} }
</script>
{#if parent_view}
</script>
{#if prim}
<ul class="active"> <ul class="active">
{#each parent_view.children as child} {#each prim.children as child}
<li> <li>
<div class="item "> <div class="item ">
{#if child.children.length > 0} {#if child.children.length > 0}
<button <button onclick={() => fState.collapseTree(copyPathAndAppend(child.key))}><span class="caret"><CaretDownOutline/></span></button>
onclick={() =>
treeState.collapseTree(
copyPathAndAppend(child.key),
)}
><span class="caret"><CaretDownOutline /></span
></button
>
{:else if child.isContainer()} {:else if child.isContainer()}
<button <button onclick={() => fState.expandTree(copyPathAndAppend(child.key))}><span class="caret"><CaretRightOutline/></span></button>
onclick={() =>
treeState.expandTree(
copyPathAndAppend(child.key),
)}
><span class="caret"><CaretRightOutline /></span
></button
>
{:else} {:else}
<span class="no-caret"></span> <span class="no-caret"></span>
{/if} {/if}
<button <button class="select-button" ondblclick={() => selectItem(child)}>
class="select-button"
ondblclick={() => selectItem(child)}
>
<div class="item"> <div class="item">
<PrimitiveIcon ptype={child.ptype}/> <PrimitiveIcon ptype={child.ptype}/>
<div class="row"> <div class="row">
<p> <p>
{child.key} {child.key}
</p> </p>
{#if child.sub_type} {#if child.subType}
<p class="ml-1 text-forge-sec small"> <p class="ml-1 text-forge-sec small">
{child.sub_type} {child.subType}
</p> </p>
{/if} {/if}
</div> </div>
@ -89,18 +66,14 @@
</button> </button>
</div> </div>
{#if child.children.length > 0} {#if child.children.length > 0}
<svelte:self <svelte:self prim={child} path={copyPathAndAppend(child.key)} {fState}></svelte:self>
parent_view={child}
parent_path={copyPathAndAppend(child.key)}
{treeState}
bind:pathSelectedHandler
></svelte:self>
{/if} {/if}
</li> </li>
{/each} {/each}
</ul> </ul>
{/if} {/if}
<style lang="postcss"> <style lang="postcss">
.select-button { .select-button {
@apply hover:bg-forge-sec pr-3; @apply hover:bg-forge-sec pr-3;
@ -130,8 +103,7 @@
} }
/* Remove default bullets */ /* Remove default bullets */
ul, ul, #myUL {
#myUL {
list-style-type: none; list-style-type: none;
} }
@ -139,6 +111,7 @@
@apply pl-5; @apply pl-5;
} }
.no-caret { .no-caret {
@apply pl-5; @apply pl-5;
user-select: none; user-select: none;
@ -149,6 +122,7 @@
@apply text-forge-sec; @apply text-forge-sec;
cursor: pointer; cursor: pointer;
user-select: none; /* Prevent text selection */ user-select: none; /* Prevent text selection */
} }
/* Create the caret/arrow with a unicode, and style it */ /* Create the caret/arrow with a unicode, and style it */

View File

@ -1,58 +1,39 @@
<script lang="ts"> <script lang="ts">
import TreeNode from "./TreeNode.svelte"; import TreeNode from "./TreeNode.svelte";
import type FileViewState from "../models/FileViewState.svelte";
import type Primitive from "../models/Primitive.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte"; import PrimitiveIcon from "./PrimitiveIcon.svelte";
import TreeViewState from "../models/TreeViewState.svelte";
import type TreeViewNode from "../models/TreeViewNode.svelte";
let { let {fState}: { fState: FileViewState } = $props()
file_id, let prim: Primitive | undefined = $derived(fState.treeView);
root,
pathSelectedHandler = $bindable(),
active = true,
}: {
file_id: string;
root: TreeViewNode;
pathSelectedHandler: any;
active: boolean;
} = $props();
let h = $state(100); let h = $state(100);
let treeState = $state(new TreeViewState(file_id, root));
</script>
</script>
<div bind:clientHeight={h} class="full-container"> <div bind:clientHeight={h} class="full-container">
<div class="overflow-auto" style="height: {h}px"> <div class="overflow-auto" style="height: {h}px">
<ul id="myUL"> <ul id="myUL">
{#if treeState.view} {#if prim}
<li> <li>
<div class="item"> <div class="item">
<PrimitiveIcon ptype={"Dictionary"}/> <PrimitiveIcon ptype={"Dictionary"}/>
{"Trailer "} {"Trailer "}
</div> </div>
{#if active} <TreeNode {prim} path={["/"]} {fState}></TreeNode>
<TreeNode
bind:pathSelectedHandler
parent_view={treeState.view}
parent_path={treeState.view.getPath()}
{treeState}
></TreeNode>
{/if}
</li> </li>
{/if} {/if}
</ul> </ul>
</div> </div>
getTrace
</div> </div>
getPath
<style lang="postcss"> <style lang="postcss">
.item { .item {
@apply text-sm rounded-sm; @apply text-sm rounded-sm;
text-align: center; text-align: center;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
ul, ul, #myUL {
#myUL {
list-style-type: none; list-style-type: none;
} }
</style> </style>

View File

@ -1,44 +1,38 @@
<script lang="ts"> <script lang="ts">
import type FileViewState from "../models/FileViewState.svelte"; import type FileViewState from "../models/FileViewState.svelte";
import type XRefEntry from "../models/XRefEntry"; import type XRefEntry from "../models/XRefEntry";
const cellH = 25; const cellH = 25;
const headerOffset = 2 * cellH; const headerOffset = 80;
let { fState, height }: { fState: FileViewState; height: number } = let {fState}: { fState: FileViewState } = $props();
$props();
let viewHeight: number = $state(100);
let fillerHeight: number = $state(0); let fillerHeight: number = $state(0);
let firstEntry = $state(0); let firstEntry = $state(0);
let lastEntry = $state(100); let lastEntry = $state(100);
let entriesToDisplay: XRefEntry[] = $derived( let entriesToDisplay: XRefEntry[] = $derived(fState.xref_entries.slice(firstEntry, lastEntry));
fState.xref_entries.slice(firstEntry, lastEntry),
);
let bodyViewHeight: number = $derived(Math.max(height - headerOffset, 0)); let bodyViewHeight: number = $derived(Math.max(viewHeight - headerOffset, 0));
$inspect(bodyViewHeight); let selectedPath: number | string | undefined = $derived(fState.getLastJump());
let selectedPath: number | string | undefined = $derived( let totalBodyHeight: number = $derived(Math.max(0, (fState.file.xref_entries * cellH) - headerOffset));
fState.getLastJump(),
);
let totalBodyHeight: number = $derived(
Math.max(0, fState.file.xref_entries * cellH - headerOffset),
);
let scrollContainer: HTMLElement; let scrollContainer: HTMLElement;
$effect(() => { $effect(() => {
if (typeof selectedPath === "number") { if (typeof selectedPath === "number") {
smoothScrollTo(scrollContainer, selectedPath); smoothScrollTo(scrollContainer, selectedPath)
} }
}); }
)
function smoothScrollTo(scrollContainer: HTMLElement, target: number) { function smoothScrollTo(scrollContainer: HTMLElement, target: number) {
const targetY = Math.max(target, 0) * cellH; const targetY = Math.max(target, 0) * cellH;
const startY = scrollContainer.scrollTop; const startY = scrollContainer.scrollTop;
if ( if (targetY - startY > 0 && targetY - startY < Math.max(viewHeight - headerOffset, 0)) {
targetY - startY > 0 &&
targetY - startY < Math.max(height - headerOffset, 0)
) {
return; return;
} }
@ -71,9 +65,11 @@
fillerHeight = firstEntry * cellH; fillerHeight = firstEntry * cellH;
} }
</script>
<div class="overflow-x-auto" style="height: {height}px;">
</script>
<div bind:clientHeight={viewHeight} class="full-container">
<div class="overflow-x-auto">
<div class="w-[281px]"> <div class="w-[281px]">
<table> <table>
<thead> <thead>
@ -83,12 +79,7 @@
<td class="cell t-header border-forge-prim">Type</td> <td class="cell t-header border-forge-prim">Type</td>
<td class="cell t-header border-forge-sec">Offset</td> <td class="cell t-header border-forge-sec">Offset</td>
</tr> </tr>
<tr <tr class={selectedPath === "/" ? "bg-forge-acc" : "hover:bg-forge-sec"} ondblclick={() => fState.selectXref(undefined)}>
class={selectedPath === "/"
? "bg-forge-acc"
: "hover:bg-forge-sec"}
ondblclick={() => fState.selectXref(undefined)}
>
<td class="cell t-data">Trailer</td> <td class="cell t-data">Trailer</td>
<td class="cell t-data">65535</td> <td class="cell t-data">65535</td>
<td class="cell t-data">Dictionary</td> <td class="cell t-data">Dictionary</td>
@ -96,24 +87,13 @@
</tr> </tr>
</thead> </thead>
</table> </table>
<div <div class="scrollContainer" style="height: {bodyViewHeight}px" onscroll={handleScroll} bind:this={scrollContainer}>
class="scrollContainer" <div class="container" style="height: {totalBodyHeight - headerOffset}px">
style="height: {bodyViewHeight}px"
onscroll={handleScroll}
bind:this={scrollContainer}
>
<div class="container" style="height: {totalBodyHeight}px">
<table> <table>
<tbody> <tbody>
<tr class="filler" style="height: {fillerHeight}px" <tr class="filler" style="height: {fillerHeight}px"></tr>
></tr>
{#each entriesToDisplay as entry} {#each entriesToDisplay as entry}
<tr <tr class={selectedPath === entry.obj_num ? "bg-forge-acc" : "hover:bg-forge-sec"} ondblclick={() => fState.selectXref(entry)}>
class={selectedPath === entry.obj_num
? "bg-forge-acc"
: "hover:bg-forge-sec"}
ondblclick={() => fState.selectXref(entry)}
>
<td class="cell t-data">{entry.obj_num}</td> <td class="cell t-data">{entry.obj_num}</td>
<td class="cell t-data">{entry.gen_num}</td> <td class="cell t-data">{entry.gen_num}</td>
<td class="cell t-data">{entry.obj_type}</td> <td class="cell t-data">{entry.obj_type}</td>
@ -126,6 +106,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<style lang="postcss"> <style lang="postcss">
.t-header { .t-header {
@ -149,15 +130,5 @@
.scrollContainer { .scrollContainer {
overflow-y: auto; overflow-y: auto;
} }
.xref-modal {
position: fixed;
right: 0;
top: 0;
bottom: 0;
width: 281px;
background: var(--background-color);
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
z-index: 1000;
overflow-y: auto;
}
</style> </style>

View File

@ -1,10 +0,0 @@
export class PathSelectedEvent extends CustomEvent<{ path: string[] }> {
static readonly eventName = 'pathselected';
constructor(path: string[]) {
super(PathSelectedEvent.eventName, {
detail: { path },
bubbles: true
});
}
}

View File

@ -7,10 +7,11 @@ import type XRefTable from "./XRefTable";
export default class FileViewState { export default class FileViewState {
public path: string[] = $state(["Trailer"]); public path: string[] = $state(["/"]);
public treeRoot: TreeViewNode = $state(new TreeViewNode("/", [new TreeViewNode("Root", [])]));
public file: PdfFile; public file: PdfFile;
public prim: Primitive | undefined = $state(); public prim: Primitive | undefined = $state();
public highlightedPrim: Primitive | undefined = $state(); public treeView: Primitive | undefined = $state();
public xref_entries: XRefEntry[] = $state([]); public xref_entries: XRefEntry[] = $state([]);
@ -18,6 +19,7 @@ export default class FileViewState {
this.file = file; this.file = file;
this.selectPath(this.path); this.selectPath(this.path);
this.loadTreeView();
this.loadXrefEntries() this.loadXrefEntries()
} }
@ -25,10 +27,6 @@ export default class FileViewState {
return this.prim?.getLastJump() return this.prim?.getLastJump()
} }
getFirstJump(): string | number | undefined {
return this.prim?.getFirstJump()
}
public loadXrefEntries() { public loadXrefEntries() {
invoke<XRefTable>("get_xref_table", {id: this.file.id}) invoke<XRefTable>("get_xref_table", {id: this.file.id})
.then(result => { .then(result => {
@ -40,30 +38,83 @@ export default class FileViewState {
public selectPath(newPath: string[]) { public selectPath(newPath: string[]) {
invoke<Primitive>("get_prim_by_path", {id: this.file.id, path: this.mergePaths(newPath)}) invoke<Primitive>("get_prim_by_path", {id: this.file.id, path: this.mergePaths(newPath)})
.then(result => { .then(result => {
let _prim = new Primitive(result) this.prim = new Primitive(result)
if (_prim.isContainer()) {
this.prim = _prim;
this.path = newPath this.path = newPath
return;
} else {
this.highlightedPrim = _prim;
this.selectPath(newPath.slice(0, newPath.length - 1))
}
}) })
.catch(err => console.error(err)); .catch(err => console.error(err));
} }
public loadTreeView() {
public getTreeRoot() { invoke<Primitive>("get_prim_tree_by_path", {id: this.file.id, path: this.treeRoot})
return this.treeState; .then(result => {
this.treeView = new Primitive(result);
}
).catch(err => console.error(err))
} }
public getTreeRoot() {
return this.treeRoot;
}
public expandTree(path: string[]) {
if (path.length == 0) {
console.error("Empty path")
return;
}
let node;
if (path[0] === "/") {
node = this.treeRoot;
} else {
console.error("Invalid Path " + path);
return;
}
for (let key of path.slice(1, path.length)) {
let _node: TreeViewNode | undefined = node.getChild(key)
if (_node) {
node = _node;
} else {
node = node.addChild(key)
}
}
this.loadTreeView();
}
public collapseTree(path: string[]) {
if (path.length == 0) {
console.error("Empty path")
return;
}
let node;
if (path[0] === "/") {
node = this.treeRoot;
} else {
console.error("Invalid Path " + path);
return;
}
if (path.length == 1) {
this.treeRoot.clearChildren();
return;
}
for (let key of path.slice(1, path.length - 1)) {
if (node) {
node = node.getChild(key)
}
}
if (node) {
node.removeChild(path[path.length - 1]);
}
this.loadTreeView()
}
public getMergedPath() { public getMergedPath() {
return this.mergePaths(this.path); return this.mergePaths(this.path);
} }
public displayPath() {
}
public popPath() { public popPath() {
let path = this.copyPath(); let path = this.copyPath();
if (path.length == 1) { if (path.length == 1) {

View File

@ -1,6 +1,5 @@
export default interface PageModel { export default interface PageModel {
key: string, key: string,
obj_num: number, id: number
page_num: number,
} }

View File

@ -4,7 +4,7 @@ export default class Primitive {
public sub_type: string; public sub_type: string;
public value: string; public value: string;
public children: Primitive[]; public children: Primitive[];
public trace: Trace[] = $state([]); public detail_path: DetailPath[] = $state([]);
constructor( constructor(
p: Primitive p: Primitive
@ -17,9 +17,9 @@ export default class Primitive {
for (let child of p.children) { for (let child of p.children) {
this.children.push(new Primitive(child)); this.children.push(new Primitive(child));
} }
this.trace = []; this.detail_path = [];
for (let path of p.trace) { for (let path of p.detail_path) {
this.trace.push(path); this.detail_path.push(path);
} }
} }
@ -27,36 +27,18 @@ export default class Primitive {
return this.ptype === "Dictionary" || this.ptype === "Array" || this.ptype === "Reference" || this.ptype === "Stream"; return this.ptype === "Dictionary" || this.ptype === "Array" || this.ptype === "Reference" || this.ptype === "Stream";
} }
public getPath(): string[] {
return this.trace.map(path => path.key);
}
public getLastJump(): string | number { public getLastJump(): string | number {
let path = this.trace[this.trace.length - 1].last_jump; let path = this.detail_path[this.detail_path.length - 1].last_jump;
if (path === "/") {return path}; if (path === "/") {return path};
return +path; return +path;
} }
public isPage(): boolean { public isPage(): boolean {
return this.trace[0].key.startsWith("Page") && !isNaN(+this.trace[0].key.replace("Page", "")); return this.sub_type === "Page";
}
public getPageNumber(): number {
if (!this.isPage()) {
throw new Error("Primitive is not a page");
}
return +this.trace[0].key.replace("Page", "");
}
public getFirstJump(): string | number | undefined {
let path = this.trace[0].last_jump;
if (path === "Trailer") { return path };
if (path.startsWith("Page")) { return path };
return +path;
} }
} }
export interface Trace { export interface DetailPath {
readonly key: string ; readonly key: string ;
readonly last_jump: string ; readonly last_jump: string ;
} }

View File

@ -10,9 +10,6 @@ export default class TreeViewNode {
this.children = children; this.children = children;
} }
static TRAILER = new TreeViewNode("Trailer", [new TreeViewNode("Root", [])]);
public getChild(key: string) { public getChild(key: string) {
return this.children.find(child => child.key === key); return this.children.find(child => child.key === key);
} }

View File

@ -1,65 +0,0 @@
import { invoke } from "@tauri-apps/api/core";
import Primitive from "./Primitive.svelte";
import TreeViewNode from "./TreeViewNode.svelte";
import type FileViewState from "./FileViewState.svelte";
export default class TreeViewState {
public root: TreeViewNode = $state(new TreeViewNode("Trailer", [new TreeViewNode("Root", [])]));
public view: Primitive | undefined = $state();
file_id: string;
constructor(file_id: string, root: TreeViewNode) {
console.log("Creating tree view state", file_id, root);
this.file_id = file_id;
this.root = root;
this.loadTreeView();
}
public loadTreeView() {
console.log("Loading tree view", this.root);
invoke<Primitive>("get_prim_tree_by_path", {
id: this.file_id,
path: this.root,
})
.then((result) => {
this.view = new Primitive(result);
})
.catch((err) => console.error(err));
}
public expandTree(path: string[]) {
if (path.length == 0) {
console.error("Empty path");
return;
}
let node = this.root;
for (let key of path.slice(1, path.length)) {
let _node: TreeViewNode | undefined = node.getChild(key);
if (_node) {
node = _node;
} else {
node = node.addChild(key);
}
}
this.loadTreeView();
}
public collapseTree(path: string[]) {
if (path.length == 0) {
console.error("Empty path");
return;
}
if (path.length == 1) {
this.root.clearChildren();
return;
}
let node: TreeViewNode | undefined = this.root;
for (let key of path.slice(1, path.length - 1)) {
node = node?.getChild(key);
}
if (node) {
node.removeChild(path[path.length - 1]);
}
this.loadTreeView();
}
}

View File

@ -11,9 +11,6 @@ const config = {
adapter: adapter(), adapter: adapter(),
}, },
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
// compilerOptions: {
// runes: true
// }
}; };
export default config; export default config;

View File

@ -1,15 +1,12 @@
import {defineConfig} from "vite"; import {defineConfig} from "vite";
import {sveltekit} from "@sveltejs/kit/vite"; import {sveltekit} from "@sveltejs/kit/vite";
// import monacoEditorPlugin from 'vite-plugin-monaco-editor';
// @ts-expect-error process is a nodejs global // @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST; const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(async () => ({ export default defineConfig(async () => ({
plugins: [ plugins: [sveltekit()],
sveltekit(),
// monacoEditorPlugin({})
],
css: { css: {
postcss: './postcss.config.cjs', // Path to PostCSS config postcss: './postcss.config.cjs', // Path to PostCSS config
}, },

466
yarn.lock

File diff suppressed because it is too large Load Diff