From: Ben Pfaff Date: Sun, 9 Mar 2025 20:10:34 +0000 (-0700) Subject: more tablelook X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6a7c9cee00e6ae45f5313d25392765f66e697de1;p=pspp more tablelook --- diff --git a/rust/pspp/src/output/pivot/look_xml.rs b/rust/pspp/src/output/pivot/look_xml.rs new file mode 100644 index 0000000000..7175e03967 --- /dev/null +++ b/rust/pspp/src/output/pivot/look_xml.rs @@ -0,0 +1,526 @@ +use std::{fmt::Debug, num::ParseFloatError, str::FromStr}; + +use enum_map::enum_map; +use serde::{de::Visitor, Deserialize}; + +use crate::output::pivot::{ + Area, AreaStyle, Axis2, Border, BorderStyle, BoxBorder, Color, FootnoteMarkerPosition, + FootnoteMarkerType, HeadingRegion, HorzAlign, Look, RowColBorder, RowLabelPosition, VertAlign, +}; +use thiserror::Error as ThisError; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct TableProperties { + #[serde(rename = "@name")] + name: Option, + general_properties: GeneralProperties, + footnote_properties: FootnoteProperties, + cell_format_properties: CellFormatProperties, + border_properties: BorderProperties, + printing_properties: PrintingProperties, +} + +impl From for Look { + fn from(table_properties: TableProperties) -> Self { + Self { + name: table_properties.name, + omit_empty: table_properties.general_properties.hide_empty_rows, + row_label_position: table_properties.general_properties.row_label_position, + heading_widths: enum_map! { + HeadingRegion::ColumnHeadings => table_properties.general_properties.minimum_column_width..=table_properties.general_properties.maximum_column_width, + HeadingRegion::RowHeadings => table_properties.general_properties.minimum_row_width..=table_properties.general_properties.maximum_row_width, + }.map(|_k, r|(*r.start()).try_into().unwrap_or_default()..=(*r.end()).try_into().unwrap_or_default()), + footnote_marker_type: table_properties.footnote_properties.marker_type, + footnote_marker_position: table_properties.footnote_properties.marker_position, + areas: enum_map! { + Area::Title => table_properties.cell_format_properties.title.style.as_area_style(), + Area::Caption => table_properties.cell_format_properties.caption.style.as_area_style(), + Area::Footer => table_properties.cell_format_properties.footnotes.style.as_area_style(), + Area::Corner => table_properties.cell_format_properties.corner_labels.style.as_area_style(), + Area::ColumnLabels => table_properties.cell_format_properties.column_labels.style.as_area_style(), + Area::RowLabels => table_properties.cell_format_properties.row_labels.style.as_area_style(), + Area::Data => table_properties.cell_format_properties.data.style.as_area_style(), + Area::Layers => table_properties.cell_format_properties.layers.style.as_area_style(), + }, + borders: enum_map! { + Border::Title => table_properties.border_properties.title_layer_separator, + Border::OuterFrame(BoxBorder::Left) => table_properties.border_properties.left_outer_frame, + Border::OuterFrame(BoxBorder::Top) => table_properties.border_properties.top_outer_frame, + Border::OuterFrame(BoxBorder::Right) => table_properties.border_properties.right_outer_frame, + Border::OuterFrame(BoxBorder::Bottom) => table_properties.border_properties.bottom_outer_frame, + Border::InnerFrame(BoxBorder::Left) => table_properties.border_properties.left_inner_frame, + Border::InnerFrame(BoxBorder::Top) => table_properties.border_properties.top_inner_frame, + Border::InnerFrame(BoxBorder::Right) => table_properties.border_properties.right_inner_frame, + Border::InnerFrame(BoxBorder::Bottom) => table_properties.border_properties.bottom_inner_frame, + Border::Dimensions(RowColBorder::ColHorz) => table_properties.border_properties.horizontal_dimension_border_columns, + Border::Dimensions(RowColBorder::ColVert) => table_properties.border_properties.vertical_dimension_border_columns, + Border::Dimensions(RowColBorder::RowHorz) => table_properties.border_properties.horizontal_dimension_border_rows, + Border::Dimensions(RowColBorder::RowVert) => table_properties.border_properties.vertical_dimension_border_rows, + Border::Categories(RowColBorder::ColHorz) => table_properties.border_properties.horizontal_category_border_columns, + Border::Categories(RowColBorder::ColVert) => table_properties.border_properties.vertical_category_border_columns, + Border::Categories(RowColBorder::RowHorz) => table_properties.border_properties.horizontal_category_border_rows, + Border::Categories(RowColBorder::RowVert) => table_properties.border_properties.vertical_category_border_rows, + Border::DataLeft => table_properties.border_properties.data_area_left, + Border::DataTop => table_properties.border_properties.data_area_top, + }, + print_all_layers: table_properties.printing_properties.print_all_layers, + paginate_layers: table_properties + .printing_properties + .print_each_layer_on_separate_page, + shrink_to_fit: enum_map! { + Axis2::X => table_properties.printing_properties.rescale_wide_table_to_fit_page, + Axis2::Y => table_properties.printing_properties.rescale_long_table_to_fit_page, + }, + top_continuation: table_properties + .printing_properties + .continuation_text_at_top, + bottom_continuation: table_properties + .printing_properties + .continuation_text_at_bottom, + continuation: { + let text = table_properties.printing_properties.continuation_text; + if text == "" { + None + } else { + Some(text) + } + }, + n_orphan_lines: table_properties + .printing_properties + .window_orphan_lines + .try_into() + .unwrap_or_default(), + } + } +} + +#[derive(Deserialize, Debug)] +struct GeneralProperties { + #[serde(rename = "@hideEmptyRows")] + hide_empty_rows: bool, + + #[serde(rename = "@maximumColumnWidth")] + maximum_column_width: i64, + + #[serde(rename = "@minimumColumnWidth")] + minimum_column_width: i64, + + #[serde(rename = "@maximumRowWidth")] + maximum_row_width: i64, + + #[serde(rename = "@minimumRowWidth")] + minimum_row_width: i64, + + #[serde(rename = "@rowDimensionLabels")] + row_label_position: RowLabelPosition, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct FootnoteProperties { + #[serde(rename = "@markerPosition")] + marker_position: FootnoteMarkerPosition, + + #[serde(rename = "@numberFormat")] + marker_type: FootnoteMarkerType, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct CellFormatProperties { + caption: CellStyleHolder, + column_labels: CellStyleHolder, + corner_labels: CellStyleHolder, + data: CellStyleHolder, + footnotes: CellStyleHolder, + layers: CellStyleHolder, + row_labels: CellStyleHolder, + title: CellStyleHolder, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct CellStyleHolder { + style: CellStyle, +} + +#[derive(Deserialize, Debug, Default)] +#[serde(default)] +struct CellStyle { + #[serde(rename = "@alternatingColor")] + alternating_color: Option, + #[serde(rename = "@alternatingTextColor")] + alternating_text_color: Option, + #[serde(rename = "@color")] + color: Option, + #[serde(rename = "@color2")] + color2: Option, + #[serde(rename = "@font-family")] + font_family: String, + #[serde(rename = "@font-size")] + font_size: Dimension, + #[serde(rename = "@font-style")] + font_style: FontStyle, + #[serde(rename = "@font-weight")] + font_weight: FontWeight, + #[serde(rename = "@font-underline")] + font_underline: FontUnderline, + #[serde(rename = "@labelLocationVertical")] + label_location_vertical: LabelLocationVertical, + #[serde(rename = "@margin-bottom")] + margin_bottom: Dimension, + #[serde(rename = "@margin-left")] + margin_left: Dimension, + #[serde(rename = "@margin-right")] + margin_right: Dimension, + #[serde(rename = "@margin-top")] + margin_top: Dimension, + #[serde(rename = "@textAlignment", default)] + text_alignment: TextAlignment, + #[serde(rename = "@decimal-offset")] + decimal_offset: Dimension, +} + +impl CellStyle { + fn as_area_style(&self) -> AreaStyle { + AreaStyle { + cell_style: super::CellStyle { + horz_align: match self.text_alignment { + TextAlignment::Left => Some(HorzAlign::Left), + TextAlignment::Right => Some(HorzAlign::Right), + TextAlignment::Center => Some(HorzAlign::Center), + TextAlignment::Decimal => Some(HorzAlign::Decimal { + offset: self.decimal_offset.as_px_f64(), + c: '.', + }), + TextAlignment::Mixed => None, + }, + vert_align: match self.label_location_vertical { + LabelLocationVertical::Positive => VertAlign::Top, + LabelLocationVertical::Negative => VertAlign::Bottom, + LabelLocationVertical::Center => VertAlign::Middle, + }, + margins: enum_map! { + Axis2::X => [self.margin_left.as_px_i32(), self.margin_right.as_px_i32()], + Axis2::Y => [self.margin_top.as_px_i32(), self.margin_bottom.as_px_i32()], + }, + }, + font_style: super::FontStyle { + bold: self.font_weight == FontWeight::Bold, + italic: self.font_style == FontStyle::Italic, + underline: self.font_underline == FontUnderline::Underline, + markup: false, + font: self.font_family.clone(), + fg: [ + self.color.unwrap_or(Color::BLACK), + self.alternating_text_color.unwrap_or(Color::BLACK), + ], + bg: [ + self.color2.unwrap_or(Color::BLACK), + self.alternating_color.unwrap_or(Color::BLACK), + ], + size: self.font_size.as_pt_i32(), + }, + } + } +} + +#[derive(Deserialize, Debug, Default, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +enum FontStyle { + #[default] + Regular, + Italic, +} + +#[derive(Deserialize, Debug, Default, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +enum FontWeight { + #[default] + Regular, + Bold, +} + +#[derive(Deserialize, Debug, Default, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +enum FontUnderline { + #[default] + None, + Underline, +} + +#[derive(Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +enum TextAlignment { + Left, + Right, + Center, + Decimal, + #[default] + Mixed, +} + +#[derive(Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +enum LabelLocationVertical { + /// Top. + #[default] + Positive, + + /// Bottom. + Negative, + + /// Center. + Center, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct BorderProperties { + bottom_inner_frame: BorderStyle, + bottom_outer_frame: BorderStyle, + data_area_left: BorderStyle, + data_area_top: BorderStyle, + horizontal_category_border_columns: BorderStyle, + horizontal_category_border_rows: BorderStyle, + horizontal_dimension_border_columns: BorderStyle, + horizontal_dimension_border_rows: BorderStyle, + left_inner_frame: BorderStyle, + left_outer_frame: BorderStyle, + right_inner_frame: BorderStyle, + right_outer_frame: BorderStyle, + title_layer_separator: BorderStyle, + top_inner_frame: BorderStyle, + top_outer_frame: BorderStyle, + vertical_category_border_columns: BorderStyle, + vertical_category_border_rows: BorderStyle, + vertical_dimension_border_rows: BorderStyle, + vertical_dimension_border_columns: BorderStyle, +} + +#[derive(Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase", default)] +struct PrintingProperties { + #[serde(rename = "@printAllLayers")] + print_all_layers: bool, + + #[serde(rename = "@printEachLayerOnSeparatePage")] + print_each_layer_on_separate_page: bool, + + #[serde(rename = "@rescaleWideTableToFitPage")] + rescale_wide_table_to_fit_page: bool, + + #[serde(rename = "@rescaleLongTableToFitPage")] + rescale_long_table_to_fit_page: bool, + + #[serde(rename = "@windowOrphanLines")] + window_orphan_lines: i64, + + #[serde(rename = "@continuationText")] + continuation_text: String, + + #[serde(rename = "@continuationTextAtBottom")] + continuation_text_at_bottom: bool, + + #[serde(rename = "@continuationTextAtTop")] + continuation_text_at_top: bool, +} + +#[derive(Copy, Clone, Default, PartialEq)] +struct Dimension( + /// In inches. + f64, +); + +impl Dimension { + fn as_px_f64(self) -> f64 { + self.0 * 96.0 + } + fn as_px_i32(self) -> i32 { + num::cast(self.as_px_f64() + 0.5).unwrap_or_default() + } + fn as_pt_f64(self) -> f64 { + self.0 * 72.0 + } + fn as_pt_i32(self) -> i32 { + num::cast(self.as_pt_f64() + 0.5).unwrap_or_default() + } +} + +impl Debug for Dimension { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:.2}in", self.0) + } +} + +impl FromStr for Dimension { + type Err = DimensionParseError; + + fn from_str(s: &str) -> Result { + let s = s.trim_start(); + let unit = s.trim_start_matches(|c: char| c.is_ascii_digit() || c == '.'); + let number: f64 = s[..s.len() - unit.len()] + .parse() + .map_err(DimensionParseError::ParseFloatError)?; + let divisor = match unit.trim() { + // Inches. + "in" | "인치" | "pol." | "cala" | "cali" => 1.0, + + // Device-independent pixels. + "px" => 96.0, + + // Points. + "pt" | "пт" | "" => 72.0, + + // Centimeters. + "cm" | "см" => 2.54, + + other => return Err(DimensionParseError::InvalidUnit(other.into())), + }; + Ok(Dimension(number / divisor)) + } +} + +#[derive(ThisError, Debug, PartialEq, Eq)] +enum DimensionParseError { + /// Invalid number. + #[error("{0}")] + ParseFloatError(ParseFloatError), + + /// Unknown unit. + #[error("Unknown unit {0:?}")] + InvalidUnit(String), +} + +impl<'de> Deserialize<'de> for Dimension { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct DimensionVisitor; + + impl<'de> Visitor<'de> for DimensionVisitor { + type Value = Dimension; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string") + } + + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: serde::de::Error, + { + v.parse().map_err(E::custom) + } + } + + deserializer.deserialize_str(DimensionVisitor) + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use quick_xml::de::from_str; + + use crate::output::pivot::look_xml::{Dimension, DimensionParseError, TableProperties}; + + #[test] + fn dimension() { + assert_eq!(Dimension::from_str("1"), Ok(Dimension(1.0 / 72.0))); + assert_eq!(Dimension::from_str("1pt"), Ok(Dimension(1.0 / 72.0))); + assert_eq!(Dimension::from_str("1пт"), Ok(Dimension(1.0 / 72.0))); + assert_eq!(Dimension::from_str("1.0"), Ok(Dimension(1.0 / 72.0))); + assert_eq!(Dimension::from_str(" 1.0"), Ok(Dimension(1.0 / 72.0))); + assert_eq!(Dimension::from_str(" 1.0 "), Ok(Dimension(1.0 / 72.0))); + assert_eq!(Dimension::from_str("1.0 pt"), Ok(Dimension(1.0 / 72.0))); + assert_eq!(Dimension::from_str("1.0pt "), Ok(Dimension(1.0 / 72.0))); + assert_eq!(Dimension::from_str(" 1.0pt "), Ok(Dimension(1.0 / 72.0))); + + assert_eq!(Dimension::from_str("1in"), Ok(Dimension(1.0))); + + assert_eq!(Dimension::from_str("96px"), Ok(Dimension(1.0))); + + assert_eq!(Dimension::from_str("2.54cm"), Ok(Dimension(1.0))); + + assert_eq!( + Dimension::from_str(""), + Err(DimensionParseError::ParseFloatError( + "".parse::().unwrap_err() + )) + ); + assert_eq!( + Dimension::from_str("1.2.3"), + Err(DimensionParseError::ParseFloatError( + "1.2.3".parse::().unwrap_err() + )) + ); + assert_eq!( + Dimension::from_str("1asdf"), + Err(DimensionParseError::InvalidUnit("asdf".into())) + ); + } + + #[test] + fn look() { + const XML: &str = r##" + + + + + + + <vizml:style color="#000000" color2="#ffffff" font-family="Sans Serif" font-size="9pt" font-weight="bold" font-underline="none" labelLocationVertical="center" margin-bottom="6pt" margin-left="6pt" margin-right="8pt" margin-top="0pt" textAlignment="left"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"##; + let table_properties: TableProperties = from_str(XML).unwrap(); + dbg!(&table_properties); + } +} diff --git a/rust/pspp/src/output/pivot/mod.rs b/rust/pspp/src/output/pivot/mod.rs index 6dcded30cd..fd51041e20 100644 --- a/rust/pspp/src/output/pivot/mod.rs +++ b/rust/pspp/src/output/pivot/mod.rs @@ -70,6 +70,7 @@ use color::{palette::css::TRANSPARENT, AlphaColor, Rgba8, Srgb}; use encoding_rs::UTF_8; use enum_iterator::Sequence; use enum_map::{enum_map, Enum, EnumMap}; +use quick_xml::{de::from_str, DeError}; use serde::{de::Visitor, Deserialize}; use smallstr::SmallString; use smallvec::{smallvec, SmallVec}; @@ -83,6 +84,9 @@ use crate::{ pub mod output; +mod look_xml; +pub use look_xml::TableProperties; + /// Areas of a pivot table for styling purposes. #[derive(Copy, Clone, Debug, Default, Enum, PartialEq, Eq)] pub enum Area { @@ -575,6 +579,10 @@ impl Look { static LOOK: OnceLock> = OnceLock::new(); LOOK.get_or_init(|| Arc::new(Look::default())).clone() } + + fn from_xml(xml: &str) -> Result { + Ok(from_str::(xml)?.into()) + } } #[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Eq)] @@ -586,524 +594,6 @@ pub enum RowLabelPosition { InCorner, } -mod look_xml { - use std::{fmt::Debug, num::ParseFloatError, str::FromStr}; - - use enum_map::enum_map; - use serde::{de::Visitor, Deserialize}; - - use crate::output::pivot::{ - Area, AreaStyle, Axis2, Color, FootnoteMarkerPosition, FootnoteMarkerType, HeadingRegion, - HorzAlign, Look, RowLabelPosition, Stroke, VertAlign, - }; - use thiserror::Error as ThisError; - - #[derive(Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - pub struct TableProperties { - #[serde(rename = "@name")] - name: Option, - general_properties: GeneralProperties, - footnote_properties: FootnoteProperties, - cell_format_properties: CellFormatProperties, - border_properties: BorderProperties, - printing_properties: PrintingProperties, - } - - impl From for Look { - fn from(table_properties: TableProperties) -> Self { - Self { - name: table_properties.name, - omit_empty: table_properties.general_properties.hide_empty_rows, - row_label_position: table_properties.general_properties.row_label_position, - heading_widths: enum_map! { - HeadingRegion::ColumnHeadings => table_properties.general_properties.minimum_column_width..=table_properties.general_properties.maximum_column_width, - HeadingRegion::RowHeadings => table_properties.general_properties.minimum_row_width..=table_properties.general_properties.maximum_row_width, - }.map(|_k, r|(*r.start()).try_into().unwrap_or_default()..=(*r.end()).try_into().unwrap_or_default()), - footnote_marker_type: table_properties.footnote_properties.marker_type, - footnote_marker_position: table_properties.footnote_properties.marker_position, - areas: enum_map! { - Area::Title => table_properties.cell_format_properties.title.style.as_area_style(), - Area::Caption => table_properties.cell_format_properties.caption.style.as_area_style(), - Area::Footer => table_properties.cell_format_properties.footnotes.style.as_area_style(), - Area::Corner => table_properties.cell_format_properties.corner_labels.style.as_area_style(), - Area::ColumnLabels => table_properties.cell_format_properties.column_labels.style.as_area_style(), - Area::RowLabels => table_properties.cell_format_properties.row_labels.style.as_area_style(), - Area::Data => table_properties.cell_format_properties.data.style.as_area_style(), - Area::Layers => table_properties.cell_format_properties.layers.style.as_area_style(), - }, - borders: todo!(), - print_all_layers: table_properties.printing_properties.print_all_layers, - paginate_layers: table_properties - .printing_properties - .print_each_layer_on_separate_page, - shrink_to_fit: enum_map! { - Axis2::X => table_properties.printing_properties.rescale_wide_table_to_fit_page, - Axis2::Y => table_properties.printing_properties.rescale_long_table_to_fit_page, - }, - top_continuation: table_properties - .printing_properties - .continuation_text_at_top, - bottom_continuation: table_properties - .printing_properties - .continuation_text_at_bottom, - continuation: { - let text = table_properties.printing_properties.continuation_text; - if text == "" { - None - } else { - Some(text) - } - }, - n_orphan_lines: table_properties - .printing_properties - .window_orphan_lines - .try_into() - .unwrap_or_default(), - } - } - } - - #[derive(Deserialize, Debug)] - struct GeneralProperties { - #[serde(rename = "@hideEmptyRows")] - hide_empty_rows: bool, - - #[serde(rename = "@maximumColumnWidth")] - maximum_column_width: i64, - - #[serde(rename = "@minimumColumnWidth")] - minimum_column_width: i64, - - #[serde(rename = "@maximumRowWidth")] - maximum_row_width: i64, - - #[serde(rename = "@minimumRowWidth")] - minimum_row_width: i64, - - #[serde(rename = "@rowDimensionLabels")] - row_label_position: RowLabelPosition, - } - - #[derive(Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - struct FootnoteProperties { - #[serde(rename = "@markerPosition")] - marker_position: FootnoteMarkerPosition, - - #[serde(rename = "@numberFormat")] - marker_type: FootnoteMarkerType, - } - - #[derive(Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - struct CellFormatProperties { - caption: CellStyleHolder, - column_labels: CellStyleHolder, - corner_labels: CellStyleHolder, - data: CellStyleHolder, - footnotes: CellStyleHolder, - layers: CellStyleHolder, - row_labels: CellStyleHolder, - title: CellStyleHolder, - } - - #[derive(Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - struct CellStyleHolder { - style: CellStyle, - } - - #[derive(Deserialize, Debug, Default)] - #[serde(default)] - struct CellStyle { - #[serde(rename = "@alternatingColor")] - alternating_color: Option, - #[serde(rename = "@alternatingTextColor")] - alternating_text_color: Option, - #[serde(rename = "@color")] - color: Option, - #[serde(rename = "@color2")] - color2: Option, - #[serde(rename = "@font-family")] - font_family: String, - #[serde(rename = "@font-size")] - font_size: Dimension, - #[serde(rename = "@font-style")] - font_style: FontStyle, - #[serde(rename = "@font-weight")] - font_weight: FontWeight, - #[serde(rename = "@font-underline")] - font_underline: FontUnderline, - #[serde(rename = "@labelLocationVertical")] - label_location_vertical: LabelLocationVertical, - #[serde(rename = "@margin-bottom")] - margin_bottom: Dimension, - #[serde(rename = "@margin-left")] - margin_left: Dimension, - #[serde(rename = "@margin-right")] - margin_right: Dimension, - #[serde(rename = "@margin-top")] - margin_top: Dimension, - #[serde(rename = "@textAlignment", default)] - text_alignment: TextAlignment, - #[serde(rename = "@decimal-offset")] - decimal_offset: Dimension, - } - - impl CellStyle { - fn as_area_style(&self) -> AreaStyle { - AreaStyle { - cell_style: super::CellStyle { - horz_align: match self.text_alignment { - TextAlignment::Left => Some(HorzAlign::Left), - TextAlignment::Right => Some(HorzAlign::Right), - TextAlignment::Center => Some(HorzAlign::Center), - TextAlignment::Decimal => Some(HorzAlign::Decimal { - offset: self.decimal_offset.as_px_f64(), - c: '.', - }), - TextAlignment::Mixed => None, - }, - vert_align: match self.label_location_vertical { - LabelLocationVertical::Positive => VertAlign::Top, - LabelLocationVertical::Negative => VertAlign::Bottom, - LabelLocationVertical::Center => VertAlign::Middle, - }, - margins: enum_map! { - Axis2::X => [self.margin_left.as_px_i32(), self.margin_right.as_px_i32()], - Axis2::Y => [self.margin_top.as_px_i32(), self.margin_bottom.as_px_i32()], - }, - }, - font_style: super::FontStyle { - bold: self.font_weight == FontWeight::Bold, - italic: self.font_style == FontStyle::Italic, - underline: self.font_underline == FontUnderline::Underline, - markup: false, - font: self.font_family.clone(), - fg: [ - self.color.unwrap_or(Color::BLACK), - self.alternating_text_color.unwrap_or(Color::BLACK), - ], - bg: [ - self.color2.unwrap_or(Color::BLACK), - self.alternating_color.unwrap_or(Color::BLACK), - ], - size: self.font_size.as_pt_i32(), - }, - } - } - } - - #[derive(Deserialize, Debug, Default, PartialEq, Eq)] - #[serde(rename_all = "camelCase")] - enum FontStyle { - #[default] - Regular, - Italic, - } - - #[derive(Deserialize, Debug, Default, PartialEq, Eq)] - #[serde(rename_all = "camelCase")] - enum FontWeight { - #[default] - Regular, - Bold, - } - - #[derive(Deserialize, Debug, Default, PartialEq, Eq)] - #[serde(rename_all = "camelCase")] - enum FontUnderline { - #[default] - None, - Underline, - } - - #[derive(Deserialize, Debug, Default)] - #[serde(rename_all = "camelCase")] - enum TextAlignment { - Left, - Right, - Center, - Decimal, - #[default] - Mixed, - } - - #[derive(Deserialize, Debug, Default)] - #[serde(rename_all = "camelCase")] - enum LabelLocationVertical { - /// Top. - #[default] - Positive, - - /// Bottom. - Negative, - - /// Center. - Center, - } - - #[derive(Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - struct BorderProperties { - bottom_inner_frame: BorderStyle, - bottom_outer_frame: BorderStyle, - data_area_left: BorderStyle, - data_area_top: BorderStyle, - horizontal_category_border_columns: BorderStyle, - horizontal_category_border_rows: BorderStyle, - horizontal_dimension_border_columns: BorderStyle, - horizontal_dimension_border_rows: BorderStyle, - left_inner_frame: BorderStyle, - left_outer_frame: BorderStyle, - right_inner_frame: BorderStyle, - right_outer_frame: BorderStyle, - title_layer_separator: BorderStyle, - top_inner_frame: BorderStyle, - top_outer_frame: BorderStyle, - vertical_category_border_columns: BorderStyle, - vertical_category_border_rows: BorderStyle, - vertical_dimension_border_rows: BorderStyle, - vertical_dimension_border_columns: BorderStyle, - } - - #[derive(Deserialize, Debug)] - struct BorderStyle { - #[serde(rename = "@borderStyleType")] - border_style_type: Stroke, - - #[serde(rename = "@color")] - color: Color, - } - - #[derive(Deserialize, Debug, Default)] - #[serde(rename_all = "camelCase", default)] - struct PrintingProperties { - #[serde(rename = "@printAllLayers")] - print_all_layers: bool, - - #[serde(rename = "@printEachLayerOnSeparatePage")] - print_each_layer_on_separate_page: bool, - - #[serde(rename = "@rescaleWideTableToFitPage")] - rescale_wide_table_to_fit_page: bool, - - #[serde(rename = "@rescaleLongTableToFitPage")] - rescale_long_table_to_fit_page: bool, - - #[serde(rename = "@windowOrphanLines")] - window_orphan_lines: i64, - - #[serde(rename = "@continuationText")] - continuation_text: String, - - #[serde(rename = "@continuationTextAtBottom")] - continuation_text_at_bottom: bool, - - #[serde(rename = "@continuationTextAtTop")] - continuation_text_at_top: bool, - } - - #[derive(Copy, Clone, Default, PartialEq)] - struct Dimension( - /// In inches. - f64, - ); - - impl Dimension { - fn as_px_f64(self) -> f64 { - self.0 * 96.0 - } - fn as_px_i32(self) -> i32 { - num::cast(self.as_px_f64() + 0.5).unwrap_or_default() - } - fn as_pt_f64(self) -> f64 { - self.0 * 72.0 - } - fn as_pt_i32(self) -> i32 { - num::cast(self.as_pt_f64() + 0.5).unwrap_or_default() - } - } - - impl Debug for Dimension { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.2}in", self.0) - } - } - - impl FromStr for Dimension { - type Err = DimensionParseError; - - fn from_str(s: &str) -> Result { - let s = s.trim_start(); - let unit = s.trim_start_matches(|c: char| c.is_ascii_digit() || c == '.'); - let number: f64 = s[..s.len() - unit.len()] - .parse() - .map_err(DimensionParseError::ParseFloatError)?; - let divisor = match unit.trim() { - // Inches. - "in" | "인치" | "pol." | "cala" | "cali" => 1.0, - - // Device-independent pixels. - "px" => 96.0, - - // Points. - "pt" | "пт" | "" => 72.0, - - // Centimeters. - "cm" | "см" => 2.54, - - other => return Err(DimensionParseError::InvalidUnit(other.into())), - }; - Ok(Dimension(number / divisor)) - } - } - - #[derive(ThisError, Debug, PartialEq, Eq)] - enum DimensionParseError { - /// Invalid number. - #[error("{0}")] - ParseFloatError(ParseFloatError), - - /// Unknown unit. - #[error("Unknown unit {0:?}")] - InvalidUnit(String), - } - - impl<'de> Deserialize<'de> for Dimension { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct DimensionVisitor; - - impl<'de> Visitor<'de> for DimensionVisitor { - type Value = Dimension; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string") - } - - fn visit_borrowed_str(self, v: &'de str) -> Result - where - E: serde::de::Error, - { - v.parse().map_err(E::custom) - } - } - - deserializer.deserialize_str(DimensionVisitor) - } - } - - #[cfg(test)] - mod test { - use std::str::FromStr; - - use quick_xml::de::from_str; - - use crate::output::pivot::look_xml::{Dimension, DimensionParseError, TableProperties}; - - #[test] - fn dimension() { - assert_eq!(Dimension::from_str("1"), Ok(Dimension(1.0 / 72.0))); - assert_eq!(Dimension::from_str("1pt"), Ok(Dimension(1.0 / 72.0))); - assert_eq!(Dimension::from_str("1пт"), Ok(Dimension(1.0 / 72.0))); - assert_eq!(Dimension::from_str("1.0"), Ok(Dimension(1.0 / 72.0))); - assert_eq!(Dimension::from_str(" 1.0"), Ok(Dimension(1.0 / 72.0))); - assert_eq!(Dimension::from_str(" 1.0 "), Ok(Dimension(1.0 / 72.0))); - assert_eq!(Dimension::from_str("1.0 pt"), Ok(Dimension(1.0 / 72.0))); - assert_eq!(Dimension::from_str("1.0pt "), Ok(Dimension(1.0 / 72.0))); - assert_eq!(Dimension::from_str(" 1.0pt "), Ok(Dimension(1.0 / 72.0))); - - assert_eq!(Dimension::from_str("1in"), Ok(Dimension(1.0))); - - assert_eq!(Dimension::from_str("96px"), Ok(Dimension(1.0))); - - assert_eq!(Dimension::from_str("2.54cm"), Ok(Dimension(1.0))); - - assert_eq!( - Dimension::from_str(""), - Err(DimensionParseError::ParseFloatError( - "".parse::().unwrap_err() - )) - ); - assert_eq!( - Dimension::from_str("1.2.3"), - Err(DimensionParseError::ParseFloatError( - "1.2.3".parse::().unwrap_err() - )) - ); - assert_eq!( - Dimension::from_str("1asdf"), - Err(DimensionParseError::InvalidUnit("asdf".into())) - ); - } - - #[test] - fn look() { - const XML: &str = r##" - - - - - - - <vizml:style color="#000000" color2="#ffffff" font-family="Sans Serif" font-size="9pt" font-weight="bold" font-underline="none" labelLocationVertical="center" margin-bottom="6pt" margin-left="6pt" margin-right="8pt" margin-top="0pt" textAlignment="left"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"##; - let table_properties: TableProperties = from_str(XML).unwrap(); - dbg!(&table_properties); - } - } -} - /// The heading region of a rendered pivot table: /// /// ```text @@ -1311,9 +801,12 @@ impl<'de> Deserialize<'de> for Color { } } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Deserialize)] pub struct BorderStyle { + #[serde(rename = "@borderStyleType")] pub stroke: Stroke, + + #[serde(rename = "@color")] pub color: Color, }