From c1300cbcd1fca10414d797b87cf469ee8c2d4642 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Wed, 10 Dec 2025 08:44:24 -0800 Subject: [PATCH] Move spv writer to its own module. --- rust/Cargo.lock | 7 - rust/pspp/Cargo.toml | 1 - rust/pspp/src/lib.rs | 1 + rust/pspp/src/output.rs | 4 +- rust/pspp/src/output/drivers/spv.rs | 1360 +-------------------------- rust/pspp/src/spv.rs | 30 + rust/pspp/src/spv/write.rs | 1354 ++++++++++++++++++++++++++ rust/pspp/src/sys/write.rs | 19 +- 8 files changed, 1425 insertions(+), 1351 deletions(-) create mode 100644 rust/pspp/src/spv.rs create mode 100644 rust/pspp/src/spv/write.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 2a7afa6850..48a5a75043 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1922,7 +1922,6 @@ dependencies = [ "unicode-segmentation", "unicode-width", "windows-sys 0.48.0", - "xmlwriter", "zeroize", "zip", ] @@ -3119,12 +3118,6 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - [[package]] name = "yoke" version = "0.8.0" diff --git a/rust/pspp/Cargo.toml b/rust/pspp/Cargo.toml index ee99cfce76..d8fa7798a5 100644 --- a/rust/pspp/Cargo.toml +++ b/rust/pspp/Cargo.toml @@ -40,7 +40,6 @@ cairo-rs = { version = "0.20.7", features = ["ps", "png", "pdf", "svg"] } pango = "0.20.9" pangocairo = "0.20.7" zip = "4.0.0" -xmlwriter = "0.1.0" csv = "1.3.1" cmac = "0.7.2" aes = "0.8.4" diff --git a/rust/pspp/src/lib.rs b/rust/pspp/src/lib.rs index baff025160..ffb4044774 100644 --- a/rust/pspp/src/lib.rs +++ b/rust/pspp/src/lib.rs @@ -121,6 +121,7 @@ pub mod pc; pub mod por; pub mod prompt; pub mod settings; +pub mod spv; pub mod sys; pub mod util; pub mod variable; diff --git a/rust/pspp/src/output.rs b/rust/pspp/src/output.rs index a90ef6e198..1253116210 100644 --- a/rust/pspp/src/output.rs +++ b/rust/pspp/src/output.rs @@ -400,9 +400,9 @@ impl Chart { #[derive(Clone, Debug, Serialize)] pub struct Text { - type_: TextType, + pub type_: TextType, - content: Value, + pub content: Value, } impl Text { diff --git a/rust/pspp/src/output/drivers/spv.rs b/rust/pspp/src/output/drivers/spv.rs index b2238ba542..d4be110591 100644 --- a/rust/pspp/src/output/drivers/spv.rs +++ b/rust/pspp/src/output/drivers/spv.rs @@ -14,57 +14,21 @@ // You should have received a copy of the GNU General Public License along with // this program. If not, see . -use core::f64; use std::{ borrow::Cow, fs::File, - io::{Cursor, Seek, Write}, - iter::{repeat, repeat_n}, + io::{Seek, Write}, path::PathBuf, sync::Arc, }; -use binrw::{BinWrite, Endian}; -use chrono::Utc; -use enum_map::EnumMap; -use paper_sizes::Length; -use quick_xml::{ - ElementWriter, - events::{BytesText, attributes::Attribute}, - writer::Writer as XmlWriter, -}; use serde::{Deserialize, Serialize}; -use zip::{ZipWriter, result::ZipResult, write::SimpleFileOptions}; use crate::{ - format::{Format, Type}, - output::{ - Details, Item, Text, - drivers::Driver, - page::{ChartSize, PageSetup}, - pivot::{ - Area, AreaStyle, Axis2, Axis3, Border, BorderStyle, BoxBorder, Category, CellStyle, - Color, Dimension, FontStyle, Footnote, FootnoteMarkerPosition, FootnoteMarkerType, - Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Leaf, PivotTable, - RowColBorder, RowParity, Stroke, Value, ValueInner, ValueStyle, VertAlign, - }, - spv::html::Document, - }, - settings::Show, - util::ToSmallString, + output::{Item, drivers::Driver, page::PageSetup}, + spv::Writer, }; -fn light_table_name(table_id: u64) -> String { - format!("{table_id:011}_lightTableData.bin") -} - -fn output_viewer_name(heading_id: u64, is_heading: bool) -> String { - format!( - "outputViewer{heading_id:010}{}.xml", - if is_heading { "_heading" } else { "" } - ) -} - #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SpvConfig { /// Output file name. @@ -78,483 +42,7 @@ pub struct SpvDriver where W: Write + Seek, { - writer: ZipWriter, - needs_page_break: bool, - next_table_id: u64, - next_heading_id: u64, - page_setup: Option, -} - -impl SpvDriver { - pub fn new(config: &SpvConfig) -> std::io::Result { - let mut driver = Self::for_writer(File::create(&config.file)?); - if let Some(page_setup) = &config.page_setup { - driver = driver.with_page_setup(page_setup.clone()); - } - Ok(driver) - } -} - -impl SpvDriver -where - W: Write + Seek, -{ - pub fn for_writer(writer: W) -> Self { - let mut writer = ZipWriter::new(writer); - writer - .start_file("META-INF/MANIFEST.MF", SimpleFileOptions::default()) - .unwrap(); - writer.write_all("allowPivoting=true".as_bytes()).unwrap(); - Self { - writer, - needs_page_break: false, - next_table_id: 1, - next_heading_id: 1, - page_setup: None, - } - } - - pub fn with_page_setup(self, page_setup: PageSetup) -> Self { - Self { - page_setup: Some(page_setup), - ..self - } - } - - pub fn close(mut self) -> ZipResult { - self.writer - .start_file("META-INF/MANIFEST.MF", SimpleFileOptions::default())?; - write!(&mut self.writer, "allowPivoting=true")?; - self.writer.finish() - } - - fn page_break_before(&mut self) -> bool { - let page_break_before = self.needs_page_break; - self.needs_page_break = false; - page_break_before - } - - fn write_table( - &mut self, - item: &Item, - pivot_table: &PivotTable, - structure: &mut XmlWriter, - ) where - X: Write, - { - let table_id = self.next_table_id; - self.next_table_id += 1; - - let mut content = Vec::new(); - let mut cursor = Cursor::new(&mut content); - pivot_table.write_le(&mut cursor).unwrap(); - - let table_name = light_table_name(table_id); - self.writer - .start_file(&table_name, SimpleFileOptions::default()) - .unwrap(); // XXX - self.writer.write_all(&content).unwrap(); // XXX - - self.container(structure, item, "vtb:table", |element| { - element - .with_attribute(("tableId", Cow::from(table_id.to_string()))) - .with_attribute(( - "commandName", - pivot_table - .metadata - .command_local - .as_ref() - .map_or("", |s| s.as_str()), - )) - .with_attribute(("type", "table" /*XXX*/)) - .with_attribute(( - "subType", - Cow::from(pivot_table.subtype().display(pivot_table).to_string()), - )) - .write_inner_content(|w| { - w.create_element("vtb:tableStructure") - .write_inner_content(|w| { - w.create_element("vtb:dataPath") - .write_text_content(BytesText::new(&table_name))?; - Ok(()) - })?; - Ok(()) - }) - .unwrap(); - }); - } - - fn write_text(&mut self, item: &Item, text: &Text, structure: &mut XmlWriter) - where - X: Write, - { - self.container(structure, item, "vtx:text", |w| { - w.with_attribute(("type", text.type_.as_xml_str())) - .write_text_content(BytesText::new(&text.content.display(()).to_string())) - .unwrap(); - }); - } - - fn write_item(&mut self, item: &Item, structure: &mut XmlWriter) - where - X: Write, - { - match &item.details { - Details::Chart | Details::Image(_) => todo!(), - Details::Heading(children) => { - let mut attributes = Vec::::new(); - if let Some(command_name) = &item.command_name { - attributes.push(("commandName", command_name.as_str()).into()); - } - if !item.show { - attributes.push(("visibility", "collapsed").into()); - } - structure - .create_element("heading") - .with_attributes(attributes) - .write_inner_content(|w| { - w.create_element("label") - .write_text_content(BytesText::new(&item.label()))?; - for child in &children.0 { - self.write_item(&child, w); - } - Ok(()) - }) - .unwrap(); - } - Details::Message(diagnostic) => { - self.write_text(item, &Text::from(diagnostic.as_ref()), structure) - } - Details::PageBreak => { - self.needs_page_break = true; - } - Details::Table(pivot_table) => self.write_table(item, pivot_table, structure), - Details::Text(text) => self.write_text(item, text, structure), - } - } - - fn container( - &mut self, - writer: &mut XmlWriter, - item: &Item, - inner_elem: &str, - closure: F, - ) where - X: Write, - F: FnOnce(ElementWriter), - { - writer - .create_element("container") - .with_attributes( - self.page_break_before() - .then_some(("page-break-before", "always")), - ) - .with_attribute(("visibility", if item.show { "visible" } else { "hidden" })) - .write_inner_content(|w| { - let mut element = w - .create_element("label") - .write_text_content(BytesText::new(&item.label())) - .unwrap() - .create_element(inner_elem); - if let Some(command_name) = &item.command_name { - element = element.with_attribute(("commandName", command_name.as_str())); - }; - closure(element); - Ok(()) - }) - .unwrap(); - } -} - -impl BinWrite for PivotTable { - type Args<'a> = (); - - fn write_options( - &self, - writer: &mut W, - endian: Endian, - _args: (), - ) -> binrw::BinResult<()> { - // Header. - ( - 1u8, - 0u8, - 3u32, // version - SpvBool(true), // x0 - SpvBool(false), // x1 - SpvBool(self.style.rotate_inner_column_labels), - SpvBool(self.style.rotate_outer_row_labels), - SpvBool(true), // x2 - 0x15u32, // x3 - *self.style.look.heading_widths[HeadingRegion::Columns].start() as i32, - *self.style.look.heading_widths[HeadingRegion::Columns].end() as i32, - *self.style.look.heading_widths[HeadingRegion::Rows].start() as i32, - *self.style.look.heading_widths[HeadingRegion::Rows].end() as i32, - 0u64, - ) - .write_le(writer)?; - - // Titles. - ( - self.title(), - self.subtype(), - Optional(Some(self.title())), - Optional(self.metadata.corner_text.as_ref()), - Optional(self.metadata.caption.as_ref()), - ) - .write_le(writer)?; - - // Footnotes. - self.footnotes.write_le(writer)?; - - // Areas. - static SPV_AREAS: [Area; 8] = [ - Area::Title, - Area::Caption, - Area::Footer, - Area::Corner, - Area::Labels(Axis2::X), - Area::Labels(Axis2::Y), - Area::Data(RowParity::Even), - Area::Layers, - ]; - for (index, area) in SPV_AREAS.into_iter().enumerate() { - let odd_data_style = if let Area::Data(_) = area { - Some(&self.style.look.areas[Area::Data(RowParity::Odd)]) - } else { - None - }; - self.style.look.areas[area].write_le_args(writer, (index, odd_data_style))?; - } - - // Borders. - static SPV_BORDERS: [Border; 19] = [ - Border::Title, - Border::OuterFrame(BoxBorder::Left), - Border::OuterFrame(BoxBorder::Top), - Border::OuterFrame(BoxBorder::Right), - Border::OuterFrame(BoxBorder::Bottom), - Border::InnerFrame(BoxBorder::Left), - Border::InnerFrame(BoxBorder::Top), - Border::InnerFrame(BoxBorder::Right), - Border::InnerFrame(BoxBorder::Bottom), - Border::DataLeft, - Border::DataTop, - Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X)), - Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::Y)), - Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)), - Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::Y)), - Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X)), - Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::Y)), - Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)), - Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::Y)), - ]; - let borders_start = Count::new(writer)?; - (1, SPV_BORDERS.len() as u32).write_be(writer)?; - for (index, border) in SPV_BORDERS.into_iter().enumerate() { - self.style.look.borders[border].write_be_args(writer, index)?; - } - (SpvBool(self.style.show_grid_lines), 0u8, 0u16).write_le(writer)?; - borders_start.finish_le32(writer)?; - - // Print Settings. - Counted::new(( - 1u32, - SpvBool(self.style.look.print_all_layers), - SpvBool(self.style.look.paginate_layers), - SpvBool(self.style.look.shrink_to_fit[Axis2::X]), - SpvBool(self.style.look.shrink_to_fit[Axis2::Y]), - SpvBool(self.style.look.top_continuation), - SpvBool(self.style.look.bottom_continuation), - self.style.look.n_orphan_lines as u32, - SpvString( - self.style - .look - .continuation - .as_ref() - .map_or("", |s| s.as_str()), - ), - )) - .with_endian(Endian::Little) - .write_be(writer)?; - - // Table Settings. - Counted::new(( - 1u32, - 4u32, - self.spv_layer() as u32, - SpvBool(self.style.look.hide_empty), - SpvBool(self.style.look.row_label_position == LabelPosition::Corner), - SpvBool(self.style.look.footnote_marker_type == FootnoteMarkerType::Alphabetic), - SpvBool( - self.style.look.footnote_marker_position == FootnoteMarkerPosition::Superscript, - ), - 0u8, - Counted::new(( - 0u32, // n-row-breaks - 0u32, // n-column-breaks - 0u32, // n-row-keeps - 0u32, // n-column-keeps - 0u32, // n-row-point-keeps - 0u32, // n-column-point-keeps - )), - SpvString::optional(&self.metadata.notes), - SpvString::optional(&self.style.look.name), - Zeros(82), - )) - .with_endian(Endian::Little) - .write_be(writer)?; - - fn y0(pivot_table: &PivotTable) -> impl for<'a> BinWrite = ()> { - ( - pivot_table.style.settings.epoch.0 as u32, - u8::from(pivot_table.style.settings.decimal), - b',', - ) - } - - fn custom_currency(pivot_table: &PivotTable) -> impl for<'a> BinWrite = ()> { - ( - 5, - EnumMap::from_fn(|cc| { - SpvString( - pivot_table - .style - .settings - .number_style(Type::CC(cc)) - .to_string(), - ) - }) - .into_array(), - ) - } - - fn x1(pivot_table: &PivotTable) -> impl for<'a> BinWrite = ()> { - ( - 0u8, // x14 - if pivot_table.style.show_title { - 1u8 - } else { - 10u8 - }, - 0u8, // x16 - 0u8, // lang - Show::as_spv(&pivot_table.style.show_variables), - Show::as_spv(&pivot_table.style.show_values), - -1i32, // x18 - -1i32, // x19 - Zeros(17), - SpvBool(false), // x20 - SpvBool(pivot_table.style.show_caption), - ) - } - - fn x2() -> impl for<'a> BinWrite = ()> { - Counted::new(( - 0u32, // n-row-heights - 0u32, // n-style-maps - 0u32, // n-styles, - 0u32, - )) - } - - fn y1(pivot_table: &PivotTable) -> impl for<'a> BinWrite = ()> + use<'_> { - ( - SpvString::optional(&pivot_table.metadata.command_c), - SpvString::optional(&pivot_table.metadata.command_local), - SpvString::optional(&pivot_table.metadata.language), - SpvString("UTF-8"), - SpvString::optional(&pivot_table.metadata.locale), - SpvBool(false), // x10 - SpvBool(pivot_table.style.settings.leading_zero), - SpvBool(true), // x12 - SpvBool(true), // x13 - y0(pivot_table), - ) - } - - fn y2(pivot_table: &PivotTable) -> impl for<'a> BinWrite = ()> { - (custom_currency(pivot_table), b'.', SpvBool(false)) - } - - fn x3(pivot_table: &PivotTable) -> impl for<'a> BinWrite = ()> + use<'_> { - Counted::new(( - 1u8, - 0u8, - 4u8, // x21 - 0u8, - 0u8, - 0u8, - y1(pivot_table), - pivot_table.style.small, - 1u8, - SpvString::optional(&pivot_table.metadata.dataset), - SpvString::optional(&pivot_table.metadata.datafile), - 0u32, - pivot_table - .metadata - .date - .map_or(0i64, |date| date.and_utc().timestamp()), - y2(pivot_table), - )) - } - - // Formats. - ( - 0u32, - SpvString("en_US.ISO_8859-1:1987"), - 0u32, // XXX current_layer - SpvBool(false), // x7 - SpvBool(false), // x8 - SpvBool(false), // x9 - y0(self), - custom_currency(self), - Counted::new((Counted::new((x1(self), x2())), x3(self))), - ) - .write_le(writer)?; - - // Dimensions. - (self.dimensions().len() as u32).write_le(writer)?; - - let x2 = repeat_n(2, self.axes()[Axis3::Z].dimensions.len()) - .chain(repeat_n(0, self.axes()[Axis3::Y].dimensions.len())) - .chain(repeat(1)); - for ((index, dimension), x2) in self.dimensions().iter().enumerate().zip(x2) { - dimension.write_options(writer, endian, (index, x2))?; - } - - // Axes. - for axis in [Axis3::Z, Axis3::Y, Axis3::X] { - (self.axes()[axis].dimensions.len() as u32).write_le(writer)?; - } - for axis in [Axis3::Z, Axis3::Y, Axis3::X] { - for index in self.axes()[axis].dimensions.iter().copied() { - (index as u32).write_le(writer)?; - } - } - - // Cells. - (self.cells().len() as u32).write_le(writer)?; - for (index, value) in self.cells() { - (*index as u64, value).write_le(writer)?; - } - - Ok(()) - } -} - -impl PivotTable { - fn spv_layer(&self) -> usize { - let mut layer = 0; - for (dimension, layer_value) in self - .axis_dimensions(Axis3::Z) - .zip(self.current_layer.iter().copied()) - .rev() - { - layer = layer * dimension.len() + layer_value; - } - layer - } + writer: Writer, } impl Driver for SpvDriver @@ -566,848 +54,40 @@ where } fn write(&mut self, item: &Arc) { - if item.details.is_page_break() { - self.needs_page_break = true; - return; - } - - let mut headings = XmlWriter::new(Cursor::new(Vec::new())); - let element = headings - .create_element("heading") - .with_attribute(( - "creation-date-time", - Cow::from(Utc::now().format("%x %x").to_string()), - )) - .with_attribute(( - "creator", - Cow::from(format!( - "{} {}", - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION") - )), - )) - .with_attribute(("creator-version", "21")) - .with_attribute(("xmlns", "http://xml.spss.com/spss/viewer/viewer-tree")) - .with_attribute(( - "xmlns:vps", - "http://xml.spss.com/spss/viewer/viewer-pagesetup", - )) - .with_attribute(("xmlns:vtx", "http://xml.spss.com/spss/viewer/viewer-text")) - .with_attribute(("xmlns:vtb", "http://xml.spss.com/spss/viewer/viewer-table")); - element - .write_inner_content(|w| { - w.create_element("label") - .write_text_content(BytesText::new("Output"))?; - if let Some(page_setup) = self.page_setup.take() { - write_page_setup(&page_setup, w)?; - } - self.write_item(item, w); - Ok(()) - }) - .unwrap(); - - let headings = headings.into_inner().into_inner(); - let heading_id = self.next_heading_id; - self.next_heading_id += 1; - self.writer - .start_file( - output_viewer_name(heading_id, item.details.is_heading()), - SimpleFileOptions::default(), - ) - .unwrap(); // XXX - self.writer.write_all(&headings).unwrap(); // XXX + self.writer.write(item); } fn setup(&mut self, page_setup: &PageSetup) -> bool { - self.page_setup = Some(page_setup.clone()); + self.writer.set_page_setup(page_setup.clone()); true } -} - -fn write_page_setup(page_setup: &PageSetup, writer: &mut XmlWriter) -> std::io::Result<()> -where - X: Write, -{ - fn length(length: Length) -> Cow<'static, str> { - Cow::from(length.to_string()) - } - - writer - .create_element("vps:pageSetup") - .with_attribute(( - "initial-page-number", - Cow::from(format!("{}", page_setup.initial_page_number)), - )) - .with_attribute(( - "chart-size", - match page_setup.chart_size { - ChartSize::AsIs => "as-is", - ChartSize::FullHeight => "full-height", - ChartSize::HalfHeight => "half-height", - ChartSize::QuarterHeight => "quarter-height", - }, - )) - .with_attribute(("margin-left", length(page_setup.margins.0[Axis2::X][0]))) - .with_attribute(("margin-right", length(page_setup.margins.0[Axis2::X][1]))) - .with_attribute(("margin-top", length(page_setup.margins.0[Axis2::Y][0]))) - .with_attribute(("margin-bottom", length(page_setup.margins.0[Axis2::Y][1]))) - .with_attribute(("paper-height", length(page_setup.paper.height()))) - .with_attribute(("paper-width", length(page_setup.paper.width()))) - .with_attribute(( - "reference-orientation", - match page_setup.orientation { - crate::output::page::Orientation::Portrait => "portrait", - crate::output::page::Orientation::Landscape => "landscape", - }, - )) - .with_attribute(("space-after", length(page_setup.object_spacing))) - .write_inner_content(|w| { - write_page_heading(&page_setup.header, "vps:pageHeader", w)?; - write_page_heading(&page_setup.footer, "vps:pageFooter", w)?; - Ok(()) - })?; - Ok(()) -} - -fn write_page_heading( - heading: &Document, - name: &str, - writer: &mut XmlWriter, -) -> std::io::Result<()> -where - X: Write, -{ - let element = writer.create_element(name); - if !heading.is_empty() { - element.write_inner_content(|w| { - w.create_element("vps:pageParagraph") - .write_inner_content(|w| { - w.create_element("vtx:text") - .with_attribute(("text", "title")) - .write_text_content(BytesText::new(&heading.to_html()))?; - Ok(()) - })?; - Ok(()) - })?; - } - Ok(()) -} - -fn maybe_with_attribute<'a, 'b, W, I>( - element: ElementWriter<'a, W>, - attr: Option, -) -> ElementWriter<'a, W> -where - I: Into>, -{ - if let Some(attr) = attr { - element.with_attribute(attr) - } else { - element - } -} - -impl BinWrite for Dimension { - type Args<'a> = (usize, u8); - - fn write_options( - &self, - writer: &mut W, - endian: Endian, - (index, x2): (usize, u8), - ) -> binrw::BinResult<()> { - ( - &self.root.name, - 0u8, // x1 - x2, - 2u32, // x3 - SpvBool(!self.root.show_label), - SpvBool(self.hide_all_labels), - SpvBool(true), - index as u32, - self.root.children.len() as u32, - ) - .write_options(writer, endian, ())?; - - let mut data_indexes = self.presentation_order.iter().copied(); - for child in &self.root.children { - child.write_le(writer, &mut data_indexes)?; - } - Ok(()) - } -} - -impl Category { - fn write_le(&self, writer: &mut W, data_indexes: &mut D) -> binrw::BinResult<()> - where - W: Write + Seek, - D: Iterator, - { - match self { - Category::Group(group) => group.write_le(writer, data_indexes), - Category::Leaf(leaf) => leaf.write_le(writer, data_indexes), - } - } -} - -impl Leaf { - fn write_le(&self, writer: &mut W, data_indexes: &mut D) -> binrw::BinResult<()> - where - W: Write + Seek, - D: Iterator, - { - ( - self.name(), - 0u8, - 0u8, - 0u8, - 2u32, - data_indexes.next().unwrap() as u32, - 0u32, - ) - .write_le(writer) - } -} - -impl Group { - fn write_le(&self, writer: &mut W, data_indexes: &mut D) -> binrw::BinResult<()> - where - W: Write + Seek, - D: Iterator, - { - ( - self.name(), - 0u8, // merge - 0u8, - 1u8, - 0u32, // x23 - -1i32, - self.children.len() as u32, - ) - .write_le(writer)?; - - for child in &self.children { - child.write_le(writer, data_indexes)?; - } - Ok(()) - } -} - -impl BinWrite for Footnote { - type Args<'a> = (); - - fn write_options( - &self, - writer: &mut W, - endian: Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - ( - &self.content, - Optional(self.marker.as_ref()), - if self.show { 1i32 } else { -1 }, - ) - .write_options(writer, endian, args) - } -} - -impl BinWrite for Footnotes { - type Args<'a> = (); - - fn write_options( - &self, - writer: &mut W, - endian: Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - (self.len() as u32).write_options(writer, endian, args)?; - for footnote in self { - footnote.write_options(writer, endian, args)?; - } - Ok(()) - } -} - -impl BinWrite for AreaStyle { - type Args<'a> = (usize, Option<&'a AreaStyle>); - - fn write_options( - &self, - writer: &mut W, - endian: Endian, - (index, odd_data_style): (usize, Option<&AreaStyle>), - ) -> binrw::BinResult<()> { - let typeface = if self.font_style.font.is_empty() { - "SansSerif" - } else { - self.font_style.font.as_str() - }; - ( - (index + 1) as u8, - 0x31u8, - SpvString(typeface), - self.font_style.size as f32 * 1.33, - self.font_style.bold as u32 + 2 * self.font_style.italic as u32, - SpvBool(self.font_style.underline), - self.cell_style - .horz_align - .map_or(64173, |horz_align| horz_align.as_spv(61453)), - self.cell_style.vert_align.as_spv(), - self.font_style.fg, - self.font_style.bg, - ) - .write_options(writer, endian, ())?; - - let alt_fg = odd_data_style.map_or(self.font_style.fg, |style| style.font_style.fg); - let alt_bg = odd_data_style.map_or(self.font_style.bg, |style| style.font_style.bg); - if self.font_style.fg != alt_fg || self.font_style.bg != alt_bg { - (SpvBool(true), alt_fg, alt_bg).write_options(writer, endian, ())?; - } else { - (SpvBool(false), SpvString(""), SpvString("")).write_options(writer, endian, ())?; - } - - ( - self.cell_style.margins[Axis2::X][0], - self.cell_style.margins[Axis2::X][1], - self.cell_style.margins[Axis2::Y][0], - self.cell_style.margins[Axis2::Y][1], - ) - .write_options(writer, endian, ()) - } -} - -impl Stroke { - fn as_spv(&self) -> u32 { - match self { - Stroke::None => 0, - Stroke::Solid => 1, - Stroke::Dashed => 2, - Stroke::Thick => 3, - Stroke::Thin => 4, - Stroke::Double => 5, - } - } -} - -impl Color { - fn as_spv(&self) -> u32 { - ((self.alpha as u32) << 24) - | ((self.r as u32) << 16) - | ((self.g as u32) << 8) - | (self.b as u32) - } -} - -impl BinWrite for BorderStyle { - type Args<'a> = usize; - - fn write_options( - &self, - writer: &mut W, - _endian: Endian, - index: usize, - ) -> binrw::BinResult<()> { - (index as u32, self.stroke.as_spv(), self.color.as_spv()).write_be(writer) - } -} - -struct SpvBool(bool); -impl BinWrite for SpvBool { - type Args<'a> = (); - - fn write_options( - &self, - writer: &mut W, - endian: binrw::Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - (self.0 as u8).write_options(writer, endian, args) - } -} - -struct SpvString(T); -impl<'a> SpvString<&'a str> { - fn optional(s: &'a Option) -> Self { - Self(s.as_ref().map_or("", |s| s.as_str())) - } -} -impl BinWrite for SpvString -where - T: AsRef, -{ - type Args<'a> = (); - - fn write_options( - &self, - writer: &mut W, - endian: binrw::Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - let s = self.0.as_ref(); - let length = s.len() as u32; - (length, s.as_bytes()).write_options(writer, endian, args) - } -} - -impl Show { - fn as_spv(this: &Option) -> u8 { - match this { - None => 0, - Some(Show::Value) => 1, - Some(Show::Label) => 2, - Some(Show::Both) => 3, - } - } -} - -struct Count(u64); - -impl Count { - fn new(writer: &mut W) -> binrw::BinResult - where - W: Write + Seek, - { - 0u32.write_le(writer)?; - Ok(Self(writer.stream_position()?)) - } - - fn finish(self, writer: &mut W, endian: Endian) -> binrw::BinResult<()> - where - W: Write + Seek, - { - let saved_position = writer.stream_position()?; - let n_bytes = saved_position - self.0; - writer.seek(std::io::SeekFrom::Start(self.0 - 4))?; - (n_bytes as u32).write_options(writer, endian, ())?; - writer.seek(std::io::SeekFrom::Start(saved_position))?; - Ok(()) - } - - fn finish_le32(self, writer: &mut W) -> binrw::BinResult<()> - where - W: Write + Seek, - { - self.finish(writer, Endian::Little) - } - - fn finish_be32(self, writer: &mut W) -> binrw::BinResult<()> - where - W: Write + Seek, - { - self.finish(writer, Endian::Big) - } -} - -struct Counted { - inner: T, - endian: Option, -} - -impl Counted { - fn new(inner: T) -> Self { - Self { - inner, - endian: None, - } - } - fn with_endian(self, endian: Endian) -> Self { - Self { - inner: self.inner, - endian: Some(endian), - } - } -} - -impl BinWrite for Counted -where - T: BinWrite, - for<'a> T: BinWrite = ()>, -{ - type Args<'a> = T::Args<'a>; - - fn write_options( - &self, - writer: &mut W, - endian: Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - let start = Count::new(writer)?; - self.inner.write_options(writer, endian, args)?; - start.finish(writer, self.endian.unwrap_or(endian)) - } -} - -pub struct Zeros(pub usize); - -impl BinWrite for Zeros { - type Args<'a> = (); - fn write_options( - &self, - writer: &mut W, - _endian: Endian, - _args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - for _ in 0..self.0 { - writer.write_all(&[0u8])?; - } - Ok(()) - } -} - -#[derive(Default)] -struct StylePair<'a> { - font_style: Option<&'a FontStyle>, - cell_style: Option<&'a CellStyle>, -} - -impl BinWrite for Color { - type Args<'a> = (); - - fn write_options( - &self, - writer: &mut W, - endian: Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - SpvString(&self.without_alpha().display_css().to_small_string::<16>()) - .write_options(writer, endian, args) - } -} - -impl BinWrite for FontStyle { - type Args<'a> = (); - - fn write_options( - &self, - writer: &mut W, - endian: Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - let typeface = if self.font.is_empty() { - "SansSerif" - } else { - self.font.as_str() - }; - ( - SpvBool(self.bold), - SpvBool(self.italic), - SpvBool(self.underline), - SpvBool(true), - self.fg, - self.bg, - SpvString(typeface), - (self.size as f64 * 1.33).ceil() as u8, - ) - .write_options(writer, endian, args) - } -} - -impl HorzAlign { - fn as_spv(&self, decimal: u32) -> u32 { - match self { - HorzAlign::Right => 4, - HorzAlign::Left => 2, - HorzAlign::Center => 0, - HorzAlign::Decimal { .. } => decimal, - } + fn handles_show(&self) -> bool { + true } - fn decimal_offset(&self) -> Option { - match *self { - HorzAlign::Decimal { offset, .. } => Some(offset), - _ => None, - } + fn handles_groups(&self) -> bool { + true } } -impl VertAlign { - fn as_spv(&self) -> u32 { - match self { - VertAlign::Top => 1, - VertAlign::Middle => 0, - VertAlign::Bottom => 3, +impl SpvDriver { + pub fn new(config: &SpvConfig) -> std::io::Result { + let mut writer = Writer::for_writer(File::create(&config.file)?); + if let Some(page_setup) = &config.page_setup { + writer = writer.with_page_setup(page_setup.clone()); } + Ok(Self { writer }) } } -impl BinWrite for CellStyle { - type Args<'a> = (); - - fn write_options( - &self, - writer: &mut W, - endian: Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - ( - self.horz_align - .map_or(0xffffffad, |horz_align| horz_align.as_spv(6)), - self.vert_align.as_spv(), - self.horz_align - .map(|horz_align| horz_align.decimal_offset()) - .unwrap_or_default(), - u16::try_from(self.margins[Axis2::X][0]).unwrap_or_default(), - u16::try_from(self.margins[Axis2::X][1]).unwrap_or_default(), - u16::try_from(self.margins[Axis2::Y][0]).unwrap_or_default(), - u16::try_from(self.margins[Axis2::Y][1]).unwrap_or_default(), - ) - .write_options(writer, endian, args) - } -} - -impl<'a> BinWrite for StylePair<'a> { - type Args<'b> = (); - - fn write_options( - &self, - writer: &mut W, - endian: Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - ( - Optional(self.font_style.as_ref()), - Optional(self.cell_style.as_ref()), - ) - .write_options(writer, endian, args) - } -} - -struct Optional(Option); - -impl BinWrite for Optional +impl SpvDriver where - T: BinWrite, + W: Write + Seek, { - type Args<'a> = T::Args<'a>; - - fn write_options( - &self, - writer: &mut W, - endian: Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - match &self.0 { - Some(value) => { - 0x31u8.write_le(writer)?; - value.write_options(writer, endian, args) - } - None => 0x58u8.write_le(writer), - } - } -} - -struct ValueMod<'a> { - style: &'a Option>, - template: Option<&'a str>, -} - -impl<'a> ValueMod<'a> { - fn new(value: &'a Value) -> Self { - Self { - style: &value.styling, - template: None, - } - } -} - -impl<'a> Default for ValueMod<'a> { - fn default() -> Self { + pub fn for_writer(writer: W) -> Self { Self { - style: &None, - template: None, - } - } -} - -impl<'a> BinWrite for ValueMod<'a> { - type Args<'b> = (); - - fn write_options( - &self, - writer: &mut W, - endian: binrw::Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - if self.style.as_ref().is_some_and(|style| !style.is_empty()) || self.template.is_some() { - 0x31u8.write_options(writer, endian, args)?; - let default_style = Default::default(); - let style = self.style.as_ref().unwrap_or(&default_style); - - (style.footnotes.len() as u32).write_options(writer, endian, args)?; - for footnote in &style.footnotes { - (footnote.index() as u16).write_options(writer, endian, args)?; - } - - (style.subscripts.len() as u32).write_options(writer, endian, args)?; - for subscript in &style.subscripts { - SpvString(subscript.as_str()).write_options(writer, endian, args)?; - } - let v3_start = Count::new(writer)?; - let template_string_start = Count::new(writer)?; - if let Some(template) = self.template { - Count::new(writer)?.finish_le32(writer)?; - (0x31u8, SpvString(template)).write_options(writer, endian, args)?; - } - template_string_start.finish_le32(writer)?; - StylePair { - font_style: style.font_style.as_ref(), - cell_style: style.cell_style.as_ref(), - } - .write_options(writer, endian, args)?; - v3_start.finish_le32(writer) - } else { - 0x58u8.write_options(writer, endian, args) - } - } -} - -struct SpvFormat { - format: Format, - honor_small: bool, -} - -impl BinWrite for SpvFormat { - type Args<'a> = (); - - fn write_options( - &self, - writer: &mut W, - endian: binrw::Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - let type_ = if self.format.type_() == Type::F && self.honor_small { - 40 - } else { - self.format.type_().into() - }; - (((type_ as u32) << 16) | ((self.format.w() as u32) << 8) | (self.format.d() as u32)) - .write_options(writer, endian, args) - } -} - -impl BinWrite for Value { - type Args<'a> = (); - - fn write_options( - &self, - writer: &mut W, - endian: binrw::Endian, - args: Self::Args<'_>, - ) -> binrw::BinResult<()> { - match &self.inner { - ValueInner::Number(number) => { - let format = SpvFormat { - format: number.format, - honor_small: number.honor_small, - }; - if number.variable.is_some() || number.value_label.is_some() { - ( - 2u8, - ValueMod::new(self), - format, - number.value.unwrap_or(f64::MIN), - SpvString::optional(&number.variable), - SpvString::optional(&number.value_label), - Show::as_spv(&number.show), - ) - .write_options(writer, endian, args)?; - } else { - ( - 1u8, - ValueMod::new(self), - format, - number.value.unwrap_or(f64::MIN), - ) - .write_options(writer, endian, args)?; - } - } - ValueInner::String(string) => { - ( - 4u8, - ValueMod::new(self), - SpvFormat { - format: if string.hex { - Format::new(Type::AHex, (string.s.len() * 2) as u16, 0).unwrap() - } else { - Format::new(Type::A, (string.s.len()) as u16, 0).unwrap() - }, - honor_small: false, - }, - SpvString::optional(&string.value_label), - SpvString::optional(&string.var_name), - Show::as_spv(&string.show), - SpvString(&string.s), - ) - .write_options(writer, endian, args)?; - } - ValueInner::Variable(variable) => { - ( - 5u8, - ValueMod::new(self), - SpvString(&variable.var_name), - SpvString::optional(&variable.variable_label), - Show::as_spv(&variable.show), - ) - .write_options(writer, endian, args)?; - } - ValueInner::Markup(markup) => { - let text = markup.to_string(); - ( - 3u8, - SpvString(&text), // XXX - ValueMod::new(self), - SpvString(&text), - SpvString(&text), - SpvBool(true), - ) - .write_options(writer, endian, args)?; - } - ValueInner::Text(text) => { - ( - 3u8, - SpvString(&text.localized), - ValueMod::new(self), - SpvString(text.id()), - SpvString(text.c()), - SpvBool(true), - ) - .write_options(writer, endian, args)?; - } - ValueInner::Template(template) => { - ( - 0u8, - ValueMod::new(self), - SpvString(&template.localized), - template.args.len() as u32, - ) - .write_options(writer, endian, args)?; - for arg in &template.args { - if arg.len() > 1 { - (arg.len() as u32, 0u32).write_options(writer, endian, args)?; - for (index, value) in arg.iter().enumerate() { - if index > 0 { - 0u32.write_le(writer)?; - } - value.write_options(writer, endian, args)?; - } - } else { - (0u32, arg).write_options(writer, endian, args)?; - } - } - } - ValueInner::Empty => { - ( - 3u8, - SpvString(""), - ValueMod::default(), - SpvString(""), - SpvString(""), - SpvBool(true), - ) - .write_options(writer, endian, args)?; - } + writer: Writer::for_writer(writer), } - Ok(()) } } diff --git a/rust/pspp/src/spv.rs b/rust/pspp/src/spv.rs new file mode 100644 index 0000000000..09d05aa05d --- /dev/null +++ b/rust/pspp/src/spv.rs @@ -0,0 +1,30 @@ +// PSPP - a program for statistical analysis. +// Copyright (C) 2025 Free Software Foundation, Inc. +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +//! Reading and writing SPV files. +//! +//! This module enables reading and writing SPSS Viewer or `.spv` files, which +//! SPSS 16 and later uses to represent the contents of its output editor. The +//! SPV file format is [documented in the PSPP manual]. +//! +//! [documented in the PSPP manual]: https://pspp.benpfaff.org/manual/spv/index.html + +// Warn about missing docs, but not for items declared with `#[cfg(test)]`. +#![cfg_attr(not(test), warn(missing_docs))] + +pub use write::Writer; + +mod write; diff --git a/rust/pspp/src/spv/write.rs b/rust/pspp/src/spv/write.rs new file mode 100644 index 0000000000..08aa5f3c27 --- /dev/null +++ b/rust/pspp/src/spv/write.rs @@ -0,0 +1,1354 @@ +// PSPP - a program for statistical analysis. +// Copyright (C) 2025 Free Software Foundation, Inc. +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +use std::{ + borrow::Cow, + io::{Cursor, Seek, Write}, + iter::{repeat, repeat_n}, +}; + +use binrw::{BinWrite, Endian}; +use chrono::Utc; +use enum_map::EnumMap; +use quick_xml::{ + ElementWriter, Writer as XmlWriter, + events::{BytesText, attributes::Attribute}, +}; +use zip::{ZipWriter, result::ZipResult, write::SimpleFileOptions}; + +use crate::{ + format::{Format, Type}, + output::{ + Details, Item, Text, + page::{ChartSize, PageSetup}, + pivot::{ + Area, AreaStyle, Axis2, Axis3, Border, BorderStyle, BoxBorder, Category, CellStyle, + Color, Dimension, FontStyle, Footnote, FootnoteMarkerPosition, FootnoteMarkerType, + Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Leaf, PivotTable, + RowColBorder, RowParity, Stroke, Value, ValueInner, ValueStyle, VertAlign, + }, + spv::html::Document, + }, + settings::Show, + util::ToSmallString, +}; + +pub struct Writer +where + W: Write + Seek, +{ + writer: ZipWriter, + needs_page_break: bool, + next_table_id: u64, + next_heading_id: u64, + page_setup: Option, +} + +impl Writer +where + W: Write + Seek, +{ + pub fn for_writer(writer: W) -> Self { + let mut writer = ZipWriter::new(writer); + writer + .start_file("META-INF/MANIFEST.MF", SimpleFileOptions::default()) + .unwrap(); + writer.write_all("allowPivoting=true".as_bytes()).unwrap(); + Self { + writer, + needs_page_break: false, + next_table_id: 1, + next_heading_id: 1, + page_setup: None, + } + } + + pub fn with_page_setup(mut self, page_setup: PageSetup) -> Self { + self.set_page_setup(page_setup); + self + } + + pub fn close(mut self) -> ZipResult { + self.writer + .start_file("META-INF/MANIFEST.MF", SimpleFileOptions::default())?; + write!(&mut self.writer, "allowPivoting=true")?; + self.writer.finish() + } + + pub fn set_page_setup(&mut self, page_setup: PageSetup) { + self.page_setup = Some(page_setup); + } + + fn page_break_before(&mut self) -> bool { + let page_break_before = self.needs_page_break; + self.needs_page_break = false; + page_break_before + } + + fn write_table( + &mut self, + item: &Item, + pivot_table: &PivotTable, + structure: &mut XmlWriter, + ) where + X: Write, + { + let table_id = self.next_table_id; + self.next_table_id += 1; + + let mut content = Vec::new(); + let mut cursor = Cursor::new(&mut content); + pivot_table.write_le(&mut cursor).unwrap(); + + let table_name = format!("{table_id:011}_lightTableData.bin"); + self.writer + .start_file(&table_name, SimpleFileOptions::default()) + .unwrap(); // XXX + self.writer.write_all(&content).unwrap(); // XXX + + self.container(structure, item, "vtb:table", |element| { + element + .with_attribute(("tableId", Cow::from(table_id.to_string()))) + .with_attribute(( + "commandName", + pivot_table + .metadata + .command_local + .as_ref() + .map_or("", |s| s.as_str()), + )) + .with_attribute(("type", "table" /*XXX*/)) + .with_attribute(( + "subType", + Cow::from(pivot_table.subtype().display(pivot_table).to_string()), + )) + .write_inner_content(|w| { + w.create_element("vtb:tableStructure") + .write_inner_content(|w| { + w.create_element("vtb:dataPath") + .write_text_content(BytesText::new(&table_name))?; + Ok(()) + })?; + Ok(()) + }) + .unwrap(); + }); + } + + fn write_text(&mut self, item: &Item, text: &Text, structure: &mut XmlWriter) + where + X: Write, + { + self.container(structure, item, "vtx:text", |w| { + w.with_attribute(("type", text.type_.as_xml_str())) + .write_text_content(BytesText::new(&text.content.display(()).to_string())) + .unwrap(); + }); + } + + fn write_item(&mut self, item: &Item, structure: &mut XmlWriter) + where + X: Write, + { + match &item.details { + Details::Chart | Details::Image(_) => todo!(), + Details::Heading(children) => { + let mut attributes = Vec::::new(); + if let Some(command_name) = &item.command_name { + attributes.push(("commandName", command_name.as_str()).into()); + } + if !item.show { + attributes.push(("visibility", "collapsed").into()); + } + structure + .create_element("heading") + .with_attributes(attributes) + .write_inner_content(|w| { + w.create_element("label") + .write_text_content(BytesText::new(&item.label()))?; + for child in &children.0 { + self.write_item(&child, w); + } + Ok(()) + }) + .unwrap(); + } + Details::Message(diagnostic) => { + self.write_text(item, &Text::from(diagnostic.as_ref()), structure) + } + Details::PageBreak => { + self.needs_page_break = true; + } + Details::Table(pivot_table) => self.write_table(item, pivot_table, structure), + Details::Text(text) => self.write_text(item, text, structure), + } + } + + fn container( + &mut self, + writer: &mut XmlWriter, + item: &Item, + inner_elem: &str, + closure: F, + ) where + X: Write, + F: FnOnce(ElementWriter), + { + writer + .create_element("container") + .with_attributes( + self.page_break_before() + .then_some(("page-break-before", "always")), + ) + .with_attribute(("visibility", if item.show { "visible" } else { "hidden" })) + .write_inner_content(|w| { + let mut element = w + .create_element("label") + .write_text_content(BytesText::new(&item.label())) + .unwrap() + .create_element(inner_elem); + if let Some(command_name) = &item.command_name { + element = element.with_attribute(("commandName", command_name.as_str())); + }; + closure(element); + Ok(()) + }) + .unwrap(); + } +} + +impl BinWrite for PivotTable { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + _args: (), + ) -> binrw::BinResult<()> { + // Header. + ( + 1u8, + 0u8, + 3u32, // version + SpvBool(true), // x0 + SpvBool(false), // x1 + SpvBool(self.style.rotate_inner_column_labels), + SpvBool(self.style.rotate_outer_row_labels), + SpvBool(true), // x2 + 0x15u32, // x3 + *self.style.look.heading_widths[HeadingRegion::Columns].start() as i32, + *self.style.look.heading_widths[HeadingRegion::Columns].end() as i32, + *self.style.look.heading_widths[HeadingRegion::Rows].start() as i32, + *self.style.look.heading_widths[HeadingRegion::Rows].end() as i32, + 0u64, + ) + .write_le(writer)?; + + // Titles. + ( + self.title(), + self.subtype(), + Optional(Some(self.title())), + Optional(self.metadata.corner_text.as_ref()), + Optional(self.metadata.caption.as_ref()), + ) + .write_le(writer)?; + + // Footnotes. + self.footnotes.write_le(writer)?; + + // Areas. + static SPV_AREAS: [Area; 8] = [ + Area::Title, + Area::Caption, + Area::Footer, + Area::Corner, + Area::Labels(Axis2::X), + Area::Labels(Axis2::Y), + Area::Data(RowParity::Even), + Area::Layers, + ]; + for (index, area) in SPV_AREAS.into_iter().enumerate() { + let odd_data_style = if let Area::Data(_) = area { + Some(&self.style.look.areas[Area::Data(RowParity::Odd)]) + } else { + None + }; + self.style.look.areas[area].write_le_args(writer, (index, odd_data_style))?; + } + + // Borders. + static SPV_BORDERS: [Border; 19] = [ + Border::Title, + Border::OuterFrame(BoxBorder::Left), + Border::OuterFrame(BoxBorder::Top), + Border::OuterFrame(BoxBorder::Right), + Border::OuterFrame(BoxBorder::Bottom), + Border::InnerFrame(BoxBorder::Left), + Border::InnerFrame(BoxBorder::Top), + Border::InnerFrame(BoxBorder::Right), + Border::InnerFrame(BoxBorder::Bottom), + Border::DataLeft, + Border::DataTop, + Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X)), + Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::Y)), + Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)), + Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::Y)), + Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X)), + Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::Y)), + Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)), + Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::Y)), + ]; + let borders_start = Count::new(writer)?; + (1, SPV_BORDERS.len() as u32).write_be(writer)?; + for (index, border) in SPV_BORDERS.into_iter().enumerate() { + self.style.look.borders[border].write_be_args(writer, index)?; + } + (SpvBool(self.style.show_grid_lines), 0u8, 0u16).write_le(writer)?; + borders_start.finish_le32(writer)?; + + // Print Settings. + Counted::new(( + 1u32, + SpvBool(self.style.look.print_all_layers), + SpvBool(self.style.look.paginate_layers), + SpvBool(self.style.look.shrink_to_fit[Axis2::X]), + SpvBool(self.style.look.shrink_to_fit[Axis2::Y]), + SpvBool(self.style.look.top_continuation), + SpvBool(self.style.look.bottom_continuation), + self.style.look.n_orphan_lines as u32, + SpvString( + self.style + .look + .continuation + .as_ref() + .map_or("", |s| s.as_str()), + ), + )) + .with_endian(Endian::Little) + .write_be(writer)?; + + // Table Settings. + Counted::new(( + 1u32, + 4u32, + self.spv_layer() as u32, + SpvBool(self.style.look.hide_empty), + SpvBool(self.style.look.row_label_position == LabelPosition::Corner), + SpvBool(self.style.look.footnote_marker_type == FootnoteMarkerType::Alphabetic), + SpvBool( + self.style.look.footnote_marker_position == FootnoteMarkerPosition::Superscript, + ), + 0u8, + Counted::new(( + 0u32, // n-row-breaks + 0u32, // n-column-breaks + 0u32, // n-row-keeps + 0u32, // n-column-keeps + 0u32, // n-row-point-keeps + 0u32, // n-column-point-keeps + )), + SpvString::optional(&self.metadata.notes), + SpvString::optional(&self.style.look.name), + Zeros(82), + )) + .with_endian(Endian::Little) + .write_be(writer)?; + + fn y0(pivot_table: &PivotTable) -> impl for<'a> BinWrite = ()> { + ( + pivot_table.style.settings.epoch.0 as u32, + u8::from(pivot_table.style.settings.decimal), + b',', + ) + } + + fn custom_currency(pivot_table: &PivotTable) -> impl for<'a> BinWrite = ()> { + ( + 5, + EnumMap::from_fn(|cc| { + SpvString( + pivot_table + .style + .settings + .number_style(Type::CC(cc)) + .to_string(), + ) + }) + .into_array(), + ) + } + + fn x1(pivot_table: &PivotTable) -> impl for<'a> BinWrite = ()> { + ( + 0u8, // x14 + if pivot_table.style.show_title { + 1u8 + } else { + 10u8 + }, + 0u8, // x16 + 0u8, // lang + Show::as_spv(&pivot_table.style.show_variables), + Show::as_spv(&pivot_table.style.show_values), + -1i32, // x18 + -1i32, // x19 + Zeros(17), + SpvBool(false), // x20 + SpvBool(pivot_table.style.show_caption), + ) + } + + fn x2() -> impl for<'a> BinWrite = ()> { + Counted::new(( + 0u32, // n-row-heights + 0u32, // n-style-maps + 0u32, // n-styles, + 0u32, + )) + } + + fn y1(pivot_table: &PivotTable) -> impl for<'a> BinWrite = ()> + use<'_> { + ( + SpvString::optional(&pivot_table.metadata.command_c), + SpvString::optional(&pivot_table.metadata.command_local), + SpvString::optional(&pivot_table.metadata.language), + SpvString("UTF-8"), + SpvString::optional(&pivot_table.metadata.locale), + SpvBool(false), // x10 + SpvBool(pivot_table.style.settings.leading_zero), + SpvBool(true), // x12 + SpvBool(true), // x13 + y0(pivot_table), + ) + } + + fn y2(pivot_table: &PivotTable) -> impl for<'a> BinWrite = ()> { + (custom_currency(pivot_table), b'.', SpvBool(false)) + } + + fn x3(pivot_table: &PivotTable) -> impl for<'a> BinWrite = ()> + use<'_> { + Counted::new(( + 1u8, + 0u8, + 4u8, // x21 + 0u8, + 0u8, + 0u8, + y1(pivot_table), + pivot_table.style.small, + 1u8, + SpvString::optional(&pivot_table.metadata.dataset), + SpvString::optional(&pivot_table.metadata.datafile), + 0u32, + pivot_table + .metadata + .date + .map_or(0i64, |date| date.and_utc().timestamp()), + y2(pivot_table), + )) + } + + // Formats. + ( + 0u32, + SpvString("en_US.ISO_8859-1:1987"), + 0u32, // XXX current_layer + SpvBool(false), // x7 + SpvBool(false), // x8 + SpvBool(false), // x9 + y0(self), + custom_currency(self), + Counted::new((Counted::new((x1(self), x2())), x3(self))), + ) + .write_le(writer)?; + + // Dimensions. + (self.dimensions().len() as u32).write_le(writer)?; + + let x2 = repeat_n(2, self.axes()[Axis3::Z].dimensions.len()) + .chain(repeat_n(0, self.axes()[Axis3::Y].dimensions.len())) + .chain(repeat(1)); + for ((index, dimension), x2) in self.dimensions().iter().enumerate().zip(x2) { + dimension.write_options(writer, endian, (index, x2))?; + } + + // Axes. + for axis in [Axis3::Z, Axis3::Y, Axis3::X] { + (self.axes()[axis].dimensions.len() as u32).write_le(writer)?; + } + for axis in [Axis3::Z, Axis3::Y, Axis3::X] { + for index in self.axes()[axis].dimensions.iter().copied() { + (index as u32).write_le(writer)?; + } + } + + // Cells. + (self.cells().len() as u32).write_le(writer)?; + for (index, value) in self.cells() { + (*index as u64, value).write_le(writer)?; + } + + Ok(()) + } +} + +impl PivotTable { + fn spv_layer(&self) -> usize { + let mut layer = 0; + for (dimension, layer_value) in self + .axis_dimensions(Axis3::Z) + .zip(self.current_layer.iter().copied()) + .rev() + { + layer = layer * dimension.len() + layer_value; + } + layer + } +} + +impl Writer +where + W: Write + Seek + 'static, +{ + pub fn write(&mut self, item: &Item) { + if item.details.is_page_break() { + self.needs_page_break = true; + return; + } + + let mut headings = XmlWriter::new(Cursor::new(Vec::new())); + let element = headings + .create_element("heading") + .with_attribute(( + "creation-date-time", + Cow::from(Utc::now().format("%x %x").to_string()), + )) + .with_attribute(( + "creator", + Cow::from(format!( + "{} {}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )), + )) + .with_attribute(("creator-version", "21")) + .with_attribute(("xmlns", "http://xml.spss.com/spss/viewer/viewer-tree")) + .with_attribute(( + "xmlns:vps", + "http://xml.spss.com/spss/viewer/viewer-pagesetup", + )) + .with_attribute(("xmlns:vtx", "http://xml.spss.com/spss/viewer/viewer-text")) + .with_attribute(("xmlns:vtb", "http://xml.spss.com/spss/viewer/viewer-table")); + element + .write_inner_content(|w| { + w.create_element("label") + .write_text_content(BytesText::new("Output"))?; + if let Some(page_setup) = self.page_setup.take() { + write_page_setup(&page_setup, w)?; + } + self.write_item(item, w); + Ok(()) + }) + .unwrap(); + + let headings = headings.into_inner().into_inner(); + let heading_id = self.next_heading_id; + self.next_heading_id += 1; + self.writer + .start_file( + format!( + "outputViewer{heading_id:010}{}.xml", + if item.details.is_heading() { + "_heading" + } else { + "" + } + ), + SimpleFileOptions::default(), + ) + .unwrap(); // XXX + self.writer.write_all(&headings).unwrap(); // XXX + } +} + +fn write_page_setup(page_setup: &PageSetup, writer: &mut XmlWriter) -> std::io::Result<()> +where + X: Write, +{ + fn length(length: paper_sizes::Length) -> Cow<'static, str> { + Cow::from(length.to_string()) + } + + writer + .create_element("vps:pageSetup") + .with_attribute(( + "initial-page-number", + Cow::from(format!("{}", page_setup.initial_page_number)), + )) + .with_attribute(( + "chart-size", + match page_setup.chart_size { + ChartSize::AsIs => "as-is", + ChartSize::FullHeight => "full-height", + ChartSize::HalfHeight => "half-height", + ChartSize::QuarterHeight => "quarter-height", + }, + )) + .with_attribute(("margin-left", length(page_setup.margins.0[Axis2::X][0]))) + .with_attribute(("margin-right", length(page_setup.margins.0[Axis2::X][1]))) + .with_attribute(("margin-top", length(page_setup.margins.0[Axis2::Y][0]))) + .with_attribute(("margin-bottom", length(page_setup.margins.0[Axis2::Y][1]))) + .with_attribute(("paper-height", length(page_setup.paper.height()))) + .with_attribute(("paper-width", length(page_setup.paper.width()))) + .with_attribute(( + "reference-orientation", + match page_setup.orientation { + crate::output::page::Orientation::Portrait => "portrait", + crate::output::page::Orientation::Landscape => "landscape", + }, + )) + .with_attribute(("space-after", length(page_setup.object_spacing))) + .write_inner_content(|w| { + write_page_heading(&page_setup.header, "vps:pageHeader", w)?; + write_page_heading(&page_setup.footer, "vps:pageFooter", w)?; + Ok(()) + })?; + Ok(()) +} + +fn write_page_heading( + heading: &Document, + name: &str, + writer: &mut XmlWriter, +) -> std::io::Result<()> +where + X: Write, +{ + let element = writer.create_element(name); + if !heading.is_empty() { + element.write_inner_content(|w| { + w.create_element("vps:pageParagraph") + .write_inner_content(|w| { + w.create_element("vtx:text") + .with_attribute(("text", "title")) + .write_text_content(BytesText::new(&heading.to_html()))?; + Ok(()) + })?; + Ok(()) + })?; + } + Ok(()) +} + +impl BinWrite for Dimension { + type Args<'a> = (usize, u8); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + (index, x2): (usize, u8), + ) -> binrw::BinResult<()> { + ( + &self.root.name, + 0u8, // x1 + x2, + 2u32, // x3 + SpvBool(!self.root.show_label), + SpvBool(self.hide_all_labels), + SpvBool(true), + index as u32, + self.root.children.len() as u32, + ) + .write_options(writer, endian, ())?; + + let mut data_indexes = self.presentation_order.iter().copied(); + for child in &self.root.children { + child.write_le(writer, &mut data_indexes)?; + } + Ok(()) + } +} + +impl Category { + fn write_le(&self, writer: &mut W, data_indexes: &mut D) -> binrw::BinResult<()> + where + W: Write + Seek, + D: Iterator, + { + match self { + Category::Group(group) => group.write_le(writer, data_indexes), + Category::Leaf(leaf) => leaf.write_le(writer, data_indexes), + } + } +} + +impl Leaf { + fn write_le(&self, writer: &mut W, data_indexes: &mut D) -> binrw::BinResult<()> + where + W: Write + Seek, + D: Iterator, + { + ( + self.name(), + 0u8, + 0u8, + 0u8, + 2u32, + data_indexes.next().unwrap() as u32, + 0u32, + ) + .write_le(writer) + } +} + +impl Group { + fn write_le(&self, writer: &mut W, data_indexes: &mut D) -> binrw::BinResult<()> + where + W: Write + Seek, + D: Iterator, + { + ( + self.name(), + 0u8, // merge + 0u8, + 1u8, + 0u32, // x23 + -1i32, + self.children.len() as u32, + ) + .write_le(writer)?; + + for child in &self.children { + child.write_le(writer, data_indexes)?; + } + Ok(()) + } +} + +impl BinWrite for Footnote { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + ( + &self.content, + Optional(self.marker.as_ref()), + if self.show { 1i32 } else { -1 }, + ) + .write_options(writer, endian, args) + } +} + +impl BinWrite for Footnotes { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + (self.len() as u32).write_options(writer, endian, args)?; + for footnote in self { + footnote.write_options(writer, endian, args)?; + } + Ok(()) + } +} + +impl BinWrite for AreaStyle { + type Args<'a> = (usize, Option<&'a AreaStyle>); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + (index, odd_data_style): (usize, Option<&AreaStyle>), + ) -> binrw::BinResult<()> { + let typeface = if self.font_style.font.is_empty() { + "SansSerif" + } else { + self.font_style.font.as_str() + }; + ( + (index + 1) as u8, + 0x31u8, + SpvString(typeface), + self.font_style.size as f32 * 1.33, + self.font_style.bold as u32 + 2 * self.font_style.italic as u32, + SpvBool(self.font_style.underline), + self.cell_style + .horz_align + .map_or(64173, |horz_align| horz_align.as_spv(61453)), + self.cell_style.vert_align.as_spv(), + self.font_style.fg, + self.font_style.bg, + ) + .write_options(writer, endian, ())?; + + let alt_fg = odd_data_style.map_or(self.font_style.fg, |style| style.font_style.fg); + let alt_bg = odd_data_style.map_or(self.font_style.bg, |style| style.font_style.bg); + if self.font_style.fg != alt_fg || self.font_style.bg != alt_bg { + (SpvBool(true), alt_fg, alt_bg).write_options(writer, endian, ())?; + } else { + (SpvBool(false), SpvString(""), SpvString("")).write_options(writer, endian, ())?; + } + + ( + self.cell_style.margins[Axis2::X][0], + self.cell_style.margins[Axis2::X][1], + self.cell_style.margins[Axis2::Y][0], + self.cell_style.margins[Axis2::Y][1], + ) + .write_options(writer, endian, ()) + } +} + +impl Stroke { + fn as_spv(&self) -> u32 { + match self { + Stroke::None => 0, + Stroke::Solid => 1, + Stroke::Dashed => 2, + Stroke::Thick => 3, + Stroke::Thin => 4, + Stroke::Double => 5, + } + } +} + +impl Color { + fn as_spv(&self) -> u32 { + ((self.alpha as u32) << 24) + | ((self.r as u32) << 16) + | ((self.g as u32) << 8) + | (self.b as u32) + } +} + +impl BinWrite for BorderStyle { + type Args<'a> = usize; + + fn write_options( + &self, + writer: &mut W, + _endian: Endian, + index: usize, + ) -> binrw::BinResult<()> { + (index as u32, self.stroke.as_spv(), self.color.as_spv()).write_be(writer) + } +} + +struct SpvBool(bool); +impl BinWrite for SpvBool { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + (self.0 as u8).write_options(writer, endian, args) + } +} + +struct SpvString(T); +impl<'a> SpvString<&'a str> { + fn optional(s: &'a Option) -> Self { + Self(s.as_ref().map_or("", |s| s.as_str())) + } +} +impl BinWrite for SpvString +where + T: AsRef, +{ + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + let s = self.0.as_ref(); + let length = s.len() as u32; + (length, s.as_bytes()).write_options(writer, endian, args) + } +} + +impl Show { + fn as_spv(this: &Option) -> u8 { + match this { + None => 0, + Some(Show::Value) => 1, + Some(Show::Label) => 2, + Some(Show::Both) => 3, + } + } +} + +struct Count(u64); + +impl Count { + fn new(writer: &mut W) -> binrw::BinResult + where + W: Write + Seek, + { + 0u32.write_le(writer)?; + Ok(Self(writer.stream_position()?)) + } + + fn finish(self, writer: &mut W, endian: Endian) -> binrw::BinResult<()> + where + W: Write + Seek, + { + let saved_position = writer.stream_position()?; + let n_bytes = saved_position - self.0; + writer.seek(std::io::SeekFrom::Start(self.0 - 4))?; + (n_bytes as u32).write_options(writer, endian, ())?; + writer.seek(std::io::SeekFrom::Start(saved_position))?; + Ok(()) + } + + fn finish_le32(self, writer: &mut W) -> binrw::BinResult<()> + where + W: Write + Seek, + { + self.finish(writer, Endian::Little) + } +} + +struct Counted { + inner: T, + endian: Option, +} + +impl Counted { + fn new(inner: T) -> Self { + Self { + inner, + endian: None, + } + } + fn with_endian(self, endian: Endian) -> Self { + Self { + inner: self.inner, + endian: Some(endian), + } + } +} + +impl BinWrite for Counted +where + T: BinWrite, + for<'a> T: BinWrite = ()>, +{ + type Args<'a> = T::Args<'a>; + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + let start = Count::new(writer)?; + self.inner.write_options(writer, endian, args)?; + start.finish(writer, self.endian.unwrap_or(endian)) + } +} + +pub struct Zeros(pub usize); + +impl BinWrite for Zeros { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + _endian: Endian, + _args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + for _ in 0..self.0 { + writer.write_all(&[0u8])?; + } + Ok(()) + } +} + +#[derive(Default)] +struct StylePair<'a> { + font_style: Option<&'a FontStyle>, + cell_style: Option<&'a CellStyle>, +} + +impl BinWrite for Color { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + SpvString(&self.without_alpha().display_css().to_small_string::<16>()) + .write_options(writer, endian, args) + } +} + +impl BinWrite for FontStyle { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + let typeface = if self.font.is_empty() { + "SansSerif" + } else { + self.font.as_str() + }; + ( + SpvBool(self.bold), + SpvBool(self.italic), + SpvBool(self.underline), + SpvBool(true), + self.fg, + self.bg, + SpvString(typeface), + (self.size as f64 * 1.33).ceil() as u8, + ) + .write_options(writer, endian, args) + } +} + +impl HorzAlign { + fn as_spv(&self, decimal: u32) -> u32 { + match self { + HorzAlign::Right => 4, + HorzAlign::Left => 2, + HorzAlign::Center => 0, + HorzAlign::Decimal { .. } => decimal, + } + } + + fn decimal_offset(&self) -> Option { + match *self { + HorzAlign::Decimal { offset, .. } => Some(offset), + _ => None, + } + } +} + +impl VertAlign { + fn as_spv(&self) -> u32 { + match self { + VertAlign::Top => 1, + VertAlign::Middle => 0, + VertAlign::Bottom => 3, + } + } +} + +impl BinWrite for CellStyle { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + ( + self.horz_align + .map_or(0xffffffad, |horz_align| horz_align.as_spv(6)), + self.vert_align.as_spv(), + self.horz_align + .map(|horz_align| horz_align.decimal_offset()) + .unwrap_or_default(), + u16::try_from(self.margins[Axis2::X][0]).unwrap_or_default(), + u16::try_from(self.margins[Axis2::X][1]).unwrap_or_default(), + u16::try_from(self.margins[Axis2::Y][0]).unwrap_or_default(), + u16::try_from(self.margins[Axis2::Y][1]).unwrap_or_default(), + ) + .write_options(writer, endian, args) + } +} + +impl<'a> BinWrite for StylePair<'a> { + type Args<'b> = (); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + ( + Optional(self.font_style.as_ref()), + Optional(self.cell_style.as_ref()), + ) + .write_options(writer, endian, args) + } +} + +struct Optional(Option); + +impl BinWrite for Optional +where + T: BinWrite, +{ + type Args<'a> = T::Args<'a>; + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + match &self.0 { + Some(value) => { + 0x31u8.write_le(writer)?; + value.write_options(writer, endian, args) + } + None => 0x58u8.write_le(writer), + } + } +} + +struct ValueMod<'a> { + style: &'a Option>, + template: Option<&'a str>, +} + +impl<'a> ValueMod<'a> { + fn new(value: &'a Value) -> Self { + Self { + style: &value.styling, + template: None, + } + } +} + +impl<'a> Default for ValueMod<'a> { + fn default() -> Self { + Self { + style: &None, + template: None, + } + } +} + +impl<'a> BinWrite for ValueMod<'a> { + type Args<'b> = (); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + if self.style.as_ref().is_some_and(|style| !style.is_empty()) || self.template.is_some() { + 0x31u8.write_options(writer, endian, args)?; + let default_style = Default::default(); + let style = self.style.as_ref().unwrap_or(&default_style); + + (style.footnotes.len() as u32).write_options(writer, endian, args)?; + for footnote in &style.footnotes { + (footnote.index() as u16).write_options(writer, endian, args)?; + } + + (style.subscripts.len() as u32).write_options(writer, endian, args)?; + for subscript in &style.subscripts { + SpvString(subscript.as_str()).write_options(writer, endian, args)?; + } + let v3_start = Count::new(writer)?; + let template_string_start = Count::new(writer)?; + if let Some(template) = self.template { + Count::new(writer)?.finish_le32(writer)?; + (0x31u8, SpvString(template)).write_options(writer, endian, args)?; + } + template_string_start.finish_le32(writer)?; + StylePair { + font_style: style.font_style.as_ref(), + cell_style: style.cell_style.as_ref(), + } + .write_options(writer, endian, args)?; + v3_start.finish_le32(writer) + } else { + 0x58u8.write_options(writer, endian, args) + } + } +} + +struct SpvFormat { + format: Format, + honor_small: bool, +} + +impl BinWrite for SpvFormat { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + let type_ = if self.format.type_() == Type::F && self.honor_small { + 40 + } else { + self.format.type_().into() + }; + (((type_ as u32) << 16) | ((self.format.w() as u32) << 8) | (self.format.d() as u32)) + .write_options(writer, endian, args) + } +} + +impl BinWrite for Value { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + match &self.inner { + ValueInner::Number(number) => { + let format = SpvFormat { + format: number.format, + honor_small: number.honor_small, + }; + if number.variable.is_some() || number.value_label.is_some() { + ( + 2u8, + ValueMod::new(self), + format, + number.value.unwrap_or(f64::MIN), + SpvString::optional(&number.variable), + SpvString::optional(&number.value_label), + Show::as_spv(&number.show), + ) + .write_options(writer, endian, args)?; + } else { + ( + 1u8, + ValueMod::new(self), + format, + number.value.unwrap_or(f64::MIN), + ) + .write_options(writer, endian, args)?; + } + } + ValueInner::String(string) => { + ( + 4u8, + ValueMod::new(self), + SpvFormat { + format: if string.hex { + Format::new(Type::AHex, (string.s.len() * 2) as u16, 0).unwrap() + } else { + Format::new(Type::A, (string.s.len()) as u16, 0).unwrap() + }, + honor_small: false, + }, + SpvString::optional(&string.value_label), + SpvString::optional(&string.var_name), + Show::as_spv(&string.show), + SpvString(&string.s), + ) + .write_options(writer, endian, args)?; + } + ValueInner::Variable(variable) => { + ( + 5u8, + ValueMod::new(self), + SpvString(&variable.var_name), + SpvString::optional(&variable.variable_label), + Show::as_spv(&variable.show), + ) + .write_options(writer, endian, args)?; + } + ValueInner::Markup(markup) => { + let text = markup.to_string(); + ( + 3u8, + SpvString(&text), // XXX + ValueMod::new(self), + SpvString(&text), + SpvString(&text), + SpvBool(true), + ) + .write_options(writer, endian, args)?; + } + ValueInner::Text(text) => { + ( + 3u8, + SpvString(&text.localized), + ValueMod::new(self), + SpvString(text.id()), + SpvString(text.c()), + SpvBool(true), + ) + .write_options(writer, endian, args)?; + } + ValueInner::Template(template) => { + ( + 0u8, + ValueMod::new(self), + SpvString(&template.localized), + template.args.len() as u32, + ) + .write_options(writer, endian, args)?; + for arg in &template.args { + if arg.len() > 1 { + (arg.len() as u32, 0u32).write_options(writer, endian, args)?; + for (index, value) in arg.iter().enumerate() { + if index > 0 { + 0u32.write_le(writer)?; + } + value.write_options(writer, endian, args)?; + } + } else { + (0u32, arg).write_options(writer, endian, args)?; + } + } + } + ValueInner::Empty => { + ( + 3u8, + SpvString(""), + ValueMod::default(), + SpvString(""), + SpvString(""), + SpvBool(true), + ) + .write_options(writer, endian, args)?; + } + } + Ok(()) + } +} diff --git a/rust/pspp/src/sys/write.rs b/rust/pspp/src/sys/write.rs index 350f7d0720..644d621db8 100644 --- a/rust/pspp/src/sys/write.rs +++ b/rust/pspp/src/sys/write.rs @@ -37,7 +37,6 @@ use crate::{ dictionary::{CategoryLabels, Dictionary, MultipleResponseType}, format::{DisplayPlain, Format}, identifier::Identifier, - output::drivers::spv::Zeros, sys::{ ProductVersion, encoding::codepage_from_encoding, @@ -1215,6 +1214,24 @@ where } } +pub struct Zeros(pub usize); + +impl BinWrite for Zeros { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + _endian: Endian, + _args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + for _ in 0..self.0 { + writer.write_all(&[0u8])?; + } + Ok(()) + } +} + #[cfg(test)] mod tests { use std::io::Cursor; -- 2.30.2