work on spv writer
authorBen Pfaff <blp@cs.stanford.edu>
Tue, 13 May 2025 02:36:07 +0000 (19:36 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Tue, 13 May 2025 02:36:07 +0000 (19:36 -0700)
rust/pspp/src/output/pivot/mod.rs
rust/pspp/src/output/spv.rs

index 02ec83193eac2dc0a22463112c3285c226ace72e..1f5ffea12ec8e1b3804031da500290c09ac3fb36 100644 (file)
@@ -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<Axis2> for Axis3 {
 #[derive(Clone, Debug, Default)]
 pub struct Axis {
     /// `dimensions[0]` is the innermost dimension.
-    dimensions: Vec<usize>,
+    pub dimensions: Vec<usize>,
 }
 
 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<Arc<Footnote>>);
+pub struct Footnotes(pub Vec<Arc<Footnote>>);
 
 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<Item = &Dimension> + 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 {
index c397beee4dee2b7a62d178f1484b84e946ea3c44..eea552b2de6a4ac4b033c9e10e4fd2fd78f23acd 100644 (file)
@@ -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<W: Write + Seek>(
+        &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<W> Driver for SpvWriter<W>
 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<W: Write + Seek>(
+        &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<W: Write + Seek>(
+        &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<W: Write + Seek>(
+        &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<W: Write + Seek>(
+        &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<W: Write + Seek>(
@@ -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<T>(Option<T>);
 
-        Ok(())
+impl<T> BinWrite for Optional<T>
+where
+    T: BinWrite,
+{
+    type Args<'a> = T::Args<'a>;
+
+    fn write_options<W: Write + Seek>(
+        &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)?;
             }