From 38e7988f038c032d5673a77c0f32ef760328dca0 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Mon, 12 May 2025 19:36:07 -0700 Subject: [PATCH] work on spv writer --- rust/pspp/src/output/pivot/mod.rs | 37 ++- rust/pspp/src/output/spv.rs | 407 ++++++++++++++++++++++++++---- 2 files changed, 393 insertions(+), 51 deletions(-) diff --git a/rust/pspp/src/output/pivot/mod.rs b/rust/pspp/src/output/pivot/mod.rs index 02ec83193e..1f5ffea12e 100644 --- a/rust/pspp/src/output/pivot/mod.rs +++ b/rust/pspp/src/output/pivot/mod.rs @@ -185,9 +185,9 @@ pub enum BoxBorder { #[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)] pub struct RowColBorder( /// Row or column headings. - HeadingRegion, + pub HeadingRegion, /// Horizontal ([Axis2::X]) or vertical ([Axis2::Y]) borders. - Axis2, + pub Axis2, ); /// Sizing for rows or columns of a rendered table. @@ -237,7 +237,7 @@ impl From for Axis3 { #[derive(Clone, Debug, Default)] pub struct Axis { /// `dimensions[0]` is the innermost dimension. - dimensions: Vec, + pub dimensions: Vec, } pub struct AxisIterator { @@ -362,6 +362,7 @@ impl Dimension { self.len() == 0 } + /// Returns the number of (leaf) categories in this dimension. pub fn len(&self) -> usize { self.root.len() } @@ -456,7 +457,7 @@ impl Group { } #[derive(Clone, Debug, Default)] -pub struct Footnotes(Vec>); +pub struct Footnotes(pub Vec>); impl Footnotes { pub fn new() -> Self { @@ -1349,6 +1350,26 @@ impl PivotTable { None => String::from("Table"), } } + + pub fn title(&self) -> &Value { + match &self.title { + Some(title) => &*title, + None => { + static EMPTY: Value = Value::empty(); + &EMPTY + } + } + } + + pub fn subtype(&self) -> &Value { + match &self.subtype { + Some(subtype) => &*subtype, + None => { + static EMPTY: Value = Value::empty(); + &EMPTY + } + } + } } impl Default for PivotTable { @@ -1475,7 +1496,7 @@ impl PivotTable { self.axes.swap(Axis3::X, Axis3::Y); } - fn axis_dimensions( + pub fn axis_dimensions( &self, axis: Axis3, ) -> impl DoubleEndedIterator + ExactSizeIterator { @@ -1702,6 +1723,12 @@ impl Value { footnotes.sort_by_key(|f| f.index); self } + pub const fn empty() -> Self { + Value { + inner: ValueInner::Empty, + styling: None, + } + } } impl From<&str> for Value { diff --git a/rust/pspp/src/output/spv.rs b/rust/pspp/src/output/spv.rs index c397beee4d..eea552b2de 100644 --- a/rust/pspp/src/output/spv.rs +++ b/rust/pspp/src/output/spv.rs @@ -21,8 +21,9 @@ use crate::{ output::{ driver::Driver, pivot::{ - Axis2, CellStyle, Color, FontStyle, HeadingRegion, HorzAlign, PivotTable, StringValue, - TemplateValue, TextValue, Value, ValueInner, ValueStyle, VariableValue, VertAlign, + Area, AreaStyle, Axis2, Axis3, Border, BorderStyle, BoxBorder, CellStyle, Color, + FontStyle, Footnote, Footnotes, HeadingRegion, HorzAlign, PivotTable, RowColBorder, + Stroke, Value, ValueInner, ValueStyle, VertAlign, }, Item, }, @@ -73,7 +74,7 @@ where let mut content = Vec::new(); let mut cursor = Cursor::new(&mut content); - Header::new(pivot_table).write_le(&mut cursor).unwrap(); + pivot_table.write_be(&mut cursor).unwrap(); self.writer .start_file(light_table_name(table_id), SimpleFileOptions::default()) @@ -98,6 +99,115 @@ where } } +impl BinWrite for PivotTable { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + _endian: Endian, + _args: (), + ) -> binrw::BinResult<()> { + // Header. + Header::new(self).write_le(writer)?; + + // Titles. + ( + self.title(), + self.subtype(), + Optional(Some(self.title())), + Optional(self.corner_text.as_ref()), + Optional(self.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, + Area::Layers, + ]; + for (index, area) in SPV_AREAS.into_iter().enumerate() { + self.look.areas[area].write_le_args(writer, index)?; + } + + // 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.look.borders[border].write_be_args(writer, index)?; + } + (SpvBool(self.show_grid_lines), 0u8, 0u16).write_le(writer)?; + borders_start.finish_le32(writer)?; + + // Print Settings. + let ps_start = Count::new(writer)?; + ( + 1u32, + SpvBool(self.look.print_all_layers), + SpvBool(self.look.paginate_layers), + SpvBool(self.look.shrink_to_fit[Axis2::X]), + SpvBool(self.look.shrink_to_fit[Axis2::Y]), + SpvBool(self.look.top_continuation), + SpvBool(self.look.bottom_continuation), + self.look.n_orphan_lines as u32, + SpvString(self.look.continuation.as_ref().map_or("", |s| s.as_str())), + ) + .write_be(writer)?; + ps_start.finish_le32(writer)?; + + // Table Settings. + let ts_start = Count::new(writer)?; + (1u32, 4u32, self.spv_layer() as u32).write_be(writer)?; + ts_start.finish_le32(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 Driver for SpvWriter where W: Write + Seek, @@ -110,14 +220,14 @@ where match &item.details { super::Details::Chart => todo!(), super::Details::Image => todo!(), - super::Details::Group(items) => todo!(), - super::Details::Message(diagnostic) => todo!(), + super::Details::Group(_items) => todo!(), + super::Details::Message(_diagnostic) => todo!(), super::Details::PageBreak => { self.needs_page_break = true; return; } super::Details::Table(pivot_table) => self.write_table(&*item, pivot_table), - super::Details::Text(text) => todo!(), + super::Details::Text(_text) => todo!(), }; todo!() } @@ -285,8 +395,130 @@ impl Table { #[derive(Serialize)] struct TableStructure; -struct Bool(bool); -impl BinWrite for Bool { +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.0.len() as u32).write_options(writer, endian, args)?; + for footnote in &self.0 { + footnote.write_options(writer, endian, args)?; + } + Ok(()) + } +} + +impl BinWrite for AreaStyle { + type Args<'a> = usize; + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + index: usize, + ) -> 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[0], + self.font_style.bg[0], + ) + .write_options(writer, endian, ())?; + + if self.font_style.fg[0] != self.font_style.fg[1] + || self.font_style.bg[0] != self.font_style.bg[1] + { + (SpvBool(true), self.font_style.fg[1], self.font_style.bg[1]).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( @@ -325,11 +557,11 @@ struct Header { #[bw(magic(1u16))] version: u8, - x0: Bool, - x1: Bool, - rotate_inner_column_labels: Bool, - rotate_outer_row_labels: Bool, - x2: Bool, + x0: SpvBool, + x1: SpvBool, + rotate_inner_column_labels: SpvBool, + rotate_outer_row_labels: SpvBool, + x2: SpvBool, x3: u32, min_col_heading_width: i32, max_col_heading_width: i32, @@ -341,11 +573,11 @@ impl Header { fn new(pivot_table: &PivotTable) -> Self { Self { version: 3, - x0: Bool(true), - x1: Bool(false), - rotate_inner_column_labels: Bool(pivot_table.rotate_inner_column_labels), - rotate_outer_row_labels: Bool(pivot_table.rotate_outer_row_labels), - x2: Bool(true), + x0: SpvBool(true), + x1: SpvBool(false), + rotate_inner_column_labels: SpvBool(pivot_table.rotate_inner_column_labels), + rotate_outer_row_labels: SpvBool(pivot_table.rotate_outer_row_labels), + x2: SpvBool(true), x3: 0x15, min_col_heading_width: *pivot_table.look.heading_widths[HeadingRegion::Columns].start() as i32, @@ -444,10 +676,10 @@ impl BinWrite for FontStyle { self.font.as_str() }; ( - Bool(self.bold), - Bool(self.italic), - Bool(self.underline), - Bool(true), + SpvBool(self.bold), + SpvBool(self.italic), + SpvBool(self.underline), + SpvBool(true), self.fg[0], self.bg[0], SpvString(typeface), @@ -519,19 +751,35 @@ impl<'a> BinWrite for StylePair<'a> { endian: Endian, args: Self::Args<'_>, ) -> binrw::BinResult<()> { - if let Some(font_style) = self.font_style { - (0x31u8, font_style).write_options(writer, endian, args)?; - } else { - 0x58u8.write_options(writer, endian, args)?; - } + ( + Optional(self.font_style.as_ref()), + Optional(self.cell_style.as_ref()), + ) + .write_options(writer, endian, args) + } +} - if let Some(cell_style) = self.cell_style { - (0x31u8, cell_style).write_options(writer, endian, args)?; - } else { - 0x58u8.write_options(writer, endian, args)?; - } +struct Optional(Option); - Ok(()) +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), + } } } @@ -540,6 +788,15 @@ struct OptionalStyle<'a> { template: Option<&'a str>, } +impl<'a> OptionalStyle<'a> { + fn new(value: &'a Value) -> Self { + Self { + style: &value.styling, + template: None, + } + } +} + impl<'a> Default for OptionalStyle<'a> { fn default() -> Self { Self { @@ -633,9 +890,9 @@ impl BinWrite for Value { match &self.inner { ValueInner::Number(number) => { if number.var_name.is_some() || number.value_label.is_some() { - 2u8.write_options(writer, endian, args)?; - //write_optional_style(self.styling.as_ref(),writer, endian, args)?; ( + 2u8, + OptionalStyle::new(self), SpvFormat { format: number.format, honor_small: number.honor_small, @@ -647,19 +904,77 @@ impl BinWrite for Value { ) .write_options(writer, endian, args)?; } else { - 1u8.write_options(writer, endian, args)?; - //write_optional_style(self.styling.as_ref(),writer, endian, args)?; - number - .value - .unwrap_or(-f64::MAX) + ( + 1u8, + OptionalStyle::new(self), + number.value.unwrap_or(-f64::MAX), + Show::as_spv(&number.show), + ) .write_options(writer, endian, args)?; - Show::as_spv(&number.show).write_options(writer, endian, args)?; } } - ValueInner::String(_string) => todo!(), - ValueInner::Variable(_variable) => todo!(), - ValueInner::Text(_text) => todo!(), - ValueInner::Template(_template) => todo!(), + ValueInner::String(string) => { + ( + 4u8, + OptionalStyle::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, + OptionalStyle::new(self), + SpvString(&variable.var_name), + SpvString::optional(&variable.variable_label), + Show::as_spv(&variable.show), + ) + .write_options(writer, endian, args)?; + } + ValueInner::Text(text) => { + ( + 3u8, + SpvString(&text.local), + OptionalStyle::new(self), + SpvString(&text.id), + SpvString(&text.c), + SpvBool(true), + ) + .write_options(writer, endian, args)?; + } + ValueInner::Template(template) => { + ( + 0u8, + OptionalStyle::new(self), + SpvString(&template.local), + 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, @@ -667,7 +982,7 @@ impl BinWrite for Value { OptionalStyle::default(), SpvString(""), SpvString(""), - Bool(true), + SpvBool(true), ) .write_options(writer, endian, args)?; } -- 2.30.2