From: Ben Pfaff Date: Tue, 30 Dec 2025 16:36:41 +0000 (-0800) Subject: work X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=dbab0728d6ade6ff75ff03e712ca3643ff25ffd4;p=pspp work --- diff --git a/rust/pspp/src/output.rs b/rust/pspp/src/output.rs index 76b45cb9f9..edcecfd6fb 100644 --- a/rust/pspp/src/output.rs +++ b/rust/pspp/src/output.rs @@ -196,7 +196,7 @@ impl Heading { #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ItemKind { - Chart, + Graph, Image, Heading, Message, @@ -208,7 +208,7 @@ pub enum ItemKind { impl ItemKind { pub fn as_str(&self) -> &'static str { match self { - ItemKind::Chart => "chart", + ItemKind::Graph => "graph", ItemKind::Image => "image", ItemKind::Heading => "heading", ItemKind::Message => "message", @@ -227,7 +227,7 @@ impl Display for ItemKind { #[derive(Clone, Debug, Serialize)] pub enum Details { - Chart, + Graph, Image(#[serde(skip_serializing)] ImageSurface), Heading(Heading), Message(Box), @@ -285,7 +285,7 @@ impl Details { pub fn command_name(&self) -> Option<&String> { match self { - Details::Chart + Details::Graph | Details::Image(_) | Details::Heading(_) | Details::Message(_) @@ -297,7 +297,7 @@ impl Details { pub fn label(&self) -> Cow<'static, str> { match self { - Details::Chart => Cow::from("chart"), + Details::Graph => Cow::from("Graph"), Details::Image(_) => Cow::from("Image"), Details::Heading(_) => Cow::from("Group"), Details::Message(diagnostic) => Cow::from(diagnostic.severity.as_title_str()), @@ -329,7 +329,7 @@ impl Details { pub fn kind(&self) -> ItemKind { match self { - Details::Chart => ItemKind::Chart, + Details::Graph => ItemKind::Graph, Details::Image(_) => ItemKind::Image, Details::Heading(_) => ItemKind::Heading, Details::Message(_) => ItemKind::Message, @@ -391,11 +391,11 @@ impl From> for Details { } #[derive(Clone, Debug, Serialize)] -pub struct Chart; +pub struct Graph; -impl Chart { +impl Graph { pub fn into_item(self) -> Item { - Details::Chart.into_item() + Details::Graph.into_item() } } @@ -619,18 +619,12 @@ impl SpvInfo { } } - pub fn with_members(self, members: SpvMembers) -> Self { - Self { - members: Some(members), - ..self - } + pub fn with_members(self, members: Option) -> Self { + Self { members, ..self } } - pub fn with_error(self) -> Self { - Self { - error: true, - ..self - } + pub fn with_error(self, error: bool) -> Self { + Self { error, ..self } } pub fn member_names(&self) -> Vec<&str> { @@ -662,7 +656,7 @@ pub enum SpvMembers { /// `.png` file. String, ), - /// Chart members. + /// Graph members. Graph { /// Data member name. data: Option, @@ -693,7 +687,7 @@ impl SpvMembers { /// output item types; for example, "warnings" are a subset of text items. #[derive(Debug, EnumSetType)] pub enum Class { - Charts, + Graphs, Headings, Logs, Models, @@ -713,7 +707,7 @@ impl FromStr for Class { fn from_str(s: &str) -> Result { match s { - "charts" => Ok(Self::Charts), + "graphs" | "charts" => Ok(Self::Graphs), "headings" => Ok(Self::Headings), "logs" => Ok(Self::Logs), "models" => Ok(Self::Models), @@ -735,7 +729,7 @@ impl Item { fn class(&self) -> Class { let label = self.label.as_ref().map(|s| s.as_str()); match &self.details { - Details::Chart => Class::Charts, + Details::Graph => Class::Graphs, Details::Image(_) => Class::Other, Details::Heading(_) => Class::OutlineHeaders, Details::Message(diagnostic) => match diagnostic.severity { diff --git a/rust/pspp/src/output/drivers/csv.rs b/rust/pspp/src/output/drivers/csv.rs index b34214a6cf..1ed5e1b163 100644 --- a/rust/pspp/src/output/drivers/csv.rs +++ b/rust/pspp/src/output/drivers/csv.rs @@ -360,7 +360,7 @@ impl Driver for CsvDriver { fn write(&mut self, item: &Arc) { // todo: error handling (should not unwrap) match &item.details { - Details::Chart | Details::Image(_) | Details::Heading(_) => (), + Details::Graph | Details::Image(_) | Details::Heading(_) => (), Details::Message(diagnostic) => { self.start_item(); let text = diagnostic.to_string(); diff --git a/rust/pspp/src/output/drivers/html.rs b/rust/pspp/src/output/drivers/html.rs index 77b549963b..ab50c15778 100644 --- a/rust/pspp/src/output/drivers/html.rs +++ b/rust/pspp/src/output/drivers/html.rs @@ -426,7 +426,7 @@ where fn write(&mut self, item: &Arc) { match &item.details { - Details::Chart | Details::Image(_) | Details::Heading(_) => todo!(), + Details::Graph | Details::Image(_) | Details::Heading(_) => todo!(), Details::Message(_diagnostic) => todo!(), Details::PageBreak => (), Details::Table(pivot_table) => { diff --git a/rust/pspp/src/output/drivers/text.rs b/rust/pspp/src/output/drivers/text.rs index 5a11d6190c..af6a106cbb 100644 --- a/rust/pspp/src/output/drivers/text.rs +++ b/rust/pspp/src/output/drivers/text.rs @@ -403,7 +403,7 @@ impl TextRenderer { for item in ItemRefIterator::without_hidden(item).filter(|item| !item.details.is_heading()) { match &item.details { - Details::Chart => { + Details::Graph => { self.start_object(writer)?; writeln!(writer, "Omitting chart from text output")? } diff --git a/rust/pspp/src/spv/read.rs b/rust/pspp/src/spv/read.rs index 3dfeb4d669..10a7e5a22c 100644 --- a/rust/pspp/src/spv/read.rs +++ b/rust/pspp/src/spv/read.rs @@ -18,32 +18,19 @@ use std::{ cell::RefCell, fmt::Display, fs::File, - io::{BufReader, Cursor, Read, Seek}, + io::{BufReader, Read, Seek}, path::Path, rc::Rc, }; -use anyhow::Context; -use binrw::{BinRead, error::ContextExt}; -use cairo::ImageSurface; use displaydoc::Display; -use paper_sizes::PaperSize; use serde::Deserialize; use zip::{ZipArchive, result::ZipError}; use crate::{ crypto::EncryptedReader, - output::{ - Details, Item, SpvInfo, SpvMembers, Text, - page::{self, Orientation}, - pivot::{Axis2, Length, TableProperties, look::Look, value::Value}, - }, - spv::read::{ - html::Document, - legacy_bin::LegacyBin, - legacy_xml::Visualization, - light::{LightTable, LightWarning}, - }, + output::{Item, page::PageSetup}, + spv::read::light::LightWarning, }; mod css; @@ -51,6 +38,7 @@ pub mod html; pub mod legacy_bin; mod legacy_xml; mod light; +mod structure; #[cfg(test)] mod tests; @@ -146,14 +134,14 @@ where ZipError::InvalidArchive(_) => Error::NotSpv, other => other.into(), })?; - Ok(self.open_zip_archive(archive)?) + Ok(self.open_archive(archive)?) } /// Opens the provided Zip `archive`. /// /// Any password provided for reading the file is unused, because if one was /// needed then it must have already been used to open the archive. - pub fn open_zip_archive( + pub fn open_archive( self, mut archive: ZipArchive>, ) -> Result { @@ -175,9 +163,10 @@ where for i in 0..archive.len() { let name = String::from(archive.name_for_index(i).unwrap()); if name.starts_with("outputViewer") && name.ends_with(".xml") { - let (mut new_items, ps) = read_heading(&mut archive, i, &name, &warn)?; - items.append(&mut new_items); - page_setup = page_setup.or(ps); + let member = BufReader::new(archive.by_index(i)?); + let member = structure::StructureMember::read(member, &name, &warn)?; + items.extend(member.root.read_items(&mut archive, &name, &warn)); + page_setup = page_setup.or(member.page_setup); } } @@ -195,7 +184,7 @@ pub struct SpvFile { pub items: Vec, /// The page setup in the SPV file, if any. - pub page_setup: Option, + pub page_setup: Option, /// The Zip archive that the file was read from. pub archive: ZipArchive>, @@ -203,7 +192,7 @@ pub struct SpvFile { impl SpvFile { /// Returns the contents of the `SpvFile`. - pub fn into_contents(self) -> (Vec, Option) { + pub fn into_contents(self) -> (Vec, Option) { (self.items, self.page_setup) } @@ -238,565 +227,23 @@ pub enum Error { /// {0} CairoError(#[from] cairo::IoError), -} - -fn new_error_item(message: impl Into) -> Item { - Text::new_log(message).into_item().with_label("Error") -} - -fn read_heading( - archive: &mut ZipArchive, - file_number: usize, - structure_member: &str, - warn: &Rc>>, -) -> Result<(Vec, Option), Error> -where - R: Read + Seek, -{ - let member = BufReader::new(archive.by_index(file_number)?); - let mut heading: Heading = match serde_path_to_error::deserialize( - &mut quick_xml::de::Deserializer::from_reader(member), - ) - .with_context(|| format!("Failed to parse {structure_member}")) - { - Ok(result) => result, - Err(error) => panic!("{error:?}"), - }; - let page_setup = heading - .page_setup - .take() - .map(|ps| ps.decode(warn, structure_member)); - Ok((heading.decode(archive, structure_member, warn)?, page_setup)) -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Heading { - #[serde(rename = "@visibility")] - visibility: Option, - #[serde(rename = "@commandName")] - command_name: Option, - label: Label, - page_setup: Option, - - #[serde(rename = "$value")] - #[serde(default)] - children: Vec, -} - -impl Heading { - fn decode( - self, - archive: &mut ZipArchive, - structure_member: &str, - warn: &Rc>>, - ) -> Result, Error> - where - R: Read + Seek, - { - let mut items = Vec::new(); - for child in self.children { - match child { - HeadingContent::Container(container) => { - if container.page_break_before == PageBreakBefore::Always { - items.push( - Details::PageBreak - .into_item() - .with_spv_info(SpvInfo::new(structure_member)), - ); - } - let item = match container.content { - ContainerContent::Table(table) => table - .decode(archive, structure_member, warn) - .unwrap_or_else(|error| { - new_error_item(format!("Error reading table: {error}")) - .with_spv_info(SpvInfo::new(structure_member).with_error()) - }), - ContainerContent::Graph(graph) => graph.decode(structure_member), - ContainerContent::Text(container_text) => Text::new( - match container_text.text_type { - TextType::Title => crate::output::TextType::Title, - TextType::Log | TextType::Text => crate::output::TextType::Log, - TextType::PageTitle => crate::output::TextType::PageTitle, - }, - container_text.decode(), - ) - .into_item() - .with_command_name(container_text.command_name) - .with_spv_info(SpvInfo::new(structure_member)), - ContainerContent::Image(image) => image - .decode(archive, structure_member) - .unwrap_or_else(|error| { - new_error_item(format!("Error reading image: {error}")) - .with_spv_info(SpvInfo::new(structure_member).with_error()) - }), - ContainerContent::Object(object) => object - .decode(archive, structure_member) - .unwrap_or_else(|error| { - new_error_item(format!("Error reading object: {error}")) - .with_spv_info(SpvInfo::new(structure_member).with_error()) - }), - ContainerContent::Model => new_error_item("models not yet implemented") - .with_spv_info(SpvInfo::new(structure_member).with_error()), - ContainerContent::Tree => new_error_item("trees not yet implemented") - .with_spv_info(SpvInfo::new(structure_member).with_error()), - }; - items.push(item.with_show(container.visibility == Visibility::Visible)); - } - HeadingContent::Heading(mut heading) => { - let show = !heading.visibility.is_some(); - let label = std::mem::take(&mut heading.label.text); - let command_name = heading.command_name.take(); - items.push( - heading - .decode(archive, structure_member, warn)? - .into_iter() - .collect::() - .with_show(show) - .with_label(label) - .with_command_name(command_name) - .with_spv_info(SpvInfo::new(structure_member)), - ); - } - } - } - Ok(items) - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PageSetup { - #[serde(rename = "@initial-page-number")] - pub initial_page_number: Option, - #[serde(rename = "@chart-size")] - pub chart_size: Option, - #[serde(rename = "@margin-left")] - pub margin_left: Option, - #[serde(rename = "@margin-right")] - pub margin_right: Option, - #[serde(rename = "@margin-top")] - pub margin_top: Option, - #[serde(rename = "@margin-bottom")] - pub margin_bottom: Option, - #[serde(rename = "@paper-height")] - pub paper_height: Option, - #[serde(rename = "@paper-width")] - pub paper_width: Option, - #[serde(rename = "@reference-orientation")] - pub reference_orientation: Option, - #[serde(rename = "@space-after")] - pub space_after: Option, - pub page_header: PageHeader, - pub page_footer: PageFooter, -} - -impl PageSetup { - fn decode( - &self, - warn: &Rc>>, - structure_member: &str, - ) -> page::PageSetup { - let mut setup = page::PageSetup::default(); - if let Some(initial_page_number) = self.initial_page_number { - setup.initial_page_number = initial_page_number; - } - if let Some(chart_size) = self.chart_size { - setup.chart_size = chart_size.into(); - } - if let Some(margin_left) = self.margin_left { - setup.margins.0[Axis2::X][0] = margin_left.into(); - } - if let Some(margin_right) = self.margin_right { - setup.margins.0[Axis2::X][1] = margin_right.into(); - } - if let Some(margin_top) = self.margin_top { - setup.margins.0[Axis2::Y][0] = margin_top.into(); - } - if let Some(margin_bottom) = self.margin_bottom { - setup.margins.0[Axis2::Y][1] = margin_bottom.into(); - } - match (self.paper_width, self.paper_height) { - (Some(width), Some(height)) => { - setup.paper = PaperSize::new(width.0, height.0, paper_sizes::Unit::Inch) - } - (Some(length), None) | (None, Some(length)) => { - setup.paper = PaperSize::new(length.0, length.0, paper_sizes::Unit::Inch) - } - (None, None) => (), - } - if let Some(reference_orientation) = &self.reference_orientation { - if reference_orientation.starts_with("0") { - setup.orientation = Orientation::Portrait; - } else if reference_orientation.starts_with("90") { - setup.orientation = Orientation::Landscape; - } else { - (warn.borrow_mut())(Warning { - member: structure_member.into(), - details: WarningDetails::UnknownOrientation(reference_orientation.clone()), - }); - } - } - if let Some(space_after) = self.space_after { - setup.object_spacing = space_after.into(); - } - if let Some(PageParagraph { text }) = &self.page_header.page_paragraph { - setup.header = text.decode(); - } - if let Some(PageParagraph { text }) = &self.page_footer.page_paragraph { - setup.footer = text.decode(); - } - setup - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PageHeader { - page_paragraph: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PageFooter { - page_paragraph: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PageParagraph { - text: PageParagraphText, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PageParagraphText { - #[serde(default, rename = "$text")] - text: String, -} - -impl PageParagraphText { - fn decode(&self) -> Document { - Document::from_html(&self.text) - } -} - -#[derive(Copy, Clone, Debug, Default, Deserialize)] -#[serde(rename = "snake_case")] -enum ReferenceOrientation { - #[serde(alias = "0")] - #[serde(alias = "0deg")] - #[serde(alias = "inherit")] - #[default] - Portrait, - - #[serde(alias = "90")] - #[serde(alias = "90deg")] - #[serde(alias = "-270")] - #[serde(alias = "-270deg")] - Landscape, - - #[serde(alias = "180")] - #[serde(alias = "180deg")] - #[serde(alias = "-1280")] - #[serde(alias = "-180deg")] - ReversePortrait, - - #[serde(alias = "270")] - #[serde(alias = "270deg")] - #[serde(alias = "-90")] - #[serde(alias = "-90deg")] - Seascape, -} - -impl From for page::Orientation { - fn from(value: ReferenceOrientation) -> Self { - match value { - ReferenceOrientation::Portrait | ReferenceOrientation::ReversePortrait => { - page::Orientation::Portrait - } - ReferenceOrientation::Landscape | ReferenceOrientation::Seascape => { - page::Orientation::Landscape - } - } - } -} - -/// Chart size. -#[derive(Copy, Clone, Debug, Default, Deserialize)] -#[serde(rename_all = "kebab-case")] -enum ChartSize { - FullHeight, - HalfHeight, - QuarterHeight, - #[default] - #[serde(other)] - AsIs, -} - -impl From for page::ChartSize { - fn from(value: ChartSize) -> Self { - match value { - ChartSize::AsIs => page::ChartSize::AsIs, - ChartSize::FullHeight => page::ChartSize::FullHeight, - ChartSize::HalfHeight => page::ChartSize::HalfHeight, - ChartSize::QuarterHeight => page::ChartSize::QuarterHeight, - } - } -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -enum HeadingContent { - Container(Container), - Heading(Box), -} -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Label { - #[serde(default, rename = "$text")] - text: String, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Container { - #[serde(default, rename = "@visibility")] - visibility: Visibility, - #[serde(rename = "@page-break-before")] - #[serde(default)] - page_break_before: PageBreakBefore, - #[serde(rename = "@text-align")] - text_align: Option, - #[serde(rename = "@width")] - width: Option, - label: Label, - - #[serde(rename = "$value")] - content: ContainerContent, -} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Deserialize)] -#[serde(rename_all = "camelCase")] -enum PageBreakBefore { - #[default] - Auto, - Always, - Avoid, - Left, - Right, - Inherit, -} - -#[derive(Deserialize, Debug, Default, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -enum Visibility { - #[default] - Visible, - Hidden, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -enum TextAlign { - Left, - Center, - Right, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -enum ContainerContent { - Table(Table), - Text(ContainerText), - Graph(Graph), - Model, - Object(Object), - Image(Image), - Tree, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Graph { - #[serde(rename = "@commandName")] - command_name: String, - data_path: Option, - path: String, - csv_path: Option, -} - -impl Graph { - fn decode(&self, structure_member: &str) -> Item { - crate::output::Chart - .into_item() - .with_spv_info( - SpvInfo::new(structure_member).with_members(SpvMembers::Graph { - data: self.data_path.clone(), - xml: self.path.clone(), - csv: self.csv_path.clone(), - }), - ) - } -} - -fn decode_image( - archive: &mut ZipArchive, - structure_member: &str, - command_name: &Option, - image_name: &str, -) -> Result -where - R: Read + Seek, -{ - let mut png = archive.by_name(image_name)?; - let image = ImageSurface::create_from_png(&mut png)?; - Ok(Details::Image(image) - .into_item() - .with_command_name(command_name.clone()) - .with_spv_info( - SpvInfo::new(structure_member).with_members(SpvMembers::Image(image_name.into())), - )) -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Image { - #[serde(rename = "@commandName")] - command_name: Option, - data_path: String, -} - -impl Image { - fn decode(&self, archive: &mut ZipArchive, structure_member: &str) -> Result - where - R: Read + Seek, - { - decode_image( - archive, - structure_member, - &self.command_name, - &self.data_path, - ) - } -} + /// Error parsing {member_name}: {error} + DeserializeError { + /// The name of the file inside the ZIP file that caused the error. + member_name: String, + /// Underlying error. + error: serde_path_to_error::Error, + }, -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Object { - #[serde(rename = "@commandName")] - command_name: Option, - #[serde(rename = "@uri")] - uri: String, -} + /// Graphs not yet implemented. + GraphTodo, -impl Object { - fn decode(&self, archive: &mut ZipArchive, structure_member: &str) -> Result - where - R: Read + Seek, - { - decode_image(archive, structure_member, &self.command_name, &self.uri) - } -} + /// Models not yet implemented. + ModelTodo, -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Table { - #[serde(rename = "@commandName")] - command_name: String, - #[serde(rename = "@subType")] - sub_type: String, - #[serde(rename = "@tableId")] - table_id: Option, - #[serde(rename = "@type")] - table_type: TableType, - table_properties: Option, - table_structure: TableStructure, -} - -impl Table { - fn decode( - &self, - archive: &mut ZipArchive, - structure_member: &str, - warn: &Rc>>, - ) -> Result - where - R: Read + Seek, - { - match &self.table_structure.path { - None => { - let member_name = self.table_structure.data_path.clone(); - let mut light = archive.by_name(&member_name)?; - let mut data = Vec::with_capacity(light.size() as usize); - light.read_to_end(&mut data)?; - let mut cursor = Cursor::new(data); - - let warn = warn.clone(); - let warning = Rc::new(RefCell::new(Box::new({ - let warn = warn.clone(); - let member_name = member_name.clone(); - move |w| { - (warn.borrow_mut())(Warning { - member: member_name.clone(), - details: WarningDetails::LightWarning(w), - }) - } - }) - as Box)); - let table = - LightTable::read_args(&mut cursor, (warning.clone(),)).map_err(|e| { - e.with_message(format!( - "While parsing {member_name:?} as light binary SPV member" - )) - })?; - let pivot_table = table.decode(&mut *warning.borrow_mut()); - Ok(pivot_table.into_item().with_spv_info( - SpvInfo::new(structure_member).with_members(SpvMembers::LightTable( - self.table_structure.data_path.clone(), - )), - )) - } - Some(xml_member_name) => { - let bin_member_name = &self.table_structure.data_path; - let mut bin_member = archive.by_name(bin_member_name)?; - let mut bin_data = Vec::with_capacity(bin_member.size() as usize); - bin_member.read_to_end(&mut bin_data)?; - let mut cursor = Cursor::new(bin_data); - let legacy_bin = LegacyBin::read(&mut cursor).map_err(|e| { - e.with_message(format!( - "While parsing {bin_member_name:?} as legacy binary SPV member" - )) - })?; - let data = legacy_bin.decode(); - drop(bin_member); - - let member = BufReader::new(archive.by_name(&xml_member_name)?); - let visualization: Visualization = match serde_path_to_error::deserialize( - &mut quick_xml::de::Deserializer::from_reader(member), - ) - .with_context(|| format!("Failed to parse {xml_member_name}")) - { - Ok(result) => result, - Err(error) => panic!("{error:?}"), - }; - let pivot_table = visualization.decode( - data, - self.table_properties - .as_ref() - .map_or_else(Look::default, |properties| properties.clone().into()), - )?; - - Ok(pivot_table.into_item().with_spv_info( - SpvInfo::new(structure_member).with_members(SpvMembers::LegacyTable { - xml: xml_member_name.clone(), - binary: bin_member_name.clone(), - }), - )) - } - } - } + /// Trees not yet implemented. + TreeTodo, } #[derive(Deserialize, Debug)] @@ -806,41 +253,3 @@ enum TableType { Note, Warning, } - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct ContainerText { - #[serde(rename = "@type")] - text_type: TextType, - #[serde(rename = "@commandName")] - command_name: Option, - html: String, -} - -impl ContainerText { - fn decode(&self) -> Value { - html::Document::from_html(&self.html).into_value() - } -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -enum TextType { - Title, - Log, - Text, - #[serde(rename = "page-title")] - PageTitle, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct TableStructure { - /// The `.xml` member name, for legacy members only. - path: Option, - /// The `.bin` member name. - data_path: String, - /// Rarely used, not understood. - #[serde(rename = "csvPath")] - _csv_path: Option, -} diff --git a/rust/pspp/src/spv/read/structure.rs b/rust/pspp/src/spv/read/structure.rs new file mode 100644 index 0000000000..e4f5239590 --- /dev/null +++ b/rust/pspp/src/spv/read/structure.rs @@ -0,0 +1,788 @@ +use std::{ + cell::RefCell, + io::{BufRead, BufReader, Cursor, Read, Seek}, + mem::take, + rc::Rc, +}; + +use anyhow::Context; +use binrw::{BinRead, error::ContextExt}; +use cairo::ImageSurface; +use zip::ZipArchive; + +use crate::{ + output::{ + Details, Item, SpvInfo, SpvMembers, Text, + page::PageSetup, + pivot::{ + Length, + look::{HorzAlign, Look}, + }, + }, + spv::{ + Error, + legacy_bin::LegacyBin, + read::{ + TableType, Warning, WarningDetails, + legacy_xml::Visualization, + light::{LightTable, LightWarning}, + }, + }, +}; + +pub struct StructureMember { + pub page_setup: Option, + pub root: Heading, +} + +impl StructureMember { + pub fn read( + reader: R, + member_name: &str, + warn: &Rc>>, + ) -> Result + where + R: BufRead, + { + let mut heading: raw::Heading = + serde_path_to_error::deserialize(&mut quick_xml::de::Deserializer::from_reader(reader)) + .map_err(|error| Error::DeserializeError { + member_name: member_name.into(), + error, + })?; + Ok(Self { + page_setup: heading + .page_setup + .take() + .map(|ps| ps.decode(warn, member_name)), + root: heading.decode(member_name, warn), + }) + } +} + +pub struct Heading { + page_setup: Option, + expand: bool, + label: String, + children: Vec, + command_name: Option, +} + +impl Heading { + pub fn read_items( + self, + archive: &mut ZipArchive, + structure_member: &str, + warn: &Rc>>, + ) -> Vec + where + R: Read + Seek, + { + let mut items = Vec::new(); + for child in self.children { + let mut spv_info = SpvInfo::new(structure_member).with_members(child.members()); + let item = match child { + HeadingChild::Container(container) => { + if container.page_break_before { + items.push( + Details::PageBreak + .into_item() + .with_spv_info(SpvInfo::new(structure_member)), + ); + } + let result = match container.content { + Content::Table(table) => table.decode(archive, warn), + Content::Graph(_) => Err(Error::GraphTodo), + Content::Text(container_text) => Ok(container_text.into_item()), + Content::Image(image) => image.decode(archive), + Content::Model => Err(Error::ModelTodo), + Content::Tree => Err(Error::TreeTodo), + }; + spv_info.error = result.is_err(); + result + .unwrap_or_else(|error| { + Text::new_log(error.to_string()) + .into_item() + .with_label("Error") + }) + .with_show(container.show) + .with_command_name(Some(container.command_name)) + .with_label(container.label) + } + HeadingChild::Heading(mut heading) => { + let expand = heading.expand; + let label = take(&mut heading.label); + let command_name = take(&mut heading.command_name); + heading + .read_items(archive, structure_member, warn) + .into_iter() + .collect::() + .with_show(expand) + .with_label(label) + .with_command_name(command_name) + .with_spv_info(SpvInfo::new(structure_member)) + } + }; + items.push(item.with_spv_info(spv_info)); + } + items + } +} + +pub enum HeadingChild { + Heading(Heading), + Container(Container), +} + +impl HeadingChild { + fn members(&self) -> Option { + match self { + HeadingChild::Heading(_) => None, + HeadingChild::Container(container) => container.content.members(), + } + } +} + +pub struct Container { + show: bool, + page_break_before: bool, + text_align: Option, + command_name: String, + width: Option, + label: String, + content: Content, +} + +pub enum Content { + Text(Text), + Table(Table), + Image(Image), + Graph(Graph), + Tree, + Model, +} + +impl Content { + fn members(&self) -> Option { + match self { + Content::Text(_text) => None, + Content::Table(table) => Some(table.members()), + Content::Image(image) => Some(image.members()), + Content::Graph(graph) => Some(graph.members()), + Content::Tree => None, + Content::Model => None, + } + } +} + +pub struct Table { + subtype: String, + table_type: TableType, + look: Option>, + bin_member: String, + xml_member: Option, +} + +impl Table { + fn members(&self) -> SpvMembers { + if let Some(xml_member) = &self.xml_member { + SpvMembers::LegacyTable { + xml: xml_member.clone(), + binary: self.bin_member.clone(), + } + } else { + SpvMembers::LightTable(self.bin_member.clone()) + } + } + + fn decode( + self, + archive: &mut ZipArchive, + warn: &Rc>>, + ) -> Result + where + R: Read + Seek, + { + if let Some(xml_member_name) = &self.xml_member { + let bin_member_name = &self.bin_member; + let mut bin_member = archive.by_name(bin_member_name)?; + let mut bin_data = Vec::with_capacity(bin_member.size() as usize); + bin_member.read_to_end(&mut bin_data)?; + let mut cursor = Cursor::new(bin_data); + let legacy_bin = LegacyBin::read(&mut cursor).map_err(|e| { + e.with_message(format!( + "While parsing {bin_member_name:?} as legacy binary SPV member" + )) + })?; + let data = legacy_bin.decode(); + drop(bin_member); + + let member = BufReader::new(archive.by_name(&xml_member_name)?); + let visualization: Visualization = match serde_path_to_error::deserialize( + &mut quick_xml::de::Deserializer::from_reader(member), + ) + .with_context(|| format!("Failed to parse {xml_member_name}")) + { + Ok(result) => result, + Err(error) => panic!("{error:?}"), + }; + let pivot_table = visualization.decode(data, *self.look.unwrap_or_default())?; + + Ok(pivot_table.into_item()) + } else { + let member_name = self.bin_member.clone(); + let mut light = archive.by_name(&member_name)?; + let mut data = Vec::with_capacity(light.size() as usize); + light.read_to_end(&mut data)?; + let mut cursor = Cursor::new(data); + + let warn = warn.clone(); + let warning = Rc::new(RefCell::new(Box::new({ + let warn = warn.clone(); + let member_name = member_name.clone(); + move |w| { + (warn.borrow_mut())(Warning { + member: member_name.clone(), + details: WarningDetails::LightWarning(w), + }) + } + }) as Box)); + let table = LightTable::read_args(&mut cursor, (warning.clone(),)).map_err(|e| { + e.with_message(format!( + "While parsing {member_name:?} as light binary SPV member" + )) + })?; + let pivot_table = table.decode(&mut *warning.borrow_mut()); + Ok(pivot_table.into_item()) + } + } +} + +pub struct Image { + member: String, +} + +impl Image { + fn members(&self) -> SpvMembers { + SpvMembers::Image(self.member.clone()) + } + fn decode(self, archive: &mut ZipArchive) -> Result + where + R: Read + Seek, + { + let mut png = archive.by_name(&self.member)?; + let image = ImageSurface::create_from_png(&mut png)?; + Ok(Details::Image(image).into_item()) + } +} + +pub struct Graph { + xml_member: String, + data_member: Option, + csv_member: Option, +} + +impl Graph { + fn members(&self) -> SpvMembers { + SpvMembers::Graph { + data: self.data_member.clone(), + xml: self.xml_member.clone(), + csv: self.csv_member.clone(), + } + } +} + +mod raw { + use std::{cell::RefCell, rc::Rc}; + + use paper_sizes::PaperSize; + use serde::Deserialize; + + use crate::{ + output::{ + Text, + page::{self, Orientation}, + pivot::{ + Axis2, Length, TableProperties, + look::{HorzAlign, Look}, + value::Value, + }, + }, + spv::{ + html::{self, Document}, + read::{ + TableType, Warning, WarningDetails, + structure::{Content, HeadingChild}, + }, + }, + }; + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct Heading { + #[serde(rename = "@visibility")] + visibility: Option, + #[serde(rename = "@commandName")] + command_name: Option, + label: Label, + pub page_setup: Option, + + #[serde(rename = "$value")] + #[serde(default)] + children: Vec, + } + + impl Heading { + pub fn decode( + self, + structure_member: &str, + warn: &Rc>>, + ) -> super::Heading { + let mut children = Vec::new(); + for child in self.children { + match child { + HeadingContent::Container(container) => { + let (content, command_name) = match container.content { + ContainerContent::Table(table) => ( + Content::Table(super::Table { + subtype: table.subtype, + table_type: table.table_type, + look: table.table_properties.map(|table_properties| { + Box::new(Look::from(*table_properties)) + }), + bin_member: table.table_structure.bin_member, + xml_member: table.table_structure.xml_member, + }), + table.command_name, + ), + ContainerContent::Graph(graph) => ( + Content::Graph(super::Graph { + xml_member: graph.xml_member, + data_member: graph.data_member, + csv_member: graph.csv_member, + }), + graph.command_name, + ), + ContainerContent::Text(text) => ( + Content::Text(Text::new( + match text.text_type { + TextType::Title => crate::output::TextType::Title, + TextType::Log | TextType::Text => { + crate::output::TextType::Log + } + TextType::PageTitle => crate::output::TextType::PageTitle, + }, + text.decode(), + )), + text.command_name.unwrap_or_default(), + ), + ContainerContent::Image(image) => ( + Content::Image(super::Image { + member: image.data_path, + }), + image.command_name.unwrap_or_default(), + ), + ContainerContent::Object(object) => ( + Content::Image(super::Image { member: object.uri }), + object.command_name.unwrap_or_default(), + ), + ContainerContent::Model(model) => (Content::Model, model.command_name), + ContainerContent::Tree(tree) => (Content::Tree, tree.command_name), + }; + children.push(HeadingChild::Container(super::Container { + show: container.visibility != Visibility::Hidden, + page_break_before: container.page_break_before + == PageBreakBefore::Always, + text_align: container.text_align.map(HorzAlign::from), + width: container.width, + label: container.label.text, + command_name, + content, + })); + } + HeadingContent::Heading(heading) => { + children.push(HeadingChild::Heading( + heading.decode(structure_member, warn), + )); + } + } + } + super::Heading { + page_setup: self + .page_setup + .map(|page_setup| page_setup.decode(warn, structure_member)), + expand: !self.visibility.is_some(), + label: self.label.text, + command_name: self.command_name, + children, + } + } + } + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PageSetup { + #[serde(rename = "@initial-page-number")] + pub initial_page_number: Option, + #[serde(rename = "@chart-size")] + pub chart_size: Option, + #[serde(rename = "@margin-left")] + pub margin_left: Option, + #[serde(rename = "@margin-right")] + pub margin_right: Option, + #[serde(rename = "@margin-top")] + pub margin_top: Option, + #[serde(rename = "@margin-bottom")] + pub margin_bottom: Option, + #[serde(rename = "@paper-height")] + pub paper_height: Option, + #[serde(rename = "@paper-width")] + pub paper_width: Option, + #[serde(rename = "@reference-orientation")] + pub reference_orientation: Option, + #[serde(rename = "@space-after")] + pub space_after: Option, + pub page_header: PageHeader, + pub page_footer: PageFooter, + } + + impl PageSetup { + pub fn decode( + self, + warn: &Rc>>, + structure_member: &str, + ) -> page::PageSetup { + let mut setup = page::PageSetup::default(); + if let Some(initial_page_number) = self.initial_page_number { + setup.initial_page_number = initial_page_number; + } + if let Some(chart_size) = self.chart_size { + setup.chart_size = chart_size.into(); + } + if let Some(margin_left) = self.margin_left { + setup.margins.0[Axis2::X][0] = margin_left.into(); + } + if let Some(margin_right) = self.margin_right { + setup.margins.0[Axis2::X][1] = margin_right.into(); + } + if let Some(margin_top) = self.margin_top { + setup.margins.0[Axis2::Y][0] = margin_top.into(); + } + if let Some(margin_bottom) = self.margin_bottom { + setup.margins.0[Axis2::Y][1] = margin_bottom.into(); + } + match (self.paper_width, self.paper_height) { + (Some(width), Some(height)) => { + setup.paper = PaperSize::new(width.0, height.0, paper_sizes::Unit::Inch) + } + (Some(length), None) | (None, Some(length)) => { + setup.paper = PaperSize::new(length.0, length.0, paper_sizes::Unit::Inch) + } + (None, None) => (), + } + if let Some(reference_orientation) = &self.reference_orientation { + if reference_orientation.starts_with("0") { + setup.orientation = Orientation::Portrait; + } else if reference_orientation.starts_with("90") { + setup.orientation = Orientation::Landscape; + } else { + (warn.borrow_mut())(Warning { + member: structure_member.into(), + details: WarningDetails::UnknownOrientation(reference_orientation.clone()), + }); + } + } + if let Some(space_after) = self.space_after { + setup.object_spacing = space_after.into(); + } + if let Some(PageParagraph { text }) = &self.page_header.page_paragraph { + setup.header = text.decode(); + } + if let Some(PageParagraph { text }) = &self.page_footer.page_paragraph { + setup.footer = text.decode(); + } + setup + } + } + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PageHeader { + page_paragraph: Option, + } + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PageFooter { + page_paragraph: Option, + } + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + struct PageParagraph { + text: PageParagraphText, + } + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + struct PageParagraphText { + #[serde(default, rename = "$text")] + text: String, + } + + impl PageParagraphText { + fn decode(&self) -> Document { + Document::from_html(&self.text) + } + } + + #[derive(Copy, Clone, Debug, Default, Deserialize)] + #[serde(rename = "snake_case")] + enum ReferenceOrientation { + #[serde(alias = "0")] + #[serde(alias = "0deg")] + #[serde(alias = "inherit")] + #[default] + Portrait, + + #[serde(alias = "90")] + #[serde(alias = "90deg")] + #[serde(alias = "-270")] + #[serde(alias = "-270deg")] + Landscape, + + #[serde(alias = "180")] + #[serde(alias = "180deg")] + #[serde(alias = "-1280")] + #[serde(alias = "-180deg")] + ReversePortrait, + + #[serde(alias = "270")] + #[serde(alias = "270deg")] + #[serde(alias = "-90")] + #[serde(alias = "-90deg")] + Seascape, + } + + impl From for page::Orientation { + fn from(value: ReferenceOrientation) -> Self { + match value { + ReferenceOrientation::Portrait | ReferenceOrientation::ReversePortrait => { + page::Orientation::Portrait + } + ReferenceOrientation::Landscape | ReferenceOrientation::Seascape => { + page::Orientation::Landscape + } + } + } + } + + /// Chart size. + #[derive(Copy, Clone, Debug, Default, Deserialize)] + #[serde(rename_all = "kebab-case")] + pub enum ChartSize { + FullHeight, + HalfHeight, + QuarterHeight, + #[default] + #[serde(other)] + AsIs, + } + + impl From for page::ChartSize { + fn from(value: ChartSize) -> Self { + match value { + ChartSize::AsIs => page::ChartSize::AsIs, + ChartSize::FullHeight => page::ChartSize::FullHeight, + ChartSize::HalfHeight => page::ChartSize::HalfHeight, + ChartSize::QuarterHeight => page::ChartSize::QuarterHeight, + } + } + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + enum HeadingContent { + Container(Container), + Heading(Box), + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct Label { + #[serde(default, rename = "$text")] + text: String, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct Container { + #[serde(default, rename = "@visibility")] + visibility: Visibility, + #[serde(rename = "@page-break-before")] + #[serde(default)] + page_break_before: PageBreakBefore, + #[serde(rename = "@text-align")] + text_align: Option, + #[serde(rename = "@width")] + width: Option, + label: Label, + + #[serde(rename = "$value")] + content: ContainerContent, + } + + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Deserialize)] + #[serde(rename_all = "camelCase")] + enum PageBreakBefore { + #[default] + Auto, + Always, + Avoid, + Left, + Right, + Inherit, + } + + #[derive(Deserialize, Debug, Default, PartialEq, Eq)] + #[serde(rename_all = "camelCase")] + enum Visibility { + #[default] + Visible, + Hidden, + } + + #[derive(Copy, Clone, Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + enum TextAlign { + Left, + Center, + Right, + } + + impl From for HorzAlign { + fn from(value: TextAlign) -> Self { + match value { + TextAlign::Left => HorzAlign::Left, + TextAlign::Center => HorzAlign::Center, + TextAlign::Right => HorzAlign::Right, + } + } + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + enum ContainerContent { + Table(Table), + Text(ContainerText), + Graph(Graph), + Model(Model), + Object(Object), + Image(Image), + Tree(Tree), + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct Graph { + #[serde(rename = "@commandName")] + command_name: String, + + /// Data. + #[serde(rename = "dataPath")] + data_member: Option, + + /// XML in VizML format. + #[serde(rename = "path")] + xml_member: String, + + /// Rarely used, not understood. + #[serde(rename = "csvPath")] + csv_member: Option, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct Model { + #[serde(rename = "@commandName")] + command_name: String, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct Tree { + #[serde(rename = "@commandName")] + command_name: String, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct Image { + #[serde(rename = "@commandName")] + command_name: Option, + data_path: String, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct Object { + #[serde(rename = "@commandName")] + command_name: Option, + #[serde(rename = "@uri")] + uri: String, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct Table { + #[serde(rename = "@commandName")] + command_name: String, + #[serde(rename = "@subType")] + subtype: String, + #[serde(rename = "@tableId")] + table_id: Option, + #[serde(rename = "@type")] + table_type: TableType, + table_properties: Option>, + table_structure: TableStructure, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct ContainerText { + #[serde(rename = "@type")] + text_type: TextType, + #[serde(rename = "@commandName")] + command_name: Option, + html: String, + } + + impl ContainerText { + fn decode(&self) -> Value { + html::Document::from_html(&self.html).into_value() + } + } + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + enum TextType { + Title, + Log, + Text, + #[serde(rename = "page-title")] + PageTitle, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + struct TableStructure { + /// The `.xml` member name, for legacy members only. + #[serde(rename = "path")] + xml_member: Option, + /// The `.bin` member name. + #[serde(rename = "dataPath")] + bin_member: String, + /// Rarely used, not understood. + #[serde(rename = "csvPath")] + _csv_member: Option, + } +} diff --git a/rust/pspp/src/spv/write.rs b/rust/pspp/src/spv/write.rs index e6f91e5422..c0cd91050f 100644 --- a/rust/pspp/src/spv/write.rs +++ b/rust/pspp/src/spv/write.rs @@ -196,7 +196,7 @@ where X: Write, { match &item.details { - Details::Chart | Details::Image(_) => todo!(), + Details::Graph | Details::Image(_) => todo!(), Details::Heading(children) => { let mut attributes = Vec::::new(); if let Some(command_name) = &item.command_name {