diff --git a/src-pdfrs/pdf/src/content.rs b/src-pdfrs/pdf/src/content.rs index 7acf38d..e831e53 100644 --- a/src-pdfrs/pdf/src/content.rs +++ b/src-pdfrs/pdf/src/content.rs @@ -1,17 +1,17 @@ +use datasize::DataSize; +use istring::SmallString; +use itertools::Itertools; +use std::cmp::Ordering; /// PDF content streams. use std::fmt::{self, Display}; -use std::cmp::Ordering; -use itertools::Itertools; -use istring::SmallString; -use datasize::DataSize; use std::sync::Arc; +use crate as pdf; +use crate::enc::StreamFilter; use crate::error::*; use crate::object::*; -use crate::parser::{Lexer, parse_with_lexer, ParseFlags}; +use crate::parser::{parse_with_lexer, Lexer, ParseFlags}; use crate::primitive::*; -use crate::enc::StreamFilter; -use crate as pdf; /// Represents a PDF content stream - a `Vec` of `Operator`s #[derive(Debug, Clone, DataSize)] @@ -41,7 +41,7 @@ pub fn parse_raw(data: &[u8]) -> Result> { let mut result = vec![]; while let Ok(next) = lexer.next() { result.push(next.to_string()); - }; + } Ok(result) } @@ -66,41 +66,51 @@ macro_rules! points { )* ) } -fn name(args: &mut impl Iterator) -> Result { +fn name(args: &mut impl Iterator) -> Result { args.next().ok_or(PdfError::NoOpArg)?.into_name() } -fn number(args: &mut impl Iterator) -> Result { +fn number(args: &mut impl Iterator) -> Result { args.next().ok_or(PdfError::NoOpArg)?.as_number() } -fn string(args: &mut impl Iterator) -> Result { +fn string(args: &mut impl Iterator) -> Result { args.next().ok_or(PdfError::NoOpArg)?.into_string() } -fn point(args: &mut impl Iterator) -> Result { +fn point(args: &mut impl Iterator) -> Result { let x = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; let y = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; Ok(Point { x, y }) } -fn rect(args: &mut impl Iterator) -> Result { +fn rect(args: &mut impl Iterator) -> Result { let x = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; let y = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; let width = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; let height = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; - Ok(ViewRect { x, y, width, height }) + Ok(ViewRect { + x, + y, + width, + height, + }) } -fn rgb(args: &mut impl Iterator) -> Result { +fn rgb(args: &mut impl Iterator) -> Result { let red = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; let green = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; let blue = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; Ok(Rgb { red, green, blue }) } -fn cmyk(args: &mut impl Iterator) -> Result { +fn cmyk(args: &mut impl Iterator) -> Result { let cyan = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; let magenta = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; let yellow = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; let key = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; - Ok(Cmyk { cyan, magenta, yellow, key }) + Ok(Cmyk { + cyan, + magenta, + yellow, + key, + }) } -fn matrix(args: &mut impl Iterator) -> Result { +fn matrix(args: &mut impl Iterator) -> Result { Ok(Matrix { a: number(args)?, b: number(args)?, @@ -110,11 +120,11 @@ fn matrix(args: &mut impl Iterator) -> Result { f: number(args)?, }) } -fn array(args: &mut impl Iterator) -> Result> { +fn array(args: &mut impl Iterator) -> Result> { match args.next() { Some(Primitive::Array(arr)) => Ok(arr), None => Ok(vec![]), - _ => Err(PdfError::NoOpArg) + _ => Err(PdfError::NoOpArg), } } @@ -129,8 +139,10 @@ fn expand_abbr_name(name: SmallString, alt: &[(&str, &str)]) -> SmallString { fn expand_abbr(p: Primitive, alt: &[(&str, &str)]) -> Primitive { match p { Primitive::Name(name) => Primitive::Name(expand_abbr_name(name, alt)), - Primitive::Array(items) => Primitive::Array(items.into_iter().map(|p| expand_abbr(p, alt)).collect()), - p => p + Primitive::Array(items) => { + Primitive::Array(items.into_iter().map(|p| expand_abbr(p, alt)).collect()) + } + p => p, } } @@ -146,19 +158,22 @@ fn inline_image(lexer: &mut Lexer, resolve: &impl Resolve) -> Result bail!("invalid key type") + Ok(_) => bail!("invalid key type"), }; - let key = expand_abbr_name(key, &[ - ("BPC", "BitsPerComponent"), - ("CS", "ColorSpace"), - ("D", "Decode"), - ("DP", "DecodeParms"), - ("F", "Filter"), - ("H", "Height"), - ("IM", "ImageMask"), - ("I", "Interpolate"), - ("W", "Width"), - ]); + let key = expand_abbr_name( + key, + &[ + ("BPC", "BitsPerComponent"), + ("CS", "ColorSpace"), + ("D", "Decode"), + ("DP", "DecodeParms"), + ("F", "Filter"), + ("H", "Height"), + ("IM", "ImageMask"), + ("I", "Interpolate"), + ("W", "Width"), + ], + ); let val = parse_with_lexer(lexer, &NoResolve, ParseFlags::ANY)?; dict.insert(key, val); } @@ -168,45 +183,87 @@ fn inline_image(lexer: &mut Lexer, resolve: &impl Resolve) -> Result parts.into_iter() - .map(|p| p.as_name().and_then(|kind| StreamFilter::from_kind_and_params(kind, decode_parms.clone(), resolve))) + Some(Primitive::Array(parts)) => parts + .into_iter() + .map(|p| { + p.as_name().and_then(|kind| { + StreamFilter::from_kind_and_params(kind, decode_parms.clone(), resolve) + }) + }) .collect::>()?, - Some(Primitive::Name(kind)) => vec![StreamFilter::from_kind_and_params(&kind, decode_parms, resolve)?], + Some(Primitive::Name(kind)) => vec![StreamFilter::from_kind_and_params( + &kind, + decode_parms, + resolve, + )?], None => vec![], - _ => bail!("invalid filter") + _ => bail!("invalid filter"), }; - + let height = dict.require("InlineImage", "Height")?.as_u32()?; - let image_mask = dict.get("ImageMask").map(|p| p.as_bool()).transpose()?.unwrap_or(false); - let intent = dict.remove("Intent").map(|p| RenderingIntent::from_primitive(p, &NoResolve)).transpose()?; - let interpolate = dict.get("Interpolate").map(|p| p.as_bool()).transpose()?.unwrap_or(false); + let image_mask = dict + .get("ImageMask") + .map(|p| p.as_bool()) + .transpose()? + .unwrap_or(false); + let intent = dict + .remove("Intent") + .map(|p| RenderingIntent::from_primitive(p, &NoResolve)) + .transpose()?; + let interpolate = dict + .get("Interpolate") + .map(|p| p.as_bool()) + .transpose()? + .unwrap_or(false); let width = dict.require("InlineImage", "Width")?.as_u32()?; let image_dict = ImageDict { @@ -225,22 +282,24 @@ fn inline_image(lexer: &mut Lexer, resolve: &impl Resolve) -> Result + ops: Vec, } impl OpBuilder { fn new() -> Self { OpBuilder { last: Point { x: 0., y: 0. }, compability_section: false, - ops: Vec::new() + ops: Vec::new(), } } fn parse(&mut self, data: &[u8], resolve: &impl Resolve) -> Result<()> { @@ -264,10 +323,10 @@ impl OpBuilder { let op = t!(lexer.next()); let operator = t!(op.as_str(), op); match self.add(operator, buffer.drain(..), &mut lexer, resolve) { - Ok(()) => {}, + Ok(()) => {} Err(e) if resolve.options().allow_invalid_ops => { warn!("OP Err: {:?}", e); - }, + } Err(e) => return Err(e), } } @@ -275,162 +334,208 @@ impl OpBuilder { match lexer.get_pos().cmp(&data.len()) { Ordering::Greater => err!(PdfError::ContentReadPastBoundary), Ordering::Less => (), - Ordering::Equal => break + Ordering::Equal => break, } } Ok(()) } - fn add(&mut self, op: &str, mut args: impl Iterator, lexer: &mut Lexer, resolve: &impl Resolve) -> Result<()> { + fn add( + &mut self, + op: &str, + mut args: impl Iterator, + lexer: &mut Lexer, + resolve: &impl Resolve, + ) -> Result<()> { use Winding::*; let ops = &mut self.ops; let mut push = move |op| ops.push(op); match op { - "b" => { + "b" => { push(Op::Close); push(Op::FillAndStroke { winding: NonZero }); - }, - "B" => push(Op::FillAndStroke { winding: NonZero }), - "b*" => { + } + "B" => push(Op::FillAndStroke { winding: NonZero }), + "b*" => { push(Op::Close); push(Op::FillAndStroke { winding: EvenOdd }); } - "B*" => push(Op::FillAndStroke { winding: EvenOdd }), + "B*" => push(Op::FillAndStroke { winding: EvenOdd }), "BDC" => push(Op::BeginMarkedContent { tag: name(&mut args)?, - properties: Some(args.next().ok_or(PdfError::NoOpArg)?) + properties: Some(args.next().ok_or(PdfError::NoOpArg)?), + }), + "BI" => push(Op::InlineImage { + image: inline_image(lexer, resolve)?, }), - "BI" => push(Op::InlineImage { image: inline_image(lexer, resolve)? }), "BMC" => push(Op::BeginMarkedContent { tag: name(&mut args)?, - properties: None + properties: None, }), - "BT" => push(Op::BeginText), - "BX" => self.compability_section = true, - "c" => { + "BT" => push(Op::BeginText), + "BX" => self.compability_section = true, + "c" => { points!(args, c1, c2, p); push(Op::CurveTo { c1, c2, p }); self.last = p; } - "cm" => { + "cm" => { numbers!(args, a, b, c, d, e, f); - push(Op::Transform { matrix: Matrix { a, b, c, d, e, f }}); + push(Op::Transform { + matrix: Matrix { a, b, c, d, e, f }, + }); } - "CS" => { + "CS" => { names!(args, name); push(Op::StrokeColorSpace { name }); } - "cs" => { + "cs" => { names!(args, name); push(Op::FillColorSpace { name }); } - "d" => { + "d" => { let p = args.next().ok_or(PdfError::NoOpArg)?; - let pattern = p.as_array()?.iter().map(|p| p.as_number()).collect::, PdfError>>()?; + let pattern = p + .as_array()? + .iter() + .map(|p| p.as_number()) + .collect::, PdfError>>()?; let phase = args.next().ok_or(PdfError::NoOpArg)?.as_number()?; push(Op::Dash { pattern, phase }); } - "d0" => {} - "d1" => {} + "d0" => {} + "d1" => {} "Do" | "Do0" => { names!(args, name); push(Op::XObject { name }); } - "DP" => push(Op::MarkedContentPoint { + "DP" => push(Op::MarkedContentPoint { tag: name(&mut args)?, - properties: Some(args.next().ok_or(PdfError::NoOpArg)?) + properties: Some(args.next().ok_or(PdfError::NoOpArg)?), }), - "EI" => bail!("Parse Error. Unexpected 'EI'"), + "EI" => bail!("Parse Error. Unexpected 'EI'"), "EMC" => push(Op::EndMarkedContent), - "ET" => push(Op::EndText), - "EX" => self.compability_section = false, - "f" | - "F" => push(Op::Fill { winding: NonZero }), - "f*" => push(Op::Fill { winding: EvenOdd }), - "G" => push(Op::StrokeColor { color: Color::Gray(number(&mut args)?) }), - "g" => push(Op::FillColor { color: Color::Gray(number(&mut args)?) }), - "gs" => push(Op::GraphicsState { name: name(&mut args)? }), - "h" => push(Op::Close), - "i" => push(Op::Flatness { tolerance: number(&mut args)? }), - "ID" => bail!("Parse Error. Unexpected 'ID'"), - "j" => { + "ET" => push(Op::EndText), + "EX" => self.compability_section = false, + "f" | "F" => push(Op::Fill { winding: NonZero }), + "f*" => push(Op::Fill { winding: EvenOdd }), + "G" => push(Op::StrokeColor { + color: Color::Gray(number(&mut args)?), + }), + "g" => push(Op::FillColor { + color: Color::Gray(number(&mut args)?), + }), + "gs" => push(Op::GraphicsState { + name: name(&mut args)?, + }), + "h" => push(Op::Close), + "i" => push(Op::Flatness { + tolerance: number(&mut args)?, + }), + "ID" => bail!("Parse Error. Unexpected 'ID'"), + "j" => { let n = args.next().ok_or(PdfError::NoOpArg)?.as_integer()?; let join = match n { 0 => LineJoin::Miter, 1 => LineJoin::Round, 2 => LineJoin::Bevel, - _ => bail!("invalid line join {}", n) + _ => bail!("invalid line join {}", n), }; push(Op::LineJoin { join }); } - "J" => { + "J" => { let n = args.next().ok_or(PdfError::NoOpArg)?.as_integer()?; let cap = match n { 0 => LineCap::Butt, 1 => LineCap::Round, 2 => LineCap::Square, - _ => bail!("invalid line cap {}", n) + _ => bail!("invalid line cap {}", n), }; push(Op::LineCap { cap }); } - "K" => { + "K" => { let color = Color::Cmyk(cmyk(&mut args)?); push(Op::StrokeColor { color }); } - "k" => { + "k" => { let color = Color::Cmyk(cmyk(&mut args)?); push(Op::FillColor { color }); } - "l" => { + "l" => { let p = point(&mut args)?; push(Op::LineTo { p }); self.last = p; } - "m" => { + "m" => { let p = point(&mut args)?; push(Op::MoveTo { p }); self.last = p; } - "M" => push(Op::MiterLimit { limit: number(&mut args)? }), - "MP" => push(Op::MarkedContentPoint { tag: name(&mut args)?, properties: None }), - "n" => push(Op::EndPath), - "q" => push(Op::Save), - "Q" => push(Op::Restore), - "re" => push(Op::Rect { rect: rect(&mut args)? }), - "RG" => push(Op::StrokeColor { color: Color::Rgb(rgb(&mut args)?) }), - "rg" => push(Op::FillColor { color: Color::Rgb(rgb(&mut args)?) }), - "ri" => { + "M" => push(Op::MiterLimit { + limit: number(&mut args)?, + }), + "MP" => push(Op::MarkedContentPoint { + tag: name(&mut args)?, + properties: None, + }), + "n" => push(Op::EndPath), + "q" => push(Op::Save), + "Q" => push(Op::Restore), + "re" => push(Op::Rect { + rect: rect(&mut args)?, + }), + "RG" => push(Op::StrokeColor { + color: Color::Rgb(rgb(&mut args)?), + }), + "rg" => push(Op::FillColor { + color: Color::Rgb(rgb(&mut args)?), + }), + "ri" => { let s = name(&mut args)?; - let intent = RenderingIntent::from_str(&s) - .ok_or_else(|| PdfError::Other { msg: format!("invalid rendering intent {}", s) })?; + let intent = RenderingIntent::from_str(&s).ok_or_else(|| PdfError::Other { + msg: format!("invalid rendering intent {}", s), + })?; push(Op::RenderingIntent { intent }); - }, - "s" => { + } + "s" => { push(Op::Close); push(Op::Stroke); } - "S" => push(Op::Stroke), + "S" => push(Op::Stroke), "SC" | "SCN" => { - push(Op::StrokeColor { color: Color::Other(args.collect()) }); + push(Op::StrokeColor { + color: Color::Other(args.collect()), + }); } "sc" | "scn" => { - push(Op::FillColor { color: Color::Other(args.collect()) }); + push(Op::FillColor { + color: Color::Other(args.collect()), + }); } - "sh" => { - - } - "T*" => push(Op::TextNewline), - "Tc" => push(Op::CharSpacing { char_space: number(&mut args)? }), - "Td" => push(Op::MoveTextPosition { translation: point(&mut args)? }), - "TD" => { + "sh" => {} + "T*" => push(Op::TextNewline), + "Tc" => push(Op::CharSpacing { + char_space: number(&mut args)?, + }), + "Td" => push(Op::MoveTextPosition { + translation: point(&mut args)?, + }), + "TD" => { let translation = point(&mut args)?; - push(Op::Leading { leading: -translation.y }); + push(Op::Leading { + leading: -translation.y, + }); push(Op::MoveTextPosition { translation }); } - "Tf" => push(Op::TextFont { name: name(&mut args)?, size: number(&mut args)? }), - "Tj" => push(Op::TextDraw { text: string(&mut args)? }), - "TJ" => { + "Tf" => push(Op::TextFont { + name: name(&mut args)?, + size: number(&mut args)?, + }), + "Tj" => push(Op::TextDraw { + text: string(&mut args)?, + }), + "TJ" => { let mut result = Vec::::new(); for spacing_or_text in array(&mut args)?.into_iter() { @@ -438,7 +543,7 @@ impl OpBuilder { Primitive::Integer(i) => TextDrawAdjusted::Spacing(i as f32), Primitive::Number(f) => TextDrawAdjusted::Spacing(f), Primitive::String(text) => TextDrawAdjusted::Text(text), - p => bail!("invalid primitive in TJ operator: {:?}", p) + p => bail!("invalid primitive in TJ operator: {:?}", p), }; result.push(spacing_or_text); @@ -446,9 +551,13 @@ impl OpBuilder { push(Op::TextDrawAdjusted { array: result }) } - "TL" => push(Op::Leading { leading: number(&mut args)? }), - "Tm" => push(Op::SetTextMatrix { matrix: matrix(&mut args)? }), - "Tr" => { + "TL" => push(Op::Leading { + leading: number(&mut args)?, + }), + "Tm" => push(Op::SetTextMatrix { + matrix: matrix(&mut args)?, + }), + "Tr" => { use TextMode::*; let n = args.next().ok_or(PdfError::NoOpArg)?.as_integer()?; @@ -465,35 +574,55 @@ impl OpBuilder { }; push(Op::TextRenderMode { mode }); } - "Ts" => push(Op::TextRise { rise: number(&mut args)? }), - "Tw" => push(Op::WordSpacing { word_space: number(&mut args)? }), - "Tz" => push(Op::TextScaling { horiz_scale: number(&mut args)? }), - "v" => { + "Ts" => push(Op::TextRise { + rise: number(&mut args)?, + }), + "Tw" => push(Op::WordSpacing { + word_space: number(&mut args)?, + }), + "Tz" => push(Op::TextScaling { + horiz_scale: number(&mut args)?, + }), + "v" => { points!(args, c2, p); - push(Op::CurveTo { c1: self.last, c2, p }); + push(Op::CurveTo { + c1: self.last, + c2, + p, + }); self.last = p; } - "w" => push(Op::LineWidth { width: number(&mut args)? }), - "W" => push(Op::Clip { winding: NonZero }), - "W*" => push(Op::Clip { winding: EvenOdd }), - "y" => { + "w" => push(Op::LineWidth { + width: number(&mut args)?, + }), + "W" => push(Op::Clip { winding: NonZero }), + "W*" => push(Op::Clip { winding: EvenOdd }), + "y" => { points!(args, c1, p); push(Op::CurveTo { c1, c2: p, p }); self.last = p; } - "'" => { + "'" => { push(Op::TextNewline); - push(Op::TextDraw { text: string(&mut args)? }); + push(Op::TextDraw { + text: string(&mut args)?, + }); } - "\"" => { - push(Op::WordSpacing { word_space: number(&mut args)? }); - push(Op::CharSpacing { char_space: number(&mut args)? }); + "\"" => { + push(Op::WordSpacing { + word_space: number(&mut args)?, + }); + push(Op::CharSpacing { + char_space: number(&mut args)?, + }); push(Op::TextNewline); - push(Op::TextDraw { text: string(&mut args)? }); + push(Op::TextDraw { + text: string(&mut args)?, + }); } o if !self.compability_section => { bail!("invalid operator {}", o) - }, + } _ => {} } Ok(()) @@ -513,7 +642,9 @@ impl Object for Content { parts.push(part); } } - Primitive::Reference(r) => return Self::from_primitive(t!(resolve.resolve(r)), resolve), + Primitive::Reference(r) => { + return Self::from_primitive(t!(resolve.resolve(r)), resolve) + } p => { let part = t!(ContentStream::from_primitive(p, resolve)); parts.push(part); @@ -543,9 +674,7 @@ impl Object for FormXObject { /// Convert primitive to Self fn from_primitive(p: Primitive, resolve: &impl Resolve) -> Result { let stream = t!(Stream::::from_primitive(p, resolve)); - Ok(FormXObject { - stream, - }) + Ok(FormXObject { stream }) } } impl ObjectWrite for FormXObject { @@ -584,39 +713,55 @@ pub fn display_ops(mut ops: &[Op]) -> Result> { while ops.len() > 0 { let mut advance = 1; match ops[0] { - Op::BeginMarkedContent { ref tag, properties: Some(ref name) } => { + Op::BeginMarkedContent { + ref tag, + properties: Some(ref name), + } => { write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} {} BDC", tag, name)?; marked_depth += 1; } - Op::BeginMarkedContent { ref tag, properties: None } => { + Op::BeginMarkedContent { + ref tag, + properties: None, + } => { write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} BMC", tag)?; - marked_depth += 1; + marked_depth = marked_depth.saturating_add(1); } - Op::MarkedContentPoint { ref tag, properties: Some(ref name) } => { + Op::MarkedContentPoint { + ref tag, + properties: Some(ref name), + } => { write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} {} DP", tag, tag)?; } - Op::MarkedContentPoint { ref tag, properties: None } => { + Op::MarkedContentPoint { + ref tag, + properties: None, + } => { write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} MP", tag)?; } Op::EndMarkedContent => { - marked_depth -= 1; + marked_depth = marked_depth.saturating_sub(1); write_ln_indented!(f, marked_depth, q_depth, t_depth, "EMC")?; - }, + } Op::Close => match ops.get(1) { Some(Op::Stroke) => { write_ln_indented!(f, marked_depth, q_depth, t_depth, "s")?; advance += 1; } - Some(Op::FillAndStroke { winding: Winding::NonZero }) => { + Some(Op::FillAndStroke { + winding: Winding::NonZero, + }) => { write_ln_indented!(f, marked_depth, q_depth, t_depth, "b")?; advance += 1; } - Some(Op::FillAndStroke { winding: Winding::EvenOdd }) => { + Some(Op::FillAndStroke { + winding: Winding::EvenOdd, + }) => { write_ln_indented!(f, marked_depth, q_depth, t_depth, "b*")?; advance += 1; } _ => write_ln_indented!(f, marked_depth, q_depth, t_depth, "h")?, - } + }, Op::MoveTo { p } => { write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} m", p)?; current_point = Some(p); @@ -624,7 +769,7 @@ pub fn display_ops(mut ops: &[Op]) -> Result> { Op::LineTo { p } => { write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} l", p)?; current_point = Some(p); - }, + } Op::CurveTo { c1, c2, p } => { if Some(c1) == current_point { write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} {} v", c2, p)?; @@ -634,105 +779,243 @@ pub fn display_ops(mut ops: &[Op]) -> Result> { write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} {} {} c", c1, c2, p)?; } current_point = Some(p); - }, - Op::Rect { rect } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} re", rect)?, + } + Op::Rect { rect } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} re", rect)? + } Op::EndPath => write_ln_indented!(f, marked_depth, q_depth, t_depth, "n")?, Op::Stroke => write_ln_indented!(f, marked_depth, q_depth, t_depth, "S")?, - Op::FillAndStroke { winding: Winding::NonZero } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "B")?, - Op::FillAndStroke { winding: Winding::EvenOdd } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "B*")?, - Op::Fill { winding: Winding::NonZero } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "f")?, - Op::Fill { winding: Winding::EvenOdd } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "f*")?, - Op::Shade { ref name } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} sh", name)?, - Op::Clip { winding: Winding::NonZero } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "W")?, - Op::Clip { winding: Winding::EvenOdd } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "W*")?, + Op::FillAndStroke { + winding: Winding::NonZero, + } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "B")?, + Op::FillAndStroke { + winding: Winding::EvenOdd, + } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "B*")?, + Op::Fill { + winding: Winding::NonZero, + } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "f")?, + Op::Fill { + winding: Winding::EvenOdd, + } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "f*")?, + Op::Shade { ref name } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} sh", name)? + } + Op::Clip { + winding: Winding::NonZero, + } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "W")?, + Op::Clip { + winding: Winding::EvenOdd, + } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "W*")?, Op::Save => { write_ln_indented!(f, marked_depth, q_depth, t_depth, "q")?; q_depth += 1; - }, + } Op::Restore => { q_depth = q_depth.saturating_sub(1); write_ln_indented!(f, marked_depth, q_depth, t_depth, "Q")?; - }, - Op::Transform { matrix } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} cm", matrix)?, - Op::LineWidth { width } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} w", width)?, - Op::Dash { ref pattern, phase } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "[{}] {} d", pattern.iter().format(" "), phase)?, - Op::LineJoin { join } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} j", join as u8)?, - Op::LineCap { cap } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} J", cap as u8)?, - Op::MiterLimit { limit } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} M", limit)?, - Op::Flatness { tolerance } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} i", tolerance)?, - Op::GraphicsState { ref name } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} gs", name)?, - Op::StrokeColor { color: Color::Gray(g) } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} G", g)?, - Op::StrokeColor { color: Color::Rgb(rgb) } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} RG", rgb)?, - Op::StrokeColor { color: Color::Cmyk(cmyk) } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} K", cmyk)?, - Op::StrokeColor { color: Color::Other(ref args) } => { - let args_str = args.iter().map(|p| format!("{}", p)).collect::>().join(" "); + } + Op::Transform { matrix } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} cm", matrix)? + } + Op::LineWidth { width } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} w", width)? + } + Op::Dash { ref pattern, phase } => write_ln_indented!( + f, + marked_depth, + q_depth, + t_depth, + "[{}] {} d", + pattern.iter().format(" "), + phase + )?, + Op::LineJoin { join } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} j", join as u8)? + } + Op::LineCap { cap } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} J", cap as u8)? + } + Op::MiterLimit { limit } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} M", limit)? + } + Op::Flatness { tolerance } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} i", tolerance)? + } + Op::GraphicsState { ref name } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} gs", name)? + } + Op::StrokeColor { + color: Color::Gray(g), + } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} G", g)?, + Op::StrokeColor { + color: Color::Rgb(rgb), + } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} RG", rgb)?, + Op::StrokeColor { + color: Color::Cmyk(cmyk), + } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} K", cmyk)?, + Op::StrokeColor { + color: Color::Other(ref args), + } => { + let args_str = args + .iter() + .map(|p| format!("{}", p)) + .collect::>() + .join(" "); write_ln_indented!(f, marked_depth, q_depth, t_depth, "{}{} SCN", args_str, " ")?; } - Op::FillColor { color: Color::Gray(g) } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} g", g)?, - Op::FillColor { color: Color::Rgb(rgb) } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} rg", rgb)?, - Op::FillColor { color: Color::Cmyk(cmyk) } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} k", cmyk)?, - Op::FillColor { color: Color::Other(ref args) } => { - let args_str = args.iter().map(|p| format!("{}", p)).collect::>().join(" "); + Op::FillColor { + color: Color::Gray(g), + } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} g", g)?, + Op::FillColor { + color: Color::Rgb(rgb), + } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} rg", rgb)?, + Op::FillColor { + color: Color::Cmyk(cmyk), + } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} k", cmyk)?, + Op::FillColor { + color: Color::Other(ref args), + } => { + let args_str = args + .iter() + .map(|p| format!("{}", p)) + .collect::>() + .join(" "); write_ln_indented!(f, marked_depth, q_depth, t_depth, "{}{} scn", args_str, " ")?; } - Op::FillColorSpace { ref name } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} cs", name)?, - Op::StrokeColorSpace { ref name } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} CS", name)?, - Op::RenderingIntent { intent } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} ri", intent.to_str())?, - Op::BeginText => {write_ln_indented!(f, marked_depth, q_depth, t_depth, "BT")?; t_depth = t_depth.saturating_add(1)}, - Op::EndText => {t_depth = t_depth.saturating_sub(1); write_ln_indented!(f, marked_depth, q_depth, t_depth, "ET")?;}, - Op::CharSpacing { char_space } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Tc", char_space)?, + Op::FillColorSpace { ref name } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} cs", name)? + } + Op::StrokeColorSpace { ref name } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} CS", name)? + } + Op::RenderingIntent { intent } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} ri", intent.to_str())? + } + Op::BeginText => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "BT")?; + t_depth = t_depth.saturating_add(1) + } + Op::EndText => { + t_depth = t_depth.saturating_sub(1); + write_ln_indented!(f, marked_depth, q_depth, t_depth, "ET")?; + } + Op::CharSpacing { char_space } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Tc", char_space)? + } Op::WordSpacing { word_space } => { - if let [Op::CharSpacing { char_space }, Op::TextNewline, Op::TextDraw { ref text }, ..] = ops[1..] { - write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} {} {:} \"", word_space, char_space, format_text(text)?)?; + if let [Op::CharSpacing { char_space }, Op::TextNewline, Op::TextDraw { ref text }, ..] = + ops[1..] + { + write_ln_indented!( + f, + marked_depth, + q_depth, + t_depth, + "{} {} {:} \"", + word_space, + char_space, + format_text(text)? + )?; advance += 3; } else { write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Tw", word_space)?; } } - Op::TextScaling { horiz_scale } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Tz", horiz_scale)?, + Op::TextScaling { horiz_scale } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Tz", horiz_scale)? + } Op::Leading { leading } => match ops[1..] { [Op::MoveTextPosition { translation }, ..] if leading == -translation.x => { - write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} {} TD", translation.x, translation.y)?; + write_ln_indented!( + f, + marked_depth, + q_depth, + t_depth, + "{} {} TD", + translation.x, + translation.y + )?; advance += 1; } _ => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} TL", leading)?, + }, + Op::TextFont { ref name, ref size } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} {} Tf", name, size)? + } + Op::TextRenderMode { mode } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Tr", mode as u8)? + } + Op::TextRise { rise } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Ts", rise)? + } + Op::MoveTextPosition { translation } => write_ln_indented!( + f, + marked_depth, + q_depth, + t_depth, + "{} {} Td", + translation.x, + translation.y + )?, + Op::SetTextMatrix { matrix } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Tm", matrix)? } - Op::TextFont { ref name, ref size } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} {} Tf", name, size)?, - Op::TextRenderMode { mode } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Tr", mode as u8)?, - Op::TextRise { rise } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Ts", rise)?, - Op::MoveTextPosition { translation } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} {} Td", translation.x, translation.y)?, - Op::SetTextMatrix { matrix } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Tm", matrix)?, Op::TextNewline => { if let [Op::TextDraw { ref text }, ..] = ops[1..] { - write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} '", format_text(text)?)?; + write_ln_indented!( + f, + marked_depth, + q_depth, + t_depth, + "{} '", + format_text(text)? + )?; advance += 1; } else { write_ln_indented!(f, marked_depth, q_depth, t_depth, "T*")?; } - }, + } Op::TextDraw { ref text } => { - write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Tj", format_text(text)?)?; - }, + write_ln_indented!( + f, + marked_depth, + q_depth, + t_depth, + "{} Tj", + format_text(text)? + )?; + } Op::TextDrawAdjusted { ref array } => { - let content = array.iter().enumerate() + let content = array + .iter() + .enumerate() .map(|(i, val)| match val { TextDrawAdjusted::Spacing(s) => s.to_string(), - TextDrawAdjusted::Text(data) => format_text(data).unwrap_or(String::from("!!!")), + TextDrawAdjusted::Text(data) => { + format_text(data).unwrap_or(String::from("!!!")) + } }) .collect::>() .join(" "); write_ln_indented!(f, marked_depth, q_depth, t_depth, "[{}] TJ", content)?; - }, - Op::InlineImage { image: _ } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "Inline image is not implemented yet!")?, - Op::XObject { ref name } => write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Do", name)?, + } + Op::InlineImage { image: _ } => write_ln_indented!( + f, + marked_depth, + q_depth, + t_depth, + "Inline image is not implemented yet!" + )?, + Op::XObject { ref name } => { + write_ln_indented!(f, marked_depth, q_depth, t_depth, "{} Do", name)? + } } ops = &ops[advance..]; } Ok(data) } - -#[allow(clippy::float_cmp)] // TODO +#[allow(clippy::float_cmp)] // TODO pub fn serialize_ops(mut ops: &[Op]) -> Result> { use std::io::Write; @@ -743,23 +1026,35 @@ pub fn serialize_ops(mut ops: &[Op]) -> Result> { while ops.len() > 0 { let mut advance = 1; match ops[0] { - Op::BeginMarkedContent { ref tag, properties: Some(ref name) } => { + Op::BeginMarkedContent { + ref tag, + properties: Some(ref name), + } => { serialize_name(tag, f)?; write!(f, " ")?; name.serialize(f)?; writeln!(f, " BDC")?; } - Op::BeginMarkedContent { ref tag, properties: None } => { + Op::BeginMarkedContent { + ref tag, + properties: None, + } => { serialize_name(tag, f)?; writeln!(f, " BMC")?; } - Op::MarkedContentPoint { ref tag, properties: Some(ref name) } => { + Op::MarkedContentPoint { + ref tag, + properties: Some(ref name), + } => { serialize_name(tag, f)?; write!(f, " ")?; name.serialize(f)?; writeln!(f, " DP")?; } - Op::MarkedContentPoint { ref tag, properties: None } => { + Op::MarkedContentPoint { + ref tag, + properties: None, + } => { serialize_name(tag, f)?; writeln!(f, " MP")?; } @@ -769,16 +1064,20 @@ pub fn serialize_ops(mut ops: &[Op]) -> Result> { writeln!(f, "s")?; advance += 1; } - Some(Op::FillAndStroke { winding: Winding::NonZero }) => { + Some(Op::FillAndStroke { + winding: Winding::NonZero, + }) => { writeln!(f, "b")?; advance += 1; } - Some(Op::FillAndStroke { winding: Winding::EvenOdd }) => { + Some(Op::FillAndStroke { + winding: Winding::EvenOdd, + }) => { writeln!(f, "b*")?; advance += 1; } _ => writeln!(f, "h")?, - } + }, Op::MoveTo { p } => { writeln!(f, "{} m", p)?; current_point = Some(p); @@ -786,7 +1085,7 @@ pub fn serialize_ops(mut ops: &[Op]) -> Result> { Op::LineTo { p } => { writeln!(f, "{} l", p)?; current_point = Some(p); - }, + } Op::CurveTo { c1, c2, p } => { if Some(c1) == current_point { writeln!(f, "{} {} v", c2, p)?; @@ -796,25 +1095,39 @@ pub fn serialize_ops(mut ops: &[Op]) -> Result> { writeln!(f, "{} {} {} c", c1, c2, p)?; } current_point = Some(p); - }, + } Op::Rect { rect } => writeln!(f, "{} re", rect)?, Op::EndPath => writeln!(f, "n")?, Op::Stroke => writeln!(f, "S")?, - Op::FillAndStroke { winding: Winding::NonZero } => writeln!(f, "B")?, - Op::FillAndStroke { winding: Winding::EvenOdd } => writeln!(f, "B*")?, - Op::Fill { winding: Winding::NonZero } => writeln!(f, "f")?, - Op::Fill { winding: Winding::EvenOdd } => writeln!(f, "f*")?, + Op::FillAndStroke { + winding: Winding::NonZero, + } => writeln!(f, "B")?, + Op::FillAndStroke { + winding: Winding::EvenOdd, + } => writeln!(f, "B*")?, + Op::Fill { + winding: Winding::NonZero, + } => writeln!(f, "f")?, + Op::Fill { + winding: Winding::EvenOdd, + } => writeln!(f, "f*")?, Op::Shade { ref name } => { serialize_name(name, f)?; writeln!(f, " sh")?; - }, - Op::Clip { winding: Winding::NonZero } => writeln!(f, "W")?, - Op::Clip { winding: Winding::EvenOdd } => writeln!(f, "W*")?, + } + Op::Clip { + winding: Winding::NonZero, + } => writeln!(f, "W")?, + Op::Clip { + winding: Winding::EvenOdd, + } => writeln!(f, "W*")?, Op::Save => writeln!(f, "q")?, Op::Restore => writeln!(f, "Q")?, Op::Transform { matrix } => writeln!(f, "{} cm", matrix)?, Op::LineWidth { width } => writeln!(f, "{} w", width)?, - Op::Dash { ref pattern, phase } => writeln!(f, "[{}] {} d", pattern.iter().format(" "), phase)?, + Op::Dash { ref pattern, phase } => { + writeln!(f, "[{}] {} d", pattern.iter().format(" "), phase)? + } Op::LineJoin { join } => writeln!(f, "{} j", join as u8)?, Op::LineCap { cap } => writeln!(f, "{} J", cap as u8)?, Op::MiterLimit { limit } => writeln!(f, "{} M", limit)?, @@ -822,21 +1135,37 @@ pub fn serialize_ops(mut ops: &[Op]) -> Result> { Op::GraphicsState { ref name } => { serialize_name(name, f)?; writeln!(f, " gs")?; - }, - Op::StrokeColor { color: Color::Gray(g) } => writeln!(f, "{} G", g)?, - Op::StrokeColor { color: Color::Rgb(rgb) } => writeln!(f, "{} RG", rgb)?, - Op::StrokeColor { color: Color::Cmyk(cmyk) } => writeln!(f, "{} K", cmyk)?, - Op::StrokeColor { color: Color::Other(ref args) } => { + } + Op::StrokeColor { + color: Color::Gray(g), + } => writeln!(f, "{} G", g)?, + Op::StrokeColor { + color: Color::Rgb(rgb), + } => writeln!(f, "{} RG", rgb)?, + Op::StrokeColor { + color: Color::Cmyk(cmyk), + } => writeln!(f, "{} K", cmyk)?, + Op::StrokeColor { + color: Color::Other(ref args), + } => { for p in args { p.serialize(f)?; write!(f, " ")?; } writeln!(f, "SCN")?; } - Op::FillColor { color: Color::Gray(g) } => writeln!(f, "{} g", g)?, - Op::FillColor { color: Color::Rgb(rgb) } => writeln!(f, "{} rg", rgb)?, - Op::FillColor { color: Color::Cmyk(cmyk) } => writeln!(f, "{} k", cmyk)?, - Op::FillColor { color: Color::Other(ref args) } => { + Op::FillColor { + color: Color::Gray(g), + } => writeln!(f, "{} g", g)?, + Op::FillColor { + color: Color::Rgb(rgb), + } => writeln!(f, "{} rg", rgb)?, + Op::FillColor { + color: Color::Cmyk(cmyk), + } => writeln!(f, "{} k", cmyk)?, + Op::FillColor { + color: Color::Other(ref args), + } => { for p in args { p.serialize(f)?; write!(f, " ")?; @@ -846,23 +1175,20 @@ pub fn serialize_ops(mut ops: &[Op]) -> Result> { Op::FillColorSpace { ref name } => { serialize_name(name, f)?; writeln!(f, " cs")?; - }, + } Op::StrokeColorSpace { ref name } => { serialize_name(name, f)?; writeln!(f, " CS")?; - }, + } Op::RenderingIntent { intent } => writeln!(f, "{} ri", intent.to_str())?, Op::BeginText => writeln!(f, "BT")?, Op::EndText => writeln!(f, "ET")?, Op::CharSpacing { char_space } => writeln!(f, "{} Tc", char_space)?, Op::WordSpacing { word_space } => { - if let [ - Op::CharSpacing { char_space }, - Op::TextNewline, - Op::TextDraw { ref text }, - .. - ] = ops[1..] { + if let [Op::CharSpacing { char_space }, Op::TextNewline, Op::TextDraw { ref text }, ..] = + ops[1..] + { write!(f, "{} {} ", word_space, char_space)?; text.serialize(f)?; writeln!(f, " \"")?; @@ -880,14 +1206,16 @@ pub fn serialize_ops(mut ops: &[Op]) -> Result> { _ => { writeln!(f, "{} TL", leading)?; } - } + }, Op::TextFont { ref name, ref size } => { serialize_name(name, f)?; writeln!(f, " {} Tf", size)?; - }, + } Op::TextRenderMode { mode } => writeln!(f, "{} Tr", mode as u8)?, Op::TextRise { rise } => writeln!(f, "{} Ts", rise)?, - Op::MoveTextPosition { translation } => writeln!(f, "{} {} Td", translation.x, translation.y)?, + Op::MoveTextPosition { translation } => { + writeln!(f, "{} {} Td", translation.x, translation.y)? + } Op::SetTextMatrix { matrix } => writeln!(f, "{} Tm", matrix)?, Op::TextNewline => { if let [Op::TextDraw { ref text }, ..] = ops[1..] { @@ -897,11 +1225,11 @@ pub fn serialize_ops(mut ops: &[Op]) -> Result> { } else { writeln!(f, "T*")?; } - }, + } Op::TextDraw { ref text } => { text.serialize(f)?; writeln!(f, " Tj")?; - }, + } Op::TextDrawAdjusted { ref array } => { write!(f, "[")?; for (i, val) in array.iter().enumerate() { @@ -914,12 +1242,12 @@ pub fn serialize_ops(mut ops: &[Op]) -> Result> { } } writeln!(f, "] TJ")?; - }, + } Op::InlineImage { image: _ } => unimplemented!(), Op::XObject { ref name } => { serialize_name(name, f)?; writeln!(f, " Do")?; - }, + } } ops = &ops[advance..]; } @@ -930,7 +1258,7 @@ impl Content { pub fn from_ops(operations: Vec) -> Self { let data = serialize_ops(&operations).unwrap(); Content { - parts: vec![Stream::new((), data)] + parts: vec![Stream::new((), data)], } } } @@ -949,7 +1277,7 @@ impl ObjectWrite for Content { #[derive(Debug, Copy, Clone, PartialEq, DataSize)] pub enum Winding { EvenOdd, - NonZero + NonZero, } #[derive(Debug, Copy, Clone, PartialEq, DataSize)] @@ -973,7 +1301,7 @@ pub struct PdfSpace(); #[repr(C, align(8))] pub struct Point { pub x: f32, - pub y: f32 + pub y: f32, } impl Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -1038,24 +1366,39 @@ impl Display for ViewRect { #[cfg(feature = "euclid")] impl Into> for ViewRect { fn into(self) -> euclid::Box2D { - let ViewRect { x, y, width, height } = self; + let ViewRect { + x, + y, + width, + height, + } = self; assert!(width > 0.0); assert!(height > 0.0); - euclid::Box2D::new(euclid::Point2D::new(x, y), euclid::Point2D::new(x + width, y + height)) + euclid::Box2D::new( + euclid::Point2D::new(x, y), + euclid::Point2D::new(x + width, y + height), + ) } } #[cfg(feature = "euclid")] impl From> for ViewRect { fn from(from: euclid::Box2D) -> Self { - let euclid::Box2D { min: euclid::Point2D { x, y, .. }, max: euclid::Point2D { x: x2, y: y2, .. }, .. } = from; + let euclid::Box2D { + min: euclid::Point2D { x, y, .. }, + max: euclid::Point2D { x: x2, y: y2, .. }, + .. + } = from; assert!(x < x2); assert!(y < y2); ViewRect { - x, y, width: x2 - x, height: y2 - y + x, + y, + width: x2 - x, + height: y2 - y, } } } @@ -1072,7 +1415,11 @@ pub struct Matrix { } impl Display for Matrix { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} {} {} {} {} {}", self.a, self.b, self.c, self.d, self.e, self.f) + write!( + f, + "{} {} {} {} {} {}", + self.a, self.b, self.c, self.d, self.e, self.f + ) } } impl Default for Matrix { @@ -1101,7 +1448,7 @@ impl ObjectWrite for Matrix { #[cfg(feature = "euclid")] impl Into> for Matrix { fn into(self) -> euclid::Transform2D { - let Matrix { a, b, c, d, e, f} = self; + let Matrix { a, b, c, d, e, f } = self; euclid::Transform2D::new(a, b, c, d, e, f) } @@ -1109,11 +1456,17 @@ impl Into> for Matrix { #[cfg(feature = "euclid")] impl From> for Matrix { fn from(from: euclid::Transform2D) -> Self { - let euclid::Transform2D { m11: a, m12: b, m21: c, m22: d, m31: e, m32: f, .. } = from; + let euclid::Transform2D { + m11: a, + m12: b, + m21: c, + m22: d, + m31: e, + m32: f, + .. + } = from; - Matrix { - a, b, c, d, e, f - } + Matrix { a, b, c, d, e, f } } } @@ -1132,7 +1485,7 @@ pub enum TextMode { FillThenStroke, Invisible, FillAndClip, - StrokeAndClip + StrokeAndClip, } #[derive(Debug, Copy, Clone, PartialEq, DataSize)] @@ -1156,7 +1509,11 @@ pub struct Cmyk { } impl Display for Cmyk { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} {} {} {}", self.cyan, self.magenta, self.yellow, self.key) + write!( + f, + "{} {} {} {}", + self.cyan, self.magenta, self.yellow, self.key + ) } } @@ -1176,146 +1533,241 @@ impl Display for TextDrawAdjusted { } /// Graphics Operator -/// +/// /// See PDF32000 A.2 #[derive(Debug, Clone, DataSize)] pub enum Op { /// Begin a marked comtent sequence - /// + /// /// Pairs with the following EndMarkedContent. - /// + /// /// generated by operators `BMC` and `BDC` - BeginMarkedContent { tag: Name, properties: Option }, + BeginMarkedContent { + tag: Name, + properties: Option, + }, /// End a marked content sequence. - /// + /// /// Pairs with the previous BeginMarkedContent. - /// + /// /// generated by operator `EMC` EndMarkedContent, /// A marked content point. - /// + /// /// generated by operators `MP` and `DP`. - MarkedContentPoint { tag: Name, properties: Option }, - + MarkedContentPoint { + tag: Name, + properties: Option, + }, Close, - MoveTo { p: Point }, - LineTo { p: Point }, - CurveTo { c1: Point, c2: Point, p: Point }, - Rect { rect: ViewRect }, + MoveTo { + p: Point, + }, + LineTo { + p: Point, + }, + CurveTo { + c1: Point, + c2: Point, + p: Point, + }, + Rect { + rect: ViewRect, + }, EndPath, Stroke, /// Fill and Stroke operation - /// + /// /// generated by operators `b`, `B`, `b*`, `B*` /// `close` indicates whether the path should be closed first - FillAndStroke { winding: Winding }, + FillAndStroke { + winding: Winding, + }, - - Fill { winding: Winding }, + Fill { + winding: Winding, + }, /// Fill using the named shading pattern - /// + /// /// operator: `sh` - Shade { name: Name }, + Shade { + name: Name, + }, - Clip { winding: Winding }, + Clip { + winding: Winding, + }, Save, Restore, - Transform { matrix: Matrix }, + Transform { + matrix: Matrix, + }, - LineWidth { width: f32 }, - Dash { pattern: Vec, phase: f32 }, - LineJoin { join: LineJoin }, - LineCap { cap: LineCap }, - MiterLimit { limit: f32 }, - Flatness { tolerance: f32 }, + LineWidth { + width: f32, + }, + Dash { + pattern: Vec, + phase: f32, + }, + LineJoin { + join: LineJoin, + }, + LineCap { + cap: LineCap, + }, + MiterLimit { + limit: f32, + }, + Flatness { + tolerance: f32, + }, - GraphicsState { name: Name }, + GraphicsState { + name: Name, + }, - StrokeColor { color: Color }, - FillColor { color: Color }, + StrokeColor { + color: Color, + }, + FillColor { + color: Color, + }, - FillColorSpace { name: Name }, - StrokeColorSpace { name: Name }, + FillColorSpace { + name: Name, + }, + StrokeColorSpace { + name: Name, + }, - RenderingIntent { intent: RenderingIntent }, + RenderingIntent { + intent: RenderingIntent, + }, BeginText, EndText, - CharSpacing { char_space: f32 }, - WordSpacing { word_space: f32 }, - TextScaling { horiz_scale: f32 }, - Leading { leading: f32 }, - TextFont { name: Name, size: f32 }, - TextRenderMode { mode: TextMode }, + CharSpacing { + char_space: f32, + }, + WordSpacing { + word_space: f32, + }, + TextScaling { + horiz_scale: f32, + }, + Leading { + leading: f32, + }, + TextFont { + name: Name, + size: f32, + }, + TextRenderMode { + mode: TextMode, + }, /// `Ts` - TextRise { rise: f32 }, + TextRise { + rise: f32, + }, /// `Td`, `TD` - MoveTextPosition { translation: Point }, + MoveTextPosition { + translation: Point, + }, /// `Tm` - SetTextMatrix { matrix: Matrix }, + SetTextMatrix { + matrix: Matrix, + }, /// `T*` TextNewline, /// `Tj` - TextDraw { text: PdfString }, + TextDraw { + text: PdfString, + }, - TextDrawAdjusted { array: Vec }, + TextDrawAdjusted { + array: Vec, + }, - XObject { name: Name }, + XObject { + name: Name, + }, - InlineImage { image: Arc }, + InlineImage { + image: Arc, + }, } -pub fn deep_clone_op(op: &Op, cloner: &mut impl Cloner, old_resources: &Resources, resources: &mut Resources) -> Result { +pub fn deep_clone_op( + op: &Op, + cloner: &mut impl Cloner, + old_resources: &Resources, + resources: &mut Resources, +) -> Result { match *op { Op::GraphicsState { ref name } => { if !resources.graphics_states.contains_key(name) { if let Some(gs) = old_resources.graphics_states.get(name) { - resources.graphics_states.insert(name.clone(), gs.deep_clone(cloner)?); + resources + .graphics_states + .insert(name.clone(), gs.deep_clone(cloner)?); } } Ok(Op::GraphicsState { name: name.clone() }) } - Op::MarkedContentPoint { ref tag, ref properties } => { - Ok(Op::MarkedContentPoint { tag: tag.clone(), properties: properties.deep_clone(cloner)? }) - } - Op::BeginMarkedContent { ref tag, ref properties } => { - Ok(Op::BeginMarkedContent { tag: tag.clone(), properties: properties.deep_clone(cloner)? }) - } + Op::MarkedContentPoint { + ref tag, + ref properties, + } => Ok(Op::MarkedContentPoint { + tag: tag.clone(), + properties: properties.deep_clone(cloner)?, + }), + Op::BeginMarkedContent { + ref tag, + ref properties, + } => Ok(Op::BeginMarkedContent { + tag: tag.clone(), + properties: properties.deep_clone(cloner)?, + }), Op::TextFont { ref name, size } => { if !resources.fonts.contains_key(name) { if let Some(f) = old_resources.fonts.get(name) { resources.fonts.insert(name.clone(), f.deep_clone(cloner)?); } } - Ok(Op::TextFont { name: name.clone(), size }) + Ok(Op::TextFont { + name: name.clone(), + size, + }) } Op::XObject { ref name } => { if !resources.xobjects.contains_key(name) { if let Some(xo) = old_resources.xobjects.get(name) { - resources.xobjects.insert(name.clone(), xo.deep_clone(cloner)?); + resources + .xobjects + .insert(name.clone(), xo.deep_clone(cloner)?); } } Ok(Op::XObject { name: name.clone() }) } - ref op => Ok(op.clone()) + ref op => Ok(op.clone()), } } - #[cfg(test)] mod tests { use super::*; @@ -1334,6 +1786,6 @@ Gb"0F_%"1Ö"#B1qiGGG^V6GZ#ZkijB5'RjB4S^5I61&$Ni:Xh=4S_9KYN;c9MUZPn/h,c]oCLUmg EI "###; let mut lexer = Lexer::new(data); - assert!(inline_image(&mut lexer, &NoResolve).is_ok()); + assert!(inline_image(&mut lexer, &NoResolve).is_ok()); } } diff --git a/src-pdfrs/pdf/src/primitive.rs b/src-pdfrs/pdf/src/primitive.rs index 00b51bd..275ddba 100644 --- a/src-pdfrs/pdf/src/primitive.rs +++ b/src-pdfrs/pdf/src/primitive.rs @@ -249,7 +249,7 @@ impl<'a> IntoIterator for &'a Dictionary { #[derive(Clone, Debug, PartialEq, DataSize)] pub struct PdfStream { pub info: Dictionary, - pub (crate) inner: StreamInner, + pub inner: StreamInner, } #[derive(Clone, Debug, PartialEq, DataSize)] diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c04a5c6..2d2f658 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1011,6 +1011,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" +[[package]] +name = "data-encoding" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" + [[package]] name = "datasize" version = "0.2.15" @@ -3681,6 +3687,7 @@ name = "pdf-forge" version = "0.1.0" dependencies = [ "image 0.25.5", + "indexmap 1.9.3", "lazy_static", "pathfinder_geometry", "pathfinder_rasterize", @@ -3694,6 +3701,7 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-opener", + "tauri-plugin-websocket", "uuid", ] @@ -4811,6 +4819,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" @@ -5350,6 +5369,25 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-websocket" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3ac71aec5fb0ae5441e830cd075b1cbed49ac3d39cb975a4894ea8fa2e62b9" +dependencies = [ + "futures-util", + "http", + "log", + "rand 0.8.5", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.11", + "tokio", + "tokio-tungstenite", +] + [[package]] name = "tauri-runtime" version = "2.3.0" @@ -5607,6 +5645,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4bf6fecd69fcdede0ec680aaf474cdab988f9de6bc73d3758f0160e3b7025a" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots", +] + [[package]] name = "tokio-util" version = "0.7.13" @@ -5775,6 +5829,26 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.11", + "utf-8", +] + [[package]] name = "tuple" version = "0.5.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5ed0d78..0f27e34 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -33,3 +33,5 @@ image = { version = "0.25.5", features = ["jpeg"] } pdf_render = { path = "../../pdf-render/render" } pathfinder_rasterize = { git = "https://github.com/s3bk/pathfinder_rasterizer" } pathfinder_geometry = { git = "https://github.com/servo/pathfinder" } +indexmap = "1.9.3" +tauri-plugin-websocket = "2" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7a3a4d6..364c59b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,14 +11,13 @@ use crate::render::Renderer; use image::DynamicImage; use lazy_static::lazy_static; use pdf::file::{File, FileOptions, NoLog, ObjectCache, StreamCache}; -use pdf::object::{Object, PlainRef}; +use pdf::object::{Object, PlainRef, Stream, Updater}; use pdf::primitive::{Dictionary, Primitive}; use pdf::xref::XRef; use regex::Regex; use retrieval::{get_prim_by_path_with_file, get_stream_data_by_path_with_file}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, VecDeque}; -use std::ops::DerefMut; use std::path::Path; use std::sync::{Arc, RwLock}; use tauri::ipc::{InvokeResponseBody, Response}; @@ -26,7 +25,6 @@ use tauri::{Manager, State}; use uuid::Uuid; type CosFile = File, ObjectCache, StreamCache, NoLog>; - #[derive(Serialize, Debug, Clone)] pub struct XRefTableModel { pub size: usize, @@ -113,7 +111,7 @@ fn get_all_files(session: State) -> Vec { session .get_all_files() .iter() - .map(|s| s.pdf_file.clone()) + .map(|s| s.read().unwrap().pdf_file.clone()) .collect() } @@ -129,7 +127,9 @@ fn close_file(id: &str, session: State) { #[tauri::command] fn get_file_by_id(id: &str, session: State) -> Result { - session.get_file(id).map(|file| file.pdf_file.clone()) + let lock = session.get_file(id)?; + let file = lock.read().unwrap(); + Ok(file.pdf_file.clone()) } #[tauri::command] @@ -183,7 +183,8 @@ fn to_pdf_file(path: &str, file: &CosFile) -> Result { #[tauri::command] fn get_contents(id: &str, path: &str, session: State) -> Result { - let file = session.get_file(id)?; + let lock = session.get_file(id)?; + let file = lock.read().unwrap(); let (_, page_prim, _) = get_prim_by_path_with_file(path, &file.cos_file)?; let resolver = file.cos_file.resolver(); @@ -208,11 +209,30 @@ fn get_stream_data_as_string( path: &str, session: State, ) -> Result { - let file = session.get_file(id)?; + let lock = session.get_file(id)?; + let file = lock.read().unwrap(); let data = get_stream_data_by_path_with_file(path, &file.cos_file)?; Ok(String::from_utf8_lossy(&data).into_owned()) } +#[tauri::command] +async fn update_stream_data_as_string( + id: &str, + path: &str, + new_data: &str, + session: State<'_, Session>, +) -> Result<(), String> { + let lock = session.get_file(id)?; + let mut file = lock.write().unwrap(); + let (_, _, trace) = get_prim_by_path_with_file(path, &file.cos_file)?; + let i = trace.len() - 1; + let id = t!(trace[i].last_jump.parse::()); + let plain_ref = PlainRef { id, gen: 0 }; + let stream = Stream::new((), new_data.as_bytes()); + let _ = t!(file.cos_file.update(plain_ref, stream)); + Ok(()) +} + fn format_img_as_response(img: DynamicImage) -> Result { use std::mem; @@ -248,7 +268,8 @@ async fn get_stream_data_as_image( path: &str, session: State<'_, Session>, ) -> Result { - let file = session.get_file(id)?; + let lock = session.get_file(id)?; + let file = lock.read().unwrap(); let img = retrieval::get_image_by_path(path, &file.cos_file)?; format_img_as_response(img) } @@ -259,21 +280,39 @@ async fn get_page_by_num( num: u32, session: State<'_, Session>, ) -> Result { - let file = session.get_file(id)?; + render_page_by_path(id, &format!("Page{}", num), session).await +} + +#[tauri::command] +async fn render_page_by_path( + id: &str, + path: &str, + session: State<'_, Session>, +) -> Result { + let lock = session.get_file(id)?; + let file = lock.read().unwrap(); + let page = get_page_by_path(path, &file.cos_file)?; let mut renderer = Renderer::new(&file.cos_file, 150); - let img = renderer.render(num)?; + let img = renderer.render(&page)?; format_img_as_response(img) } +fn get_page_by_path(path: &str, file: &CosFile) -> Result { + let resolver = file.resolver(); + let (_, page_prim, _) = get_prim_by_path_with_file(path, file)?; + let page = t!(pdf::object::Page::from_primitive(page_prim, &resolver)); + Ok(page) +} + #[tauri::command] fn get_prim_by_path( id: &str, path: &str, session: State, ) -> Result { - let file = session.get_file(id)?; - + let lock = session.get_file(id)?; + let file = lock.read().unwrap(); get_prim_model_by_path_with_file(path, &file.cos_file) } @@ -313,7 +352,8 @@ fn get_prim_tree_by_path( paths: Vec, session: State, ) -> Result, String> { - let file = session.get_file(id)?; + let lock = session.get_file(id)?; + let file = lock.read().unwrap(); let results = paths .into_iter() @@ -659,9 +699,11 @@ impl PrimitiveTreeView { } #[tauri::command] fn get_xref_table(id: &str, session: State) -> Result { - let file = session.get_file(id)?; + let lock = session.get_file(id)?; + let file = lock.read().unwrap(); get_xref_table_model_with_file(&file.cos_file) } + fn get_xref_table_model_with_file(file: &CosFile) -> Result { let resolver = file.resolver(); let x_ref_table = file.get_xref(); @@ -719,7 +761,7 @@ fn get_xref_table_model_with_file(file: &CosFile) -> Result>>, + files: RwLock>>>, } struct SessionFile { @@ -734,20 +776,15 @@ impl Session { } } - fn get_file(&self, id: &str) -> Result, String> { + fn get_file(&self, id: &str) -> Result>, String> { let lock = self.files.read().unwrap(); lock.get(id) .cloned() .ok_or(format!(" File {} not found!", id)) } - fn get_all_files(&self) -> Vec> { - self.files - .read() - .unwrap() - .values() - .map(|f| f.clone()) - .collect() + fn get_all_files(&self) -> Vec>> { + self.files.read().unwrap().values().cloned().collect() } fn get_all_file_ids(&self) -> Vec { @@ -763,10 +800,10 @@ impl Session { if let Ok(mut files) = self.files.write() { return match files.insert( pdf_file.id.clone(), - Arc::new(SessionFile { + Arc::new(RwLock::new(SessionFile { pdf_file: pdf_file, cos_file: cos_file, - }), + })), ) { Some(_) => Err("File could not be uploaded!".to_string()), None => Ok(()), @@ -802,7 +839,8 @@ pub fn run() { get_contents, get_stream_data_as_string, get_stream_data_as_image, - get_page_by_num + get_page_by_num, + update_stream_data_as_string ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/render.rs b/src-tauri/src/render.rs index ff5c21d..cc7e4f8 100644 --- a/src-tauri/src/render.rs +++ b/src-tauri/src/render.rs @@ -5,6 +5,7 @@ use crate::{t, CosFile}; use image::{DynamicImage, RgbaImage}; use pathfinder_geometry::transform2d::Transform2F; use pathfinder_rasterize::Rasterizer; +use pdf::object::Page; use pdf_render::{render_page, Cache, SceneBackend}; pub struct Renderer<'a> { @@ -22,9 +23,8 @@ impl<'a> Renderer<'a> { } } - pub fn render(&mut self, page_num: u32) -> Result { + pub fn render(&mut self, page: &Page) -> Result { let resolver = self.file.resolver(); - let page = t!(self.file.get_page(page_num - 1)); let mut backend = SceneBackend::new(&mut self.cache); t!(render_page(&mut backend, &resolver, &page, self.transform)); let scene = backend.finish(); diff --git a/src/components/App.svelte b/src/components/App.svelte index df48bb1..c185d5e 100644 --- a/src/components/App.svelte +++ b/src/components/App.svelte @@ -97,6 +97,7 @@ closeTab={closeFile} openTab={upload} selectTab={(state) => (fState = state)} + reload={() => fState?.reload()} >
diff --git a/src/components/PageView.svelte b/src/components/PageView.svelte index bc83ce6..688092c 100644 --- a/src/components/PageView.svelte +++ b/src/components/PageView.svelte @@ -1,6 +1,4 @@ -{#if fState.container_prim && fState.container_prim.isPage() && state} +{#if state}
- +
diff --git a/src/components/PrimitiveView.svelte b/src/components/PrimitiveView.svelte index d108799..fed77a3 100644 --- a/src/components/PrimitiveView.svelte +++ b/src/components/PrimitiveView.svelte @@ -2,12 +2,12 @@ import type FileViewState from "../models/FileViewState.svelte"; import type Primitive from "../models/Primitive.svelte"; import PrimitiveIcon from "./PrimitiveIcon.svelte"; - import {Pane, Splitpanes} from "svelte-splitpanes"; + import { Pane, Splitpanes } from "svelte-splitpanes"; import StreamDataView from "./StreamDataView.svelte"; const cellH = 29; const headerOffset = 24; - let {fState, height}: { fState: FileViewState; height: number } = + let { fState, height }: { fState: FileViewState; height: number } = $props(); let fillerHeight: number = $state(0); let firstEntry = $state(0); @@ -20,9 +20,6 @@ let tableHeight = $derived(prim ? prim.children.length * cellH : 0); let bodyHeight = $derived(height); - let editorHeight = $derived( - Math.max(800, bodyHeight - tableHeight - headerOffset), - ); let locallySelected: Primitive | undefined = $state(undefined); $effect(() => { @@ -50,53 +47,65 @@ fState.selectPath(_path); } - + + {#if prim && prim.children && prim.children.length > 0}
- - - - - + + + + +
Key - Type - Value
Key + Type + Value
- - {#each entriesToDisplay as entry} + {#each entriesToDisplay as entry} + handlePrimClick(entry)} - ondblclick={() => handlePrimDbLClick(entry)} - > - - - - - {/each} + ondblclick={() => + handlePrimDbLClick(entry)} + > + + + + + {/each}
-
- -

- {entry.key} -

-
-
{entry.ptype}{entry.value}
+
+ +

+ {entry.key} +

+
+
{entry.ptype}{entry.value}
@@ -105,17 +114,18 @@ {/if} -
+
{#if fState.container_prim?.stream_data} {/if}
+ \ No newline at end of file + diff --git a/src/components/StreamEditor.svelte b/src/components/StreamEditor.svelte index 7285517..e9308c6 100644 --- a/src/components/StreamEditor.svelte +++ b/src/components/StreamEditor.svelte @@ -1,20 +1,32 @@ -
+
+
+ {#if isEdited} + + {/if} +
diff --git a/src/components/TitleBar.svelte b/src/components/TitleBar.svelte index a5b8c2e..05cfcbc 100644 --- a/src/components/TitleBar.svelte +++ b/src/components/TitleBar.svelte @@ -6,6 +6,7 @@ CaretDownOutline, PlusOutline, FilePdfSolid, + ArrowsRepeatOutline, } from "flowbite-svelte-icons"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { homeDir } from "@tauri-apps/api/path"; @@ -21,12 +22,14 @@ selectTab, closeTab, openTab: newFile, + reload }: { fStates: FileViewState[]; fState: FileViewState | undefined; selectTab: (arg0: FileViewState) => void; closeTab: (arg0: FileViewState) => void; openTab: () => void; + reload: () => void; } = $props(); let dropdownVisible = $state(false); @@ -167,7 +170,15 @@ {/if}
+
+