From: Ben Pfaff Date: Thu, 11 Dec 2025 17:28:47 +0000 (-0800) Subject: add new pivot::look module. X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=610fee046bea89146303b17ed4c271f8f6b09156;p=pspp add new pivot::look module. --- diff --git a/rust/pspp/src/output.rs b/rust/pspp/src/output.rs index 225051e6e2..106616679d 100644 --- a/rust/pspp/src/output.rs +++ b/rust/pspp/src/output.rs @@ -37,7 +37,10 @@ use serde::Serialize; use crate::{ message::{Diagnostic, Severity}, - output::pivot::{Axis3, BorderStyle, Dimension, Group, Look}, + output::pivot::{ + Axis3, Dimension, Group, + look::{BorderStyle, Look}, + }, }; use self::pivot::Value; diff --git a/rust/pspp/src/output/drivers/cairo.rs b/rust/pspp/src/output/drivers/cairo.rs index b1b34d5f13..f64fe1bf37 100644 --- a/rust/pspp/src/output/drivers/cairo.rs +++ b/rust/pspp/src/output/drivers/cairo.rs @@ -16,7 +16,7 @@ use pango::SCALE; -use crate::output::pivot::HorzAlign; +use crate::output::pivot::look::HorzAlign; mod driver; pub mod fsm; diff --git a/rust/pspp/src/output/drivers/cairo/driver.rs b/rust/pspp/src/output/drivers/cairo/driver.rs index 7a4933ae85..bbe75c40b3 100644 --- a/rust/pspp/src/output/drivers/cairo/driver.rs +++ b/rust/pspp/src/output/drivers/cairo/driver.rs @@ -38,7 +38,10 @@ use crate::{ }, }, page::PageSetup, - pivot::{Color, Coord2, FontStyle}, + pivot::{ + Coord2, + look::{Color, FontStyle}, + }, }, spv::html::Variable, }; diff --git a/rust/pspp/src/output/drivers/cairo/fsm.rs b/rust/pspp/src/output/drivers/cairo/fsm.rs index 4f38fabc3f..277666cca0 100644 --- a/rust/pspp/src/output/drivers/cairo/fsm.rs +++ b/rust/pspp/src/output/drivers/cairo/fsm.rs @@ -26,13 +26,19 @@ use pango::{ use pangocairo::functions::show_layout; use smallvec::{SmallVec, smallvec}; -use crate::output::drivers::cairo::{px_to_xr, xr_to_pt}; -use crate::output::pivot::{Axis2, BorderStyle, Coord2, FontStyle, HorzAlign, Rect2, Stroke}; -use crate::output::render::{Device, Extreme, Pager, Params}; -use crate::output::table::DrawCell; -use crate::output::{Details, Item}; -use crate::output::{pivot::Color, table::Content}; -use crate::spv::html::Markup; +use crate::{ + output::{ + Details, Item, + drivers::cairo::{px_to_xr, xr_to_pt}, + pivot::{ + Axis2, Coord2, Rect2, + look::{BorderStyle, Color, FontStyle, HorzAlign, Stroke}, + }, + render::{Device, Extreme, Pager, Params}, + table::{Content, DrawCell}, + }, + spv::html::Markup, +}; /// Width of an ordinary line. const LINE_WIDTH: isize = LINE_SPACE / 2; diff --git a/rust/pspp/src/output/drivers/cairo/pager.rs b/rust/pspp/src/output/drivers/cairo/pager.rs index 62f999d2b6..5ae677e19d 100644 --- a/rust/pspp/src/output/drivers/cairo/pager.rs +++ b/rust/pspp/src/output/drivers/cairo/pager.rs @@ -27,7 +27,11 @@ use crate::{ fsm::{CairoFsm, CairoFsmStyle}, xr_to_pt, }, - pivot::{Axis2, CellStyle, FontStyle, Rect2, value::ValueOptions}, + pivot::{ + Axis2, Rect2, + look::{CellStyle, FontStyle}, + value::ValueOptions, + }, table::DrawCell, }, spv::html::{Document, Variable}, diff --git a/rust/pspp/src/output/drivers/html.rs b/rust/pspp/src/output/drivers/html.rs index ad19aded6c..6552ebe8ae 100644 --- a/rust/pspp/src/output/drivers/html.rs +++ b/rust/pspp/src/output/drivers/html.rs @@ -29,7 +29,10 @@ use smallstr::SmallString; use crate::output::{ Details, Item, drivers::Driver, - pivot::{Axis2, BorderStyle, Color, HorzAlign, PivotTable, Stroke, VertAlign}, + pivot::{ + Axis2, PivotTable, + look::{BorderStyle, Color, HorzAlign, Stroke, VertAlign}, + }, table::{CellPos, CellRect, DrawCell, Table}, }; diff --git a/rust/pspp/src/output/drivers/text.rs b/rust/pspp/src/output/drivers/text.rs index 423664dd02..333a902908 100644 --- a/rust/pspp/src/output/drivers/text.rs +++ b/rust/pspp/src/output/drivers/text.rs @@ -34,7 +34,10 @@ use crate::output::{ItemRefIterator, render::Extreme, table::DrawCell}; use crate::output::{ Details, Item, drivers::Driver, - pivot::{Axis2, BorderStyle, Coord2, HorzAlign, PivotTable, Rect2, Stroke}, + pivot::{ + Axis2, Coord2, PivotTable, Rect2, + look::{BorderStyle, HorzAlign, Stroke}, + }, render::{Device, Pager, Params}, table::Content, }; diff --git a/rust/pspp/src/output/drivers/text/text_line.rs b/rust/pspp/src/output/drivers/text/text_line.rs index b986163011..bd31c5f932 100644 --- a/rust/pspp/src/output/drivers/text/text_line.rs +++ b/rust/pspp/src/output/drivers/text/text_line.rs @@ -24,7 +24,7 @@ use std::{ use unicode_width::UnicodeWidthChar; -use crate::output::pivot::FontStyle; +use crate::output::pivot::look::FontStyle; /// A line of text, encoded in UTF-8, with support functions that properly /// handle double-width characters and backspaces. diff --git a/rust/pspp/src/output/pivot.rs b/rust/pspp/src/output/pivot.rs index 1944ef3a26..f418636b17 100644 --- a/rust/pspp/src/output/pivot.rs +++ b/rust/pspp/src/output/pivot.rs @@ -45,39 +45,26 @@ use std::{ collections::HashMap, fmt::{Debug, Display}, - io::Read, iter::{FusedIterator, once, repeat_n}, - ops::{Index, IndexMut, Not, Range, RangeInclusive}, - str::{FromStr, Utf8Error, from_utf8}, - sync::{Arc, OnceLock}, + ops::{Index, IndexMut, Not, Range}, + sync::Arc, }; -use binrw::Error as BinError; use chrono::NaiveDateTime; -pub use color::ParseError as ParseColorError; -use color::{AlphaColor, Rgba8, Srgb, palette::css::TRANSPARENT}; use enum_iterator::Sequence; use enum_map::{Enum, EnumMap, enum_map}; use itertools::Itertools; pub use look_xml::{Length, TableProperties}; -use quick_xml::{DeError, de::from_str}; -use serde::{ - Deserialize, Serialize, - de::Visitor, - ser::{SerializeMap, SerializeStruct}, -}; +use serde::{Deserialize, Serialize, ser::SerializeMap}; use smallvec::SmallVec; -use thiserror::Error as ThisError; -use tlo::parse_tlo; use crate::{ - format::{Decimal, F40, F40_2, F40_3, Format, PCT40_1, Settings as FormatSettings}, - output::pivot::value::{ + format::{Format, Settings as FormatSettings, F40, F40_2, F40_3, PCT40_1}, + output::pivot::{look::{Look, Sizing}, value::{ BareValue, DisplayValue, IntoValueOptions, NumberValue, ValueInner, ValueOptions, - }, + }}, settings::{Settings, Show}, - util::ToSmallString, - variable::{VarType, Variable}, + variable::Variable, }; pub(crate) use tlo::parse_bool; @@ -91,230 +78,7 @@ mod tlo; #[cfg(test)] pub mod tests; -/// Areas of a pivot table for styling purposes. -#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)] -pub enum Area { - /// Title. - Title, - - /// Caption. - Caption, - - /// Footnotes, - Footer, - - // Top-left corner. - Corner, - - /// Labels. - Labels( - /// - [Axis2::X]: Column labels, along the top of the table. - /// - [Axis2::Y]: Row labels, along the left side of the table. - Axis2, - ), - - /// Data cells. - Data( - /// This allows styling for even rows and odd rows to differ - /// arbitrarily, but the SPV file format only distinguishes foreground - /// and background colors, so any other differences will be lost upon - /// save. - RowParity, - ), - - /// Layer indication. - Layers, -} - -impl Default for Area { - fn default() -> Self { - Self::Data(RowParity::default()) - } -} - -impl Display for Area { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Area::Title => write!(f, "title"), - Area::Caption => write!(f, "caption"), - Area::Footer => write!(f, "footer"), - Area::Corner => write!(f, "corner"), - Area::Labels(axis2) => write!(f, "labels({axis2})"), - Area::Data(row) => write!(f, "data({row})"), - Area::Layers => write!(f, "layers"), - } - } -} - -impl Serialize for Area { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_small_string::<16>()) - } -} - -/// Distinguishes [Area::Data] for even-numbered and odd-numbered rows. -#[derive(Copy, Clone, Debug, Default, Enum, PartialEq, Eq)] -pub enum RowParity { - /// Even-numbered rows. - /// - /// The first row is row 0, hence even. - #[default] - Even, - /// Odd-numbered rows. - Odd, -} - -impl From for RowParity { - fn from(value: usize) -> Self { - if value % 2 == 1 { - Self::Odd - } else { - Self::Even - } - } -} - -impl Display for RowParity { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - RowParity::Even => write!(f, "even"), - RowParity::Odd => write!(f, "odd"), - } - } -} - -/// Table borders for styling purposes. -#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)] -pub enum Border { - Title, - OuterFrame(BoxBorder), - InnerFrame(BoxBorder), - Dimension(RowColBorder), - Category(RowColBorder), - DataLeft, - DataTop, -} - -impl Border { - pub fn default_stroke(self) -> Stroke { - match self { - Self::InnerFrame(_) | Self::DataLeft | Self::DataTop => Stroke::Thick, - Self::Dimension( - RowColBorder(HeadingRegion::Columns, _) | RowColBorder(_, Axis2::X), - ) - | Self::Category(RowColBorder(HeadingRegion::Columns, _)) => Stroke::Solid, - _ => Stroke::None, - } - } - pub fn default_border_style(self) -> BorderStyle { - BorderStyle { - stroke: self.default_stroke(), - color: Color::BLACK, - } - } - - fn fallback(self) -> Self { - match self { - Self::Title - | Self::OuterFrame(_) - | Self::InnerFrame(_) - | Self::DataLeft - | Self::DataTop - | Self::Category(_) => self, - Self::Dimension(row_col_border) => Self::Category(row_col_border), - } - } - - pub fn default_borders() -> EnumMap { - EnumMap::from_fn(Border::default_border_style) - } -} - -impl Display for Border { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Border::Title => write!(f, "title"), - Border::OuterFrame(box_border) => write!(f, "outer_frame({box_border})"), - Border::InnerFrame(box_border) => write!(f, "inner_frame({box_border})"), - Border::Dimension(row_col_border) => write!(f, "dimension({row_col_border})"), - Border::Category(row_col_border) => write!(f, "category({row_col_border})"), - Border::DataLeft => write!(f, "data(left)"), - Border::DataTop => write!(f, "data(top)"), - } - } -} - -impl Serialize for Border { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_small_string::<32>()) - } -} - -/// The borders on a box. -#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum BoxBorder { - Left, - Top, - Right, - Bottom, -} - -impl BoxBorder { - fn as_str(&self) -> &'static str { - match self { - BoxBorder::Left => "left", - BoxBorder::Top => "top", - BoxBorder::Right => "right", - BoxBorder::Bottom => "bottom", - } - } -} - -impl Display for BoxBorder { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.as_str()) - } -} - -/// Borders between rows and columns. -#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub struct RowColBorder( - /// Row or column headings. - pub HeadingRegion, - /// Horizontal ([Axis2::X]) or vertical ([Axis2::Y]) borders. - pub Axis2, -); - -impl Display for RowColBorder { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}", self.0, self.1) - } -} - -/// Sizing for rows or columns of a rendered table. -/// -/// The comments below talk about columns and their widths but they apply -/// equally to rows and their heights. -#[derive(Default, Clone, Debug, Serialize)] -pub struct Sizing { - /// Specific column widths, in 1/96" units. - pub widths: Vec, - - /// Specific page breaks: 0-based columns after which a page break must - /// occur, e.g. a value of 1 requests a break after the second column. - pub breaks: Vec, - - /// Keeps: columns to keep together on a page if possible. - pub keeps: Vec>, -} +pub mod look; /// A 3-dimensional axis. #[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Sequence, Serialize)] @@ -939,626 +703,6 @@ impl From<&String> for Category { } } -/// Styling for a pivot table. -/// -/// The division between this and the style information in [PivotTable] seems -/// fairly arbitrary. The ultimate reason for the division is simply because -/// that's how SPSS documentation and file formats do it. -#[derive(Clone, Debug, PartialEq, Serialize)] -pub struct Look { - pub name: Option, - - /// Whether to hide rows or columns whose cells are all empty. - pub hide_empty: bool, - - pub row_label_position: LabelPosition, - - /// Ranges of column widths in the two heading regions, in 1/96" units. - pub heading_widths: EnumMap>, - - /// Kind of markers to use for footnotes. - pub footnote_marker_type: FootnoteMarkerType, - - /// Where to put the footnote markers. - pub footnote_marker_position: FootnoteMarkerPosition, - - /// Styles for areas of the pivot table. - pub areas: EnumMap, - - /// Styles for borders in the pivot table. - pub borders: EnumMap, - - pub print_all_layers: bool, - - pub paginate_layers: bool, - - pub shrink_to_fit: EnumMap, - - pub top_continuation: bool, - - pub bottom_continuation: bool, - - pub continuation: Option, - - pub n_orphan_lines: usize, -} - -impl Look { - pub fn with_omit_empty(mut self, omit_empty: bool) -> Self { - self.hide_empty = omit_empty; - self - } - pub fn with_row_label_position(mut self, row_label_position: LabelPosition) -> Self { - self.row_label_position = row_label_position; - self - } - pub fn with_borders(mut self, borders: EnumMap) -> Self { - self.borders = borders; - self - } -} - -impl Default for Look { - fn default() -> Self { - Self { - name: None, - hide_empty: true, - row_label_position: LabelPosition::default(), - heading_widths: EnumMap::from_fn(|region| match region { - HeadingRegion::Rows => 36..=72, - HeadingRegion::Columns => 36..=120, - }), - footnote_marker_type: FootnoteMarkerType::default(), - footnote_marker_position: FootnoteMarkerPosition::default(), - areas: EnumMap::from_fn(AreaStyle::default_for_area), - borders: Border::default_borders(), - print_all_layers: false, - paginate_layers: false, - shrink_to_fit: EnumMap::from_fn(|_| false), - top_continuation: false, - bottom_continuation: false, - continuation: None, - n_orphan_lines: 0, - } - } -} - -#[derive(ThisError, Debug)] -pub enum ParseLookError { - #[error(transparent)] - XmlError(#[from] DeError), - - #[error(transparent)] - Utf8Error(#[from] Utf8Error), - - #[error(transparent)] - BinError(#[from] BinError), - - #[error(transparent)] - IoError(#[from] std::io::Error), -} - -impl Look { - pub fn shared_default() -> Arc { - static LOOK: OnceLock> = OnceLock::new(); - LOOK.get_or_init(|| Arc::new(Look::default())).clone() - } - - pub fn from_xml(xml: &str) -> Result { - Ok(from_str::(xml) - .map_err(ParseLookError::from)? - .into()) - } - - pub fn from_binary(tlo: &[u8]) -> Result { - parse_tlo(tlo).map_err(ParseLookError::from) - } - - pub fn from_data(data: &[u8]) -> Result { - if data.starts_with(b"\xff\xff\0\0") { - Self::from_binary(data) - } else { - Self::from_xml(from_utf8(data).map_err(ParseLookError::from)?) - } - } - - pub fn from_reader(mut reader: R) -> Result - where - R: Read, - { - let mut buffer = Vec::new(); - reader - .read_to_end(&mut buffer) - .map_err(ParseLookError::from)?; - Self::from_data(&buffer) - } -} - -/// Position for group labels. -#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -pub enum LabelPosition { - /// Hierarchically enclosing the categories. - /// - /// For column labels, group labels appear above the categories. For row - /// labels, group labels appear to the left of the categories. - /// - /// ```text - /// ┌────┬──────────────┐ ┌─────────┬──────────┐ - /// │ │ nested │ │ │ columns │ - /// │ ├────┬────┬────┤ ├──────┬──┼──────────┤ - /// │ │ a1 │ a2 │ a3 │ │ │a1│...data...│ - /// ├────┼────┼────┼────┤ │nested│a2│...data...│ - /// │ │data│data│data│ │ │a3│...data...│ - /// │ │ . │ . │ . │ └──────┴──┴──────────┘ - /// │rows│ . │ . │ . │ - /// │ │ . │ . │ . │ - /// └────┴────┴────┴────┘ - /// ``` - #[serde(rename = "nested")] - Nested, - - /// In the corner (row labels only). - /// - /// ```text - /// ┌──────┬──────────┐ - /// │corner│ columns │ - /// ├──────┼──────────┤ - /// │ a1│...data...│ - /// │ a2│...data...│ - /// │ a3│...data...│ - /// └──────┴──────────┘ - /// ``` - #[default] - #[serde(rename = "inCorner")] - Corner, -} - -/// The heading region of a rendered pivot table: -/// -/// ```text -/// ┌──────────────────┬─────────────────────────────────────────────────┐ -/// │ │ column headings │ -/// │ ├─────────────────────────────────────────────────┤ -/// │ corner │ │ -/// │ and │ │ -/// │ row headings │ data │ -/// │ │ │ -/// │ │ │ -/// └──────────────────┴─────────────────────────────────────────────────┘ -/// ``` -#[derive(Copy, Clone, Debug, PartialEq, Eq, Enum, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum HeadingRegion { - Rows, - Columns, -} - -impl HeadingRegion { - pub fn as_str(&self) -> &'static str { - match self { - HeadingRegion::Rows => "rows", - HeadingRegion::Columns => "columns", - } - } -} - -impl Display for HeadingRegion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -impl From for HeadingRegion { - fn from(axis: Axis2) -> Self { - match axis { - Axis2::X => HeadingRegion::Columns, - Axis2::Y => HeadingRegion::Rows, - } - } -} - -#[derive(Clone, Debug, PartialEq, Serialize)] -pub struct AreaStyle { - pub cell_style: CellStyle, - pub font_style: FontStyle, -} - -impl AreaStyle { - pub fn default_for_area(area: Area) -> Self { - Self { - cell_style: CellStyle::default_for_area(area), - font_style: FontStyle::default_for_area(area), - } - } -} - -#[derive(Clone, Debug, Serialize, PartialEq)] -pub struct CellStyle { - /// `None` means "mixed" alignment: align strings to the left, numbers to - /// the right. - pub horz_align: Option, - pub vert_align: VertAlign, - - /// Margins in 1/96" units. - /// - /// `margins[Axis2::X][0]` is the left margin. - /// `margins[Axis2::X][1]` is the right margin. - /// `margins[Axis2::Y][0]` is the top margin. - /// `margins[Axis2::Y][1]` is the bottom margin. - pub margins: EnumMap, -} - -impl Default for CellStyle { - fn default() -> Self { - Self::default_for_area(Area::default()) - } -} - -impl CellStyle { - pub fn default_for_area(area: Area) -> Self { - use HorzAlign::*; - use VertAlign::*; - let (horz_align, vert_align, hmargins, vmargins) = match area { - Area::Title => (Some(Center), Middle, [8, 11], [1, 8]), - Area::Caption => (Some(Left), Top, [8, 11], [1, 1]), - Area::Footer => (Some(Left), Top, [11, 8], [2, 3]), - Area::Corner => (Some(Left), Bottom, [8, 11], [1, 1]), - Area::Labels(Axis2::X) => (Some(Center), Bottom, [8, 11], [1, 3]), - Area::Labels(Axis2::Y) => (Some(Left), Top, [8, 11], [1, 3]), - Area::Data(_) => (None, Top, [8, 11], [1, 1]), - Area::Layers => (Some(Left), Bottom, [8, 11], [1, 3]), - }; - Self { - horz_align, - vert_align, - margins: enum_map! { Axis2::X => hmargins, Axis2::Y => vmargins }, - } - } - pub fn with_horz_align(self, horz_align: Option) -> Self { - Self { horz_align, ..self } - } - pub fn with_vert_align(self, vert_align: VertAlign) -> Self { - Self { vert_align, ..self } - } - pub fn with_margins(self, margins: EnumMap) -> Self { - Self { margins, ..self } - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum HorzAlign { - /// Right aligned. - Right, - - /// Left aligned. - Left, - - /// Centered. - Center, - - /// Align the decimal point at the specified position. - Decimal { - /// Decimal offset from the right side of the cell, in 1/96" units. - offset: f64, - - /// Decimal character. - decimal: Decimal, - }, -} - -impl HorzAlign { - pub fn for_mixed(var_type: VarType) -> Self { - match var_type { - VarType::Numeric => Self::Right, - VarType::String => Self::Left, - } - } - - pub fn as_str(&self) -> Option<&'static str> { - match self { - HorzAlign::Right => Some("right"), - HorzAlign::Left => Some("left"), - HorzAlign::Center => Some("center"), - HorzAlign::Decimal { .. } => None, - } - } -} - -/// Unknown horizontal alignment. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct UnknownHorzAlign; - -impl FromStr for HorzAlign { - type Err = UnknownHorzAlign; - - fn from_str(s: &str) -> Result { - if s.eq_ignore_ascii_case("left") { - Ok(Self::Left) - } else if s.eq_ignore_ascii_case("center") { - Ok(Self::Center) - } else if s.eq_ignore_ascii_case("right") { - Ok(Self::Right) - } else { - Err(UnknownHorzAlign) - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum VertAlign { - /// Top alignment. - Top, - - /// Centered, - Middle, - - /// Bottom alignment. - Bottom, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize)] -pub struct FontStyle { - pub bold: bool, - pub italic: bool, - pub underline: bool, - pub font: String, - pub fg: Color, - pub bg: Color, - - /// In 1/72" units. - pub size: i32, -} - -impl Default for FontStyle { - fn default() -> Self { - FontStyle { - bold: false, - italic: false, - underline: false, - font: String::from("Sans Serif"), - fg: Color::BLACK, - bg: Color::WHITE, - size: 9, - } - } -} - -impl FontStyle { - pub fn default_for_area(area: Area) -> Self { - Self::default().with_bold(area == Area::Title) - } - pub fn with_size(self, size: i32) -> Self { - Self { size, ..self } - } - pub fn with_bold(self, bold: bool) -> Self { - Self { bold, ..self } - } - pub fn with_italic(self, italic: bool) -> Self { - Self { italic, ..self } - } - pub fn with_underline(self, underline: bool) -> Self { - Self { underline, ..self } - } - pub fn with_font(self, font: impl Into) -> Self { - Self { - font: font.into(), - ..self - } - } - pub fn with_fg(self, fg: Color) -> Self { - Self { fg, ..self } - } - pub fn with_bg(self, fg: Color) -> Self { - Self { fg, ..self } - } -} - -#[derive(Copy, Clone, PartialEq, Eq)] -pub struct Color { - pub alpha: u8, - pub r: u8, - pub g: u8, - pub b: u8, -} - -impl Color { - pub const BLACK: Color = Color::new(0, 0, 0); - pub const WHITE: Color = Color::new(255, 255, 255); - pub const RED: Color = Color::new(255, 0, 0); - pub const BLUE: Color = Color::new(0, 0, 255); - pub const TRANSPARENT: Color = Color::new(0, 0, 0).with_alpha(0); - - pub const fn new(r: u8, g: u8, b: u8) -> Self { - Self { - alpha: 255, - r, - g, - b, - } - } - - pub const fn with_alpha(self, alpha: u8) -> Self { - Self { alpha, ..self } - } - - pub const fn without_alpha(self) -> Self { - self.with_alpha(255) - } - - /// Displays opaque colors as `#rrggbb` and others as `rgb(r, g, b, alpha)`. - pub fn display_css(&self) -> DisplayCss { - DisplayCss(*self) - } - - pub fn into_rgb(&self) -> (u8, u8, u8) { - (self.r, self.g, self.b) - } - - pub fn into_rgb16(&self) -> (u16, u16, u16) { - ( - self.r as u16 * 257, - self.g as u16 * 257, - self.b as u16 * 257, - ) - } -} - -impl Debug for Color { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.display_css()) - } -} - -impl From for Color { - fn from(Rgba8 { r, g, b, a }: Rgba8) -> Self { - Self::new(r, g, b).with_alpha(a) - } -} - -impl FromStr for Color { - type Err = ParseColorError; - - fn from_str(s: &str) -> Result { - fn is_bare_hex(s: &str) -> bool { - let s = s.trim(); - s.chars().count() == 6 && s.chars().all(|c| c.is_ascii_hexdigit()) - } - let color: AlphaColor = match s.parse() { - Err(_) if is_bare_hex(s) => ("#".to_owned() + s).parse(), - Err(_) if s.trim().eq_ignore_ascii_case("transparent") => Ok(TRANSPARENT), - other => other, - }?; - Ok(color.to_rgba8().into()) - } -} - -impl Serialize for Color { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.display_css().to_small_string::<32>()) - } -} - -impl<'de> Deserialize<'de> for Color { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct ColorVisitor; - - impl<'de> Visitor<'de> for ColorVisitor { - type Value = Color; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("\"#rrggbb\" or \"rrggbb\" or web color name") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - v.parse().map_err(E::custom) - } - } - - deserializer.deserialize_str(ColorVisitor) - } -} - -pub struct DisplayCss(Color); - -impl Display for DisplayCss { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Color { alpha, r, g, b } = self.0; - match alpha { - 255 => write!(f, "#{r:02x}{g:02x}{b:02x}"), - _ => write!(f, "rgb({r}, {g}, {b}, {:.2})", alpha as f64 / 255.0), - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Deserialize)] -pub struct BorderStyle { - #[serde(rename = "@borderStyleType")] - pub stroke: Stroke, - - #[serde(rename = "@color")] - pub color: Color, -} - -impl Serialize for BorderStyle { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut s = serializer.serialize_struct("BorderStyle", 2)?; - s.serialize_field("stroke", &self.stroke)?; - s.serialize_field("color", &self.color)?; - s.end() - } -} - -impl From for BorderStyle { - fn from(value: Stroke) -> Self { - Self::new(value) - } -} - -impl BorderStyle { - pub const fn new(stroke: Stroke) -> Self { - Self { - stroke, - color: Color::BLACK, - } - } - - pub const fn none() -> Self { - Self::new(Stroke::None) - } - - pub fn is_none(&self) -> bool { - self.stroke.is_none() - } - - /// Returns a border style that "combines" the two arguments, that is, that - /// gives a reasonable choice for a rule for different reasons should have - /// both styles. - pub fn combine(self, other: BorderStyle) -> Self { - Self { - stroke: self.stroke.combine(other.stroke), - color: self.color, - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Enum, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum Stroke { - None, - Solid, - Dashed, - Thick, - Thin, - Double, -} - -impl Stroke { - pub fn is_none(&self) -> bool { - self == &Self::None - } - - /// Returns a stroke that "combines" the two arguments, that is, that gives - /// a reasonable stroke choice for a rule for different reasons should have - /// both styles. - pub fn combine(self, other: Stroke) -> Self { - self.max(other) - } -} - /// An axis of a 2-dimensional table. #[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -2331,7 +1475,7 @@ impl Display for Display26Adic { number /= 26; } output.reverse(); - write!(f, "{}", from_utf8(&output).unwrap()) + write!(f, "{}", str::from_utf8(&output).unwrap()) } } @@ -2431,7 +1575,8 @@ mod test { use std::str::FromStr; use crate::output::pivot::{ - Color, Display26Adic, MetadataEntry, MetadataValue, Value, + Display26Adic, MetadataEntry, MetadataValue, Value, + look::Color, tests::assert_rendering, value::{TemplateValue, ValueInner}, }; diff --git a/rust/pspp/src/output/pivot/look.rs b/rust/pspp/src/output/pivot/look.rs new file mode 100644 index 0000000000..583eb41dfb --- /dev/null +++ b/rust/pspp/src/output/pivot/look.rs @@ -0,0 +1,866 @@ +use std::{ + fmt::{Debug, Display}, + io::Read, + ops::{Range, RangeInclusive}, + str::{FromStr, Utf8Error}, + sync::{Arc, OnceLock}, +}; + +use color::{AlphaColor, Rgba8, Srgb, palette::css::TRANSPARENT}; +use enum_map::{Enum, EnumMap, enum_map}; +use quick_xml::{DeError, de::from_str}; +use serde::{Deserialize, Serialize, de::Visitor, ser::SerializeStruct}; + +use crate::{ + format::Decimal, + output::pivot::{ + Axis2, FootnoteMarkerPosition, FootnoteMarkerType, TableProperties, tlo::parse_tlo, + }, + util::ToSmallString, + variable::VarType, +}; + +/// Areas of a pivot table for styling purposes. +#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)] +pub enum Area { + /// Title. + Title, + + /// Caption. + Caption, + + /// Footnotes, + Footer, + + // Top-left corner. + Corner, + + /// Labels. + Labels( + /// - [Axis2::X]: Column labels, along the top of the table. + /// - [Axis2::Y]: Row labels, along the left side of the table. + Axis2, + ), + + /// Data cells. + Data( + /// This allows styling for even rows and odd rows to differ + /// arbitrarily, but the SPV file format only distinguishes foreground + /// and background colors, so any other differences will be lost upon + /// save. + RowParity, + ), + + /// Layer indication. + Layers, +} + +impl Default for Area { + fn default() -> Self { + Self::Data(RowParity::default()) + } +} + +impl Display for Area { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Area::Title => write!(f, "title"), + Area::Caption => write!(f, "caption"), + Area::Footer => write!(f, "footer"), + Area::Corner => write!(f, "corner"), + Area::Labels(axis2) => write!(f, "labels({axis2})"), + Area::Data(row) => write!(f, "data({row})"), + Area::Layers => write!(f, "layers"), + } + } +} + +impl Serialize for Area { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_small_string::<16>()) + } +} + +/// Distinguishes [Area::Data] for even-numbered and odd-numbered rows. +#[derive(Copy, Clone, Debug, Default, Enum, PartialEq, Eq)] +pub enum RowParity { + /// Even-numbered rows. + /// + /// The first row is row 0, hence even. + #[default] + Even, + /// Odd-numbered rows. + Odd, +} + +impl From for RowParity { + fn from(value: usize) -> Self { + if value % 2 == 1 { + Self::Odd + } else { + Self::Even + } + } +} + +impl Display for RowParity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RowParity::Even => write!(f, "even"), + RowParity::Odd => write!(f, "odd"), + } + } +} + +/// Table borders for styling purposes. +#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)] +pub enum Border { + Title, + OuterFrame(BoxBorder), + InnerFrame(BoxBorder), + Dimension(RowColBorder), + Category(RowColBorder), + DataLeft, + DataTop, +} + +impl Border { + pub fn default_stroke(self) -> Stroke { + match self { + Self::InnerFrame(_) | Self::DataLeft | Self::DataTop => Stroke::Thick, + Self::Dimension( + RowColBorder(HeadingRegion::Columns, _) | RowColBorder(_, Axis2::X), + ) + | Self::Category(RowColBorder(HeadingRegion::Columns, _)) => Stroke::Solid, + _ => Stroke::None, + } + } + pub fn default_border_style(self) -> BorderStyle { + BorderStyle { + stroke: self.default_stroke(), + color: Color::BLACK, + } + } + + pub fn fallback(self) -> Self { + match self { + Self::Title + | Self::OuterFrame(_) + | Self::InnerFrame(_) + | Self::DataLeft + | Self::DataTop + | Self::Category(_) => self, + Self::Dimension(row_col_border) => Self::Category(row_col_border), + } + } + + pub fn default_borders() -> EnumMap { + EnumMap::from_fn(Border::default_border_style) + } +} + +impl Display for Border { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Border::Title => write!(f, "title"), + Border::OuterFrame(box_border) => write!(f, "outer_frame({box_border})"), + Border::InnerFrame(box_border) => write!(f, "inner_frame({box_border})"), + Border::Dimension(row_col_border) => write!(f, "dimension({row_col_border})"), + Border::Category(row_col_border) => write!(f, "category({row_col_border})"), + Border::DataLeft => write!(f, "data(left)"), + Border::DataTop => write!(f, "data(top)"), + } + } +} + +impl Serialize for Border { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_small_string::<32>()) + } +} + +/// The borders on a box. +#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum BoxBorder { + Left, + Top, + Right, + Bottom, +} + +impl BoxBorder { + fn as_str(&self) -> &'static str { + match self { + BoxBorder::Left => "left", + BoxBorder::Top => "top", + BoxBorder::Right => "right", + BoxBorder::Bottom => "bottom", + } + } +} + +impl Display for BoxBorder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Borders between rows and columns. +#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct RowColBorder( + /// Row or column headings. + pub HeadingRegion, + /// Horizontal ([Axis2::X]) or vertical ([Axis2::Y]) borders. + pub Axis2, +); + +impl Display for RowColBorder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.0, self.1) + } +} + +/// Sizing for rows or columns of a rendered table. +/// +/// The comments below talk about columns and their widths but they apply +/// equally to rows and their heights. +#[derive(Default, Clone, Debug, Serialize)] +pub struct Sizing { + /// Specific column widths, in 1/96" units. + pub widths: Vec, + + /// Specific page breaks: 0-based columns after which a page break must + /// occur, e.g. a value of 1 requests a break after the second column. + pub breaks: Vec, + + /// Keeps: columns to keep together on a page if possible. + pub keeps: Vec>, +} + +/// Styling for a pivot table. +/// +/// The division between this and the style information in [PivotTable] seems +/// fairly arbitrary. The ultimate reason for the division is simply because +/// that's how SPSS documentation and file formats do it. +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct Look { + pub name: Option, + + /// Whether to hide rows or columns whose cells are all empty. + pub hide_empty: bool, + + pub row_label_position: LabelPosition, + + /// Ranges of column widths in the two heading regions, in 1/96" units. + pub heading_widths: EnumMap>, + + /// Kind of markers to use for footnotes. + pub footnote_marker_type: FootnoteMarkerType, + + /// Where to put the footnote markers. + pub footnote_marker_position: FootnoteMarkerPosition, + + /// Styles for areas of the pivot table. + pub areas: EnumMap, + + /// Styles for borders in the pivot table. + pub borders: EnumMap, + + pub print_all_layers: bool, + + pub paginate_layers: bool, + + pub shrink_to_fit: EnumMap, + + pub top_continuation: bool, + + pub bottom_continuation: bool, + + pub continuation: Option, + + pub n_orphan_lines: usize, +} + +impl Look { + pub fn with_omit_empty(mut self, omit_empty: bool) -> Self { + self.hide_empty = omit_empty; + self + } + pub fn with_row_label_position(mut self, row_label_position: LabelPosition) -> Self { + self.row_label_position = row_label_position; + self + } + pub fn with_borders(mut self, borders: EnumMap) -> Self { + self.borders = borders; + self + } +} + +impl Default for Look { + fn default() -> Self { + Self { + name: None, + hide_empty: true, + row_label_position: LabelPosition::default(), + heading_widths: EnumMap::from_fn(|region| match region { + HeadingRegion::Rows => 36..=72, + HeadingRegion::Columns => 36..=120, + }), + footnote_marker_type: FootnoteMarkerType::default(), + footnote_marker_position: FootnoteMarkerPosition::default(), + areas: EnumMap::from_fn(AreaStyle::default_for_area), + borders: Border::default_borders(), + print_all_layers: false, + paginate_layers: false, + shrink_to_fit: EnumMap::from_fn(|_| false), + top_continuation: false, + bottom_continuation: false, + continuation: None, + n_orphan_lines: 0, + } + } +} + +#[derive(thiserror::Error, Debug)] +pub enum ParseLookError { + #[error(transparent)] + XmlError(#[from] DeError), + + #[error(transparent)] + Utf8Error(#[from] Utf8Error), + + #[error(transparent)] + BinError(#[from] binrw::Error), + + #[error(transparent)] + IoError(#[from] std::io::Error), +} + +impl Look { + pub fn shared_default() -> Arc { + static LOOK: OnceLock> = OnceLock::new(); + LOOK.get_or_init(|| Arc::new(Look::default())).clone() + } + + pub fn from_xml(xml: &str) -> Result { + Ok(from_str::(xml) + .map_err(ParseLookError::from)? + .into()) + } + + pub fn from_binary(tlo: &[u8]) -> Result { + parse_tlo(tlo).map_err(ParseLookError::from) + } + + pub fn from_data(data: &[u8]) -> Result { + if data.starts_with(b"\xff\xff\0\0") { + Self::from_binary(data) + } else { + Self::from_xml(str::from_utf8(data).map_err(ParseLookError::from)?) + } + } + + pub fn from_reader(mut reader: R) -> Result + where + R: Read, + { + let mut buffer = Vec::new(); + reader + .read_to_end(&mut buffer) + .map_err(ParseLookError::from)?; + Self::from_data(&buffer) + } +} + +/// Position for group labels. +#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub enum LabelPosition { + /// Hierarchically enclosing the categories. + /// + /// For column labels, group labels appear above the categories. For row + /// labels, group labels appear to the left of the categories. + /// + /// ```text + /// ┌────┬──────────────┐ ┌─────────┬──────────┐ + /// │ │ nested │ │ │ columns │ + /// │ ├────┬────┬────┤ ├──────┬──┼──────────┤ + /// │ │ a1 │ a2 │ a3 │ │ │a1│...data...│ + /// ├────┼────┼────┼────┤ │nested│a2│...data...│ + /// │ │data│data│data│ │ │a3│...data...│ + /// │ │ . │ . │ . │ └──────┴──┴──────────┘ + /// │rows│ . │ . │ . │ + /// │ │ . │ . │ . │ + /// └────┴────┴────┴────┘ + /// ``` + #[serde(rename = "nested")] + Nested, + + /// In the corner (row labels only). + /// + /// ```text + /// ┌──────┬──────────┐ + /// │corner│ columns │ + /// ├──────┼──────────┤ + /// │ a1│...data...│ + /// │ a2│...data...│ + /// │ a3│...data...│ + /// └──────┴──────────┘ + /// ``` + #[default] + #[serde(rename = "inCorner")] + Corner, +} + +/// The heading region of a rendered pivot table: +/// +/// ```text +/// ┌──────────────────┬─────────────────────────────────────────────────┐ +/// │ │ column headings │ +/// │ ├─────────────────────────────────────────────────┤ +/// │ corner │ │ +/// │ and │ │ +/// │ row headings │ data │ +/// │ │ │ +/// │ │ │ +/// └──────────────────┴─────────────────────────────────────────────────┘ +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, Eq, Enum, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum HeadingRegion { + Rows, + Columns, +} + +impl HeadingRegion { + pub fn as_str(&self) -> &'static str { + match self { + HeadingRegion::Rows => "rows", + HeadingRegion::Columns => "columns", + } + } +} + +impl Display for HeadingRegion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl From for HeadingRegion { + fn from(axis: Axis2) -> Self { + match axis { + Axis2::X => HeadingRegion::Columns, + Axis2::Y => HeadingRegion::Rows, + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct AreaStyle { + pub cell_style: CellStyle, + pub font_style: FontStyle, +} + +impl AreaStyle { + pub fn default_for_area(area: Area) -> Self { + Self { + cell_style: CellStyle::default_for_area(area), + font_style: FontStyle::default_for_area(area), + } + } +} + +#[derive(Clone, Debug, Serialize, PartialEq)] +pub struct CellStyle { + /// `None` means "mixed" alignment: align strings to the left, numbers to + /// the right. + pub horz_align: Option, + pub vert_align: VertAlign, + + /// Margins in 1/96" units. + /// + /// `margins[Axis2::X][0]` is the left margin. + /// `margins[Axis2::X][1]` is the right margin. + /// `margins[Axis2::Y][0]` is the top margin. + /// `margins[Axis2::Y][1]` is the bottom margin. + pub margins: EnumMap, +} + +impl Default for CellStyle { + fn default() -> Self { + Self::default_for_area(Area::default()) + } +} + +impl CellStyle { + pub fn default_for_area(area: Area) -> Self { + use HorzAlign::*; + use VertAlign::*; + let (horz_align, vert_align, hmargins, vmargins) = match area { + Area::Title => (Some(Center), Middle, [8, 11], [1, 8]), + Area::Caption => (Some(Left), Top, [8, 11], [1, 1]), + Area::Footer => (Some(Left), Top, [11, 8], [2, 3]), + Area::Corner => (Some(Left), Bottom, [8, 11], [1, 1]), + Area::Labels(Axis2::X) => (Some(Center), Bottom, [8, 11], [1, 3]), + Area::Labels(Axis2::Y) => (Some(Left), Top, [8, 11], [1, 3]), + Area::Data(_) => (None, Top, [8, 11], [1, 1]), + Area::Layers => (Some(Left), Bottom, [8, 11], [1, 3]), + }; + Self { + horz_align, + vert_align, + margins: enum_map! { Axis2::X => hmargins, Axis2::Y => vmargins }, + } + } + pub fn with_horz_align(self, horz_align: Option) -> Self { + Self { horz_align, ..self } + } + pub fn with_vert_align(self, vert_align: VertAlign) -> Self { + Self { vert_align, ..self } + } + pub fn with_margins(self, margins: EnumMap) -> Self { + Self { margins, ..self } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum HorzAlign { + /// Right aligned. + Right, + + /// Left aligned. + Left, + + /// Centered. + Center, + + /// Align the decimal point at the specified position. + Decimal { + /// Decimal offset from the right side of the cell, in 1/96" units. + offset: f64, + + /// Decimal character. + decimal: Decimal, + }, +} + +impl HorzAlign { + pub fn for_mixed(var_type: VarType) -> Self { + match var_type { + VarType::Numeric => Self::Right, + VarType::String => Self::Left, + } + } + + pub fn as_str(&self) -> Option<&'static str> { + match self { + HorzAlign::Right => Some("right"), + HorzAlign::Left => Some("left"), + HorzAlign::Center => Some("center"), + HorzAlign::Decimal { .. } => None, + } + } +} + +/// Unknown horizontal alignment. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct UnknownHorzAlign; + +impl FromStr for HorzAlign { + type Err = UnknownHorzAlign; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("left") { + Ok(Self::Left) + } else if s.eq_ignore_ascii_case("center") { + Ok(Self::Center) + } else if s.eq_ignore_ascii_case("right") { + Ok(Self::Right) + } else { + Err(UnknownHorzAlign) + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum VertAlign { + /// Top alignment. + Top, + + /// Centered, + Middle, + + /// Bottom alignment. + Bottom, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct FontStyle { + pub bold: bool, + pub italic: bool, + pub underline: bool, + pub font: String, + pub fg: Color, + pub bg: Color, + + /// In 1/72" units. + pub size: i32, +} + +impl Default for FontStyle { + fn default() -> Self { + FontStyle { + bold: false, + italic: false, + underline: false, + font: String::from("Sans Serif"), + fg: Color::BLACK, + bg: Color::WHITE, + size: 9, + } + } +} + +impl FontStyle { + pub fn default_for_area(area: Area) -> Self { + Self::default().with_bold(area == Area::Title) + } + pub fn with_size(self, size: i32) -> Self { + Self { size, ..self } + } + pub fn with_bold(self, bold: bool) -> Self { + Self { bold, ..self } + } + pub fn with_italic(self, italic: bool) -> Self { + Self { italic, ..self } + } + pub fn with_underline(self, underline: bool) -> Self { + Self { underline, ..self } + } + pub fn with_font(self, font: impl Into) -> Self { + Self { + font: font.into(), + ..self + } + } + pub fn with_fg(self, fg: Color) -> Self { + Self { fg, ..self } + } + pub fn with_bg(self, fg: Color) -> Self { + Self { fg, ..self } + } +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Color { + pub alpha: u8, + pub r: u8, + pub g: u8, + pub b: u8, +} + +impl Color { + pub const BLACK: Color = Color::new(0, 0, 0); + pub const WHITE: Color = Color::new(255, 255, 255); + pub const RED: Color = Color::new(255, 0, 0); + pub const BLUE: Color = Color::new(0, 0, 255); + pub const TRANSPARENT: Color = Color::new(0, 0, 0).with_alpha(0); + + pub const fn new(r: u8, g: u8, b: u8) -> Self { + Self { + alpha: 255, + r, + g, + b, + } + } + + pub const fn with_alpha(self, alpha: u8) -> Self { + Self { alpha, ..self } + } + + pub const fn without_alpha(self) -> Self { + self.with_alpha(255) + } + + /// Displays opaque colors as `#rrggbb` and others as `rgb(r, g, b, alpha)`. + pub fn display_css(&self) -> DisplayCss { + DisplayCss(*self) + } + + pub fn into_rgb(&self) -> (u8, u8, u8) { + (self.r, self.g, self.b) + } + + pub fn into_rgb16(&self) -> (u16, u16, u16) { + ( + self.r as u16 * 257, + self.g as u16 * 257, + self.b as u16 * 257, + ) + } +} + +impl Debug for Color { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.display_css()) + } +} + +impl From for Color { + fn from(Rgba8 { r, g, b, a }: Rgba8) -> Self { + Self::new(r, g, b).with_alpha(a) + } +} + +impl FromStr for Color { + type Err = color::ParseError; + + fn from_str(s: &str) -> Result { + fn is_bare_hex(s: &str) -> bool { + let s = s.trim(); + s.chars().count() == 6 && s.chars().all(|c| c.is_ascii_hexdigit()) + } + let color: AlphaColor = match s.parse() { + Err(_) if is_bare_hex(s) => ("#".to_owned() + s).parse(), + Err(_) if s.trim().eq_ignore_ascii_case("transparent") => Ok(TRANSPARENT), + other => other, + }?; + Ok(color.to_rgba8().into()) + } +} + +impl Serialize for Color { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.display_css().to_small_string::<32>()) + } +} + +impl<'de> Deserialize<'de> for Color { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct ColorVisitor; + + impl<'de> Visitor<'de> for ColorVisitor { + type Value = Color; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("\"#rrggbb\" or \"rrggbb\" or web color name") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + v.parse().map_err(E::custom) + } + } + + deserializer.deserialize_str(ColorVisitor) + } +} + +pub struct DisplayCss(Color); + +impl Display for DisplayCss { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Color { alpha, r, g, b } = self.0; + match alpha { + 255 => write!(f, "#{r:02x}{g:02x}{b:02x}"), + _ => write!(f, "rgb({r}, {g}, {b}, {:.2})", alpha as f64 / 255.0), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Deserialize)] +pub struct BorderStyle { + #[serde(rename = "@borderStyleType")] + pub stroke: Stroke, + + #[serde(rename = "@color")] + pub color: Color, +} + +impl Serialize for BorderStyle { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut s = serializer.serialize_struct("BorderStyle", 2)?; + s.serialize_field("stroke", &self.stroke)?; + s.serialize_field("color", &self.color)?; + s.end() + } +} + +impl From for BorderStyle { + fn from(value: Stroke) -> Self { + Self::new(value) + } +} + +impl BorderStyle { + pub const fn new(stroke: Stroke) -> Self { + Self { + stroke, + color: Color::BLACK, + } + } + + pub const fn none() -> Self { + Self::new(Stroke::None) + } + + pub fn is_none(&self) -> bool { + self.stroke.is_none() + } + + /// Returns a border style that "combines" the two arguments, that is, that + /// gives a reasonable choice for a rule for different reasons should have + /// both styles. + pub fn combine(self, other: BorderStyle) -> Self { + Self { + stroke: self.stroke.combine(other.stroke), + color: self.color, + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Enum, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum Stroke { + None, + Solid, + Dashed, + Thick, + Thin, + Double, +} + +impl Stroke { + pub fn is_none(&self) -> bool { + self == &Self::None + } + + /// Returns a stroke that "combines" the two arguments, that is, that gives + /// a reasonable stroke choice for a rule for different reasons should have + /// both styles. + pub fn combine(self, other: Stroke) -> Self { + self.max(other) + } +} diff --git a/rust/pspp/src/output/pivot/look_xml.rs b/rust/pspp/src/output/pivot/look_xml.rs index 1c3c64d031..1c7e90d3f0 100644 --- a/rust/pspp/src/output/pivot/look_xml.rs +++ b/rust/pspp/src/output/pivot/look_xml.rs @@ -22,9 +22,11 @@ use serde::{Deserialize, de::Visitor}; use crate::{ format::Decimal, output::pivot::{ - Area, AreaStyle, Axis2, Border, BorderStyle, BoxBorder, Color, FootnoteMarkerPosition, - FootnoteMarkerType, HeadingRegion, HorzAlign, LabelPosition, Look, RowColBorder, RowParity, - VertAlign, + Axis2, FootnoteMarkerPosition, FootnoteMarkerType, + look::{ + self, Area, AreaStyle, Border, BorderStyle, BoxBorder, Color, HeadingRegion, HorzAlign, + LabelPosition, Look, RowColBorder, RowParity, VertAlign, + }, }, }; use thiserror::Error as ThisError; @@ -205,7 +207,7 @@ struct CellStyle { impl CellStyle { fn as_area_style(&self, data_row: RowParity) -> AreaStyle { AreaStyle { - cell_style: super::CellStyle { + cell_style: look::CellStyle { horz_align: match self.text_alignment { TextAlignment::Left => Some(HorzAlign::Left), TextAlignment::Right => Some(HorzAlign::Right), @@ -226,7 +228,7 @@ impl CellStyle { Axis2::Y => [self.margin_top.as_px_i32(), self.margin_bottom.as_px_i32()], }, }, - font_style: super::FontStyle { + font_style: look::FontStyle { bold: self.font_weight == FontWeight::Bold, italic: self.font_style == FontStyle::Italic, underline: self.font_underline == FontUnderline::Underline, @@ -452,9 +454,12 @@ mod tests { use quick_xml::de::from_str; use crate::output::pivot::{ - Area, AreaStyle, Axis2, Border, BorderStyle, BoxBorder, CellStyle, Color, FontStyle, - FootnoteMarkerPosition, FootnoteMarkerType, HeadingRegion, HorzAlign, LabelPosition, Look, - RowColBorder, RowParity, Stroke, VertAlign, + Axis2, FootnoteMarkerPosition, FootnoteMarkerType, + look::{ + Area, AreaStyle, Border, BorderStyle, BoxBorder, CellStyle, Color, FontStyle, + HeadingRegion, HorzAlign, LabelPosition, Look, RowColBorder, RowParity, Stroke, + VertAlign, + }, look_xml::{Length, LengthParseError, TableProperties}, }; diff --git a/rust/pspp/src/output/pivot/output.rs b/rust/pspp/src/output/pivot/output.rs index 33bfc4c6b7..317da926e3 100644 --- a/rust/pspp/src/output/pivot/output.rs +++ b/rust/pspp/src/output/pivot/output.rs @@ -20,13 +20,17 @@ use enum_map::{EnumMap, enum_map}; use itertools::Itertools; use crate::output::{ - pivot::{HeadingRegion, LabelPosition, Path, RowParity}, + pivot::{ + Footnote, Path, + look::{HeadingRegion, LabelPosition, RowParity}, + }, table::{CellInner, CellPos, CellRect, Table}, }; -use super::{ - Area, Axis2, Axis3, Border, BorderStyle, BoxBorder, Color, Dimension, Footnote, PivotTable, - RowColBorder, Stroke, Value, value::IntoValueOptions, +use crate::output::pivot::{ + Axis2, Axis3, Dimension, PivotTable, Value, + look::{Area, Border, BorderStyle, BoxBorder, Color, RowColBorder, Stroke}, + value::IntoValueOptions, }; /// All of the combinations of dimensions along an axis. diff --git a/rust/pspp/src/output/pivot/tests.rs b/rust/pspp/src/output/pivot/tests.rs index 23df42aca4..8149b4fd43 100644 --- a/rust/pspp/src/output/pivot/tests.rs +++ b/rust/pspp/src/output/pivot/tests.rs @@ -27,9 +27,12 @@ use crate::output::{ spv::SpvDriver, }, pivot::{ - Area, Axis2, Border, BorderStyle, Class, Color, Dimension, Footnote, - FootnoteMarkerPosition, FootnoteMarkerType, Footnotes, Group, HeadingRegion, LabelPosition, - Look, PivotTable, RowColBorder, Stroke, + Axis2, Class, Dimension, Footnote, FootnoteMarkerPosition, FootnoteMarkerType, Footnotes, + Group, PivotTable, + look::{ + Area, Border, BorderStyle, Color, HeadingRegion, HorzAlign, LabelPosition, Look, + RowColBorder, Stroke, + }, }, }; @@ -77,7 +80,7 @@ fn d1_r() { fn test_look() -> Look { let mut look = Look::default(); - look.areas[Area::Title].cell_style.horz_align = Some(super::HorzAlign::Left); + look.areas[Area::Title].cell_style.horz_align = Some(HorzAlign::Left); look.areas[Area::Title].font_style.bold = false; look } diff --git a/rust/pspp/src/output/pivot/tlo.rs b/rust/pspp/src/output/pivot/tlo.rs index 39c6c3240d..c1329afe18 100644 --- a/rust/pspp/src/output/pivot/tlo.rs +++ b/rust/pspp/src/output/pivot/tlo.rs @@ -19,12 +19,12 @@ use std::{fmt::Debug, io::Cursor}; use crate::{ format::Decimal, output::pivot::{ - Axis2, Border, BoxBorder, FootnoteMarkerPosition, FootnoteMarkerType, HeadingRegion, - LabelPosition, RowColBorder, + Axis2, FootnoteMarkerPosition, FootnoteMarkerType, + look::{self, Border, BoxBorder, HeadingRegion, LabelPosition, RowColBorder}, }, }; -use super::{Area, BorderStyle, Color, HorzAlign, Look, Stroke, VertAlign}; +use crate::output::pivot::look::{Area, BorderStyle, Color, HorzAlign, Look, Stroke, VertAlign}; use binrw::{BinRead, BinResult, Error as BinError, binread}; use enum_map::enum_map; @@ -97,7 +97,7 @@ impl From for Look { FootnoteMarkerPosition::Superscript }, areas: enum_map! { - Area::Title => super::AreaStyle::from_tlo(look.pv_cell_style.title_color, &look.pv_text_style.title_style), + Area::Title => look::AreaStyle::from_tlo(look.pv_cell_style.title_color, &look.pv_text_style.title_style), Area::Caption => (&look.pv_text_style.caption).into(), Area::Footer => (&look.pv_text_style.footer).into(), Area::Corner => (&look.pv_text_style.corner).into(), @@ -332,16 +332,16 @@ struct MostAreas { style: AreaStyle, } -impl From<&MostAreas> for super::AreaStyle { +impl From<&MostAreas> for look::AreaStyle { fn from(area: &MostAreas) -> Self { Self::from_tlo(area.color, &area.style) } } -impl super::AreaStyle { +impl look::AreaStyle { fn from_tlo(bg: Color, style: &AreaStyle) -> Self { Self { - cell_style: super::CellStyle { + cell_style: look::CellStyle { horz_align: match style.halign { 0 => Some(HorzAlign::Left), 1 => Some(HorzAlign::Right), @@ -367,7 +367,7 @@ impl super::AreaStyle { } }, }, - font_style: super::FontStyle { + font_style: look::FontStyle { bold: style.weight > 400, italic: style.italic, underline: style.underline, diff --git a/rust/pspp/src/output/pivot/value.rs b/rust/pspp/src/output/pivot/value.rs index 6d1a87cba8..25391f0e1b 100644 --- a/rust/pspp/src/output/pivot/value.rs +++ b/rust/pspp/src/output/pivot/value.rs @@ -15,7 +15,8 @@ use crate::{ data::{Datum, EncodedString}, format::{DATETIME40_0, F8_2, F40, Format, Type, UncheckedFormat}, output::pivot::{ - CellStyle, DisplayMarker, FontStyle, Footnote, FootnoteMarkerType, PivotTable, + DisplayMarker, Footnote, FootnoteMarkerType, PivotTable, + look::{CellStyle, FontStyle}, }, settings::{Settings, Show}, spv::html::Markup, diff --git a/rust/pspp/src/output/render.rs b/rust/pspp/src/output/render.rs index c1e6e84656..bdb30d6138 100644 --- a/rust/pspp/src/output/render.rs +++ b/rust/pspp/src/output/render.rs @@ -24,11 +24,13 @@ use itertools::{Itertools, interleave}; use num::Integer; use smallvec::SmallVec; -use crate::output::pivot::VertAlign; -use crate::output::table::{CellPos, CellRect, DrawCell}; - -use super::pivot::{Axis2, BorderStyle, Coord2, Look, PivotTable, Rect2, Stroke}; -use super::table::{Content, Table}; +use crate::output::{ + pivot::{ + Axis2, Coord2, PivotTable, Rect2, + look::{BorderStyle, Look, Stroke, VertAlign}, + }, + table::{CellPos, CellRect, Content, DrawCell, Table}, +}; /// Parameters for rendering a table_item to a device. /// diff --git a/rust/pspp/src/output/table.rs b/rust/pspp/src/output/table.rs index 0dd010931a..eb9b7d04bb 100644 --- a/rust/pspp/src/output/table.rs +++ b/rust/pspp/src/output/table.rs @@ -37,16 +37,15 @@ use ndarray::{Array, Array2}; use crate::{ output::pivot::{ - CellStyle, FontStyle, Footnote, HorzAlign, - value::{DisplayValue, ValueInner}, + Axis2, Footnote, Value, + look::{ + Area, AreaStyle, Border, BorderStyle, CellStyle, FontStyle, HeadingRegion, HorzAlign, + }, + value::{DisplayValue, ValueInner, ValueOptions}, }, spv::html, }; -use super::pivot::{ - Area, AreaStyle, Axis2, Border, BorderStyle, HeadingRegion, Value, value::ValueOptions, -}; - /// The `(x,y)` position of a cell in a [Table]. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)] pub struct CellPos { diff --git a/rust/pspp/src/settings.rs b/rust/pspp/src/settings.rs index 5bb412c311..978c3c8ff6 100644 --- a/rust/pspp/src/settings.rs +++ b/rust/pspp/src/settings.rs @@ -23,7 +23,7 @@ use serde::Serialize; use crate::{ format::{F8_2, Format, Settings as FormatSettings}, message::Severity, - output::pivot::Look, + output::pivot::look::Look, }; /// Whether to show variable or value labels or the underlying value or variable diff --git a/rust/pspp/src/spv/read.rs b/rust/pspp/src/spv/read.rs index 8b0c2dca97..fc2b0fff03 100644 --- a/rust/pspp/src/spv/read.rs +++ b/rust/pspp/src/spv/read.rs @@ -31,9 +31,8 @@ use zip::{ZipArchive, result::ZipError}; use crate::{ crypto::EncryptedFile, output::{ - Details, Item, SpvInfo, SpvMembers, Text, - page::{self}, - pivot::{Axis2, Length, Look, TableProperties, Value}, + Details, Item, SpvInfo, SpvMembers, Text, page, + pivot::{Axis2, Length, TableProperties, Value, look::Look}, }, spv::read::{ html::Document, diff --git a/rust/pspp/src/spv/read/css.rs b/rust/pspp/src/spv/read/css.rs index 5733b2869e..76d14190d1 100644 --- a/rust/pspp/src/spv/read/css.rs +++ b/rust/pspp/src/spv/read/css.rs @@ -8,7 +8,7 @@ use std::{ use itertools::Itertools; use crate::{ - output::pivot::{FontStyle, HorzAlign}, + output::pivot::look::{FontStyle, HorzAlign}, spv::read::html::Style, }; @@ -256,7 +256,7 @@ mod tests { use std::borrow::Cow; use crate::{ - output::pivot::{Color, FontStyle, HorzAlign}, + output::pivot::look::{Color, FontStyle, HorzAlign}, spv::read::css::{Lexer, Token}, }; diff --git a/rust/pspp/src/spv/read/html.rs b/rust/pspp/src/spv/read/html.rs index ec896e64f1..39a01351d0 100644 --- a/rust/pspp/src/spv/read/html.rs +++ b/rust/pspp/src/spv/read/html.rs @@ -39,7 +39,10 @@ use quick_xml::{ }; use serde::{Deserialize, Deserializer, Serialize, ser::SerializeMap}; -use crate::output::pivot::{CellStyle, Color, FontStyle, HorzAlign, Value}; +use crate::output::pivot::{ + Value, + look::{CellStyle, Color, FontStyle, HorzAlign}, +}; fn lowercase<'a>(s: &'a str) -> Cow<'a, str> { if s.chars().any(|c| c.is_ascii_uppercase()) { diff --git a/rust/pspp/src/spv/read/legacy_xml.rs b/rust/pspp/src/spv/read/legacy_xml.rs index baaf0d8a6a..f926961d95 100644 --- a/rust/pspp/src/spv/read/legacy_xml.rs +++ b/rust/pspp/src/spv/read/legacy_xml.rs @@ -35,9 +35,12 @@ use crate::{ data::Datum, format::{self, Decimal::Dot, F8_0, F40_2, Type, UncheckedFormat}, output::pivot::{ - self, Area, AreaStyle, Axis2, Axis3, Category, CategoryLocator, CellStyle, Color, - Dimension, Group, HeadingRegion, HorzAlign, Leaf, Length, Look, PivotTable, RowParity, - Value, VertAlign, + self, Axis2, Axis3, Category, CategoryLocator, Dimension, Group, Leaf, Length, PivotTable, + Value, + look::{ + self, Area, AreaStyle, CellStyle, Color, HeadingRegion, HorzAlign, Look, RowParity, + VertAlign, + }, value::{NumberValue, ValueInner}, }, spv::read::legacy_bin::DataValue, @@ -1912,7 +1915,7 @@ impl Style { fg: Option<&Style>, bg: Option<&Style>, cell_style: &mut CellStyle, - font_style: &mut pivot::FontStyle, + font_style: &mut look::FontStyle, ) { if let Some(fg) = fg { if let Some(weight) = fg.font_weight { diff --git a/rust/pspp/src/spv/read/light.rs b/rust/pspp/src/spv/read/light.rs index 45b4c37a69..39cdbe031f 100644 --- a/rust/pspp/src/spv/read/light.rs +++ b/rust/pspp/src/spv/read/light.rs @@ -21,10 +21,13 @@ use crate::{ Width, }, output::pivot::{ - self, AreaStyle, Axis2, Axis3, BoxBorder, Color, FootnoteMarkerPosition, - FootnoteMarkerType, Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Look, - PivotTable, PivotTableMetadata, PivotTableStyle, PrecomputedIndex, RowColBorder, RowParity, - Stroke, VertAlign, parse_bool, + self, Axis2, Axis3, FootnoteMarkerPosition, FootnoteMarkerType, Footnotes, Group, + PivotTable, PivotTableMetadata, PivotTableStyle, PrecomputedIndex, + look::{ + self, AreaStyle, BoxBorder, Color, HeadingRegion, HorzAlign, LabelPosition, Look, + RowColBorder, RowParity, Stroke, VertAlign, + }, + parse_bool, value::{StringValue, TemplateValue, ValueStyle, VariableValue}, }, settings::Show, @@ -344,20 +347,20 @@ struct Areas { } impl Areas { - fn decode(&self, encoding: &'static Encoding) -> EnumMap { + fn decode(&self, encoding: &'static Encoding) -> EnumMap { EnumMap::from_fn(|area| { let index = match area { - pivot::Area::Title => 0, - pivot::Area::Caption => 1, - pivot::Area::Footer => 2, - pivot::Area::Corner => 3, - pivot::Area::Labels(Axis2::X) => 4, - pivot::Area::Labels(Axis2::Y) => 5, - pivot::Area::Data(_) => 6, - pivot::Area::Layers => 7, + look::Area::Title => 0, + look::Area::Caption => 1, + look::Area::Footer => 2, + look::Area::Corner => 3, + look::Area::Labels(Axis2::X) => 4, + look::Area::Labels(Axis2::Y) => 5, + look::Area::Data(_) => 6, + look::Area::Layers => 7, }; let data_row = match area { - pivot::Area::Data(row) => row, + look::Area::Data(row) => row, _ => RowParity::default(), }; self.areas[index].decode(encoding, data_row) @@ -411,7 +414,7 @@ struct Area { impl Area { fn decode(&self, encoding: &'static Encoding, data_row: RowParity) -> AreaStyle { AreaStyle { - cell_style: pivot::CellStyle { + cell_style: look::CellStyle { horz_align: match self.halign { 0 => Some(HorzAlign::Center), 2 => Some(HorzAlign::Left), @@ -428,7 +431,7 @@ impl Area { Axis2::Y => [self.margins.top_margin, self.margins.bottom_margin] }, }, - font_style: pivot::FontStyle { + font_style: look::FontStyle { bold: (self.style & 1) != 0, italic: (self.style & 2) != 0, underline: self.underline, @@ -474,8 +477,8 @@ struct Borders { } impl Borders { - fn decode(&self) -> EnumMap { - let mut borders = pivot::Border::default_borders(); + fn decode(&self) -> EnumMap { + let mut borders = look::Border::default_borders(); for border in &self.borders { if let Some((border, style)) = border.decode() { borders[border] = style; @@ -498,27 +501,27 @@ struct Border { } impl Border { - fn decode(&self) -> Option<(pivot::Border, pivot::BorderStyle)> { + fn decode(&self) -> Option<(look::Border, look::BorderStyle)> { let border = match self.index { - 0 => pivot::Border::Title, - 1 => pivot::Border::OuterFrame(BoxBorder::Left), - 2 => pivot::Border::OuterFrame(BoxBorder::Top), - 3 => pivot::Border::OuterFrame(BoxBorder::Right), - 4 => pivot::Border::OuterFrame(BoxBorder::Bottom), - 5 => pivot::Border::InnerFrame(BoxBorder::Left), - 6 => pivot::Border::InnerFrame(BoxBorder::Top), - 7 => pivot::Border::InnerFrame(BoxBorder::Right), - 8 => pivot::Border::InnerFrame(BoxBorder::Bottom), - 9 => pivot::Border::DataLeft, - 10 => pivot::Border::DataLeft, - 11 => pivot::Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X)), - 12 => pivot::Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X)), - 13 => pivot::Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)), - 14 => pivot::Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)), - 15 => pivot::Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X)), - 16 => pivot::Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X)), - 17 => pivot::Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)), - 18 => pivot::Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)), + 0 => look::Border::Title, + 1 => look::Border::OuterFrame(BoxBorder::Left), + 2 => look::Border::OuterFrame(BoxBorder::Top), + 3 => look::Border::OuterFrame(BoxBorder::Right), + 4 => look::Border::OuterFrame(BoxBorder::Bottom), + 5 => look::Border::InnerFrame(BoxBorder::Left), + 6 => look::Border::InnerFrame(BoxBorder::Top), + 7 => look::Border::InnerFrame(BoxBorder::Right), + 8 => look::Border::InnerFrame(BoxBorder::Bottom), + 9 => look::Border::DataLeft, + 10 => look::Border::DataLeft, + 11 => look::Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X)), + 12 => look::Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X)), + 13 => look::Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)), + 14 => look::Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)), + 15 => look::Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X)), + 16 => look::Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X)), + 17 => look::Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)), + 18 => look::Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)), _ => return None, }; @@ -538,7 +541,7 @@ impl Border { ) .with_alpha((self.color >> 24) as u8); - Some((border, pivot::BorderStyle { stroke, color })) + Some((border, look::BorderStyle { stroke, color })) } } @@ -612,16 +615,16 @@ impl Sizing { &self, column_widths: &[i32], row_heights: &[i32], - ) -> EnumMap>> { + ) -> EnumMap>> { fn decode_axis( widths: &[i32], breaks: &[u32], keeps: &[(i32, i32)], - ) -> Option> { + ) -> Option> { if widths.is_empty() && breaks.is_empty() && keeps.is_empty() { None } else { - Some(Box::new(pivot::Sizing { + Some(Box::new(look::Sizing { widths: widths.into(), breaks: breaks.into_iter().map(|b| *b as usize).collect(), keeps: keeps @@ -1376,22 +1379,22 @@ struct ValueModsV3 { impl ValueMods { fn decode(&self, encoding: &'static Encoding, footnotes: &pivot::Footnotes) -> ValueStyle { - let font_style = - self.v3 - .style_pair - .font_style - .as_ref() - .map(|font_style| pivot::FontStyle { - bold: font_style.bold, - italic: font_style.italic, - underline: font_style.underline, - font: font_style.typeface.decode(encoding), - fg: font_style.fg, - bg: font_style.bg, - size: (font_style.size as i32) * 4 / 3, - }); + let font_style = self + .v3 + .style_pair + .font_style + .as_ref() + .map(|font_style| look::FontStyle { + bold: font_style.bold, + italic: font_style.italic, + underline: font_style.underline, + font: font_style.typeface.decode(encoding), + fg: font_style.fg, + bg: font_style.bg, + size: (font_style.size as i32) * 4 / 3, + }); let cell_style = self.v3.style_pair.cell_style.as_ref().map(|cell_style| { - pivot::CellStyle { + look::CellStyle { horz_align: match cell_style.halign { 0 => Some(HorzAlign::Center), 2 => Some(HorzAlign::Left), diff --git a/rust/pspp/src/spv/write.rs b/rust/pspp/src/spv/write.rs index e3d34602fd..1798874d0e 100644 --- a/rust/pspp/src/spv/write.rs +++ b/rust/pspp/src/spv/write.rs @@ -35,10 +35,13 @@ use crate::{ 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, VertAlign, + Axis2, Axis3, Category, Dimension, Footnote, FootnoteMarkerPosition, + FootnoteMarkerType, Footnotes, Group, Leaf, PivotTable, Value, + look::{ + Area, AreaStyle, Border, BorderStyle, BoxBorder, CellStyle, Color, FontStyle, + HeadingRegion, HorzAlign, LabelPosition, RowColBorder, RowParity, Stroke, + VertAlign, + }, value::{ValueInner, ValueStyle}, }, },