use crate::{
message::{Diagnostic, Severity},
- output::pivot::{Axis3, BorderStyle, Dimension, Group, Look},
+ output::pivot::{
+ Axis3, Dimension, Group,
+ look::{BorderStyle, Look},
+ },
};
use self::pivot::Value;
use pango::SCALE;
-use crate::output::pivot::HorzAlign;
+use crate::output::pivot::look::HorzAlign;
mod driver;
pub mod fsm;
},
},
page::PageSetup,
- pivot::{Color, Coord2, FontStyle},
+ pivot::{
+ Coord2,
+ look::{Color, FontStyle},
+ },
},
spv::html::Variable,
};
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;
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},
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},
};
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,
};
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.
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;
#[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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- 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<usize> 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<Border, BorderStyle> {
- 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- 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<i32>,
-
- /// 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<usize>,
-
- /// Keeps: columns to keep together on a page if possible.
- pub keeps: Vec<Range<usize>>,
-}
+pub mod look;
/// A 3-dimensional axis.
#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq, Sequence, Serialize)]
}
}
-/// 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<String>,
-
- /// 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<HeadingRegion, RangeInclusive<isize>>,
-
- /// 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<Area, AreaStyle>,
-
- /// Styles for borders in the pivot table.
- pub borders: EnumMap<Border, BorderStyle>,
-
- pub print_all_layers: bool,
-
- pub paginate_layers: bool,
-
- pub shrink_to_fit: EnumMap<Axis2, bool>,
-
- pub top_continuation: bool,
-
- pub bottom_continuation: bool,
-
- pub continuation: Option<String>,
-
- 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<Border, BorderStyle>) -> 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<Look> {
- static LOOK: OnceLock<Arc<Look>> = OnceLock::new();
- LOOK.get_or_init(|| Arc::new(Look::default())).clone()
- }
-
- pub fn from_xml(xml: &str) -> Result<Self, ParseLookError> {
- Ok(from_str::<TableProperties>(xml)
- .map_err(ParseLookError::from)?
- .into())
- }
-
- pub fn from_binary(tlo: &[u8]) -> Result<Self, ParseLookError> {
- parse_tlo(tlo).map_err(ParseLookError::from)
- }
-
- pub fn from_data(data: &[u8]) -> Result<Self, ParseLookError> {
- 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<R>(mut reader: R) -> Result<Self, ParseLookError>
- 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<Axis2> 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<HorzAlign>,
- 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<Axis2, [i32; 2]>,
-}
-
-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<HorzAlign>) -> 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<Axis2, [i32; 2]>) -> 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<Self, Self::Err> {
- 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<String>) -> 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<Rgba8> 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<Self, Self::Err> {
- 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<Srgb> = 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: serde::Serializer,
- {
- serializer.serialize_str(&self.display_css().to_small_string::<32>())
- }
-}
-
-impl<'de> Deserialize<'de> for Color {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- 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<E>(self, v: &str) -> Result<Self::Value, E>
- 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- 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<Stroke> 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")]
number /= 26;
}
output.reverse();
- write!(f, "{}", from_utf8(&output).unwrap())
+ write!(f, "{}", str::from_utf8(&output).unwrap())
}
}
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},
};
--- /dev/null
+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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<usize> 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<Border, BorderStyle> {
+ 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<i32>,
+
+ /// 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<usize>,
+
+ /// Keeps: columns to keep together on a page if possible.
+ pub keeps: Vec<Range<usize>>,
+}
+
+/// 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<String>,
+
+ /// 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<HeadingRegion, RangeInclusive<isize>>,
+
+ /// 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<Area, AreaStyle>,
+
+ /// Styles for borders in the pivot table.
+ pub borders: EnumMap<Border, BorderStyle>,
+
+ pub print_all_layers: bool,
+
+ pub paginate_layers: bool,
+
+ pub shrink_to_fit: EnumMap<Axis2, bool>,
+
+ pub top_continuation: bool,
+
+ pub bottom_continuation: bool,
+
+ pub continuation: Option<String>,
+
+ 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<Border, BorderStyle>) -> 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<Look> {
+ static LOOK: OnceLock<Arc<Look>> = OnceLock::new();
+ LOOK.get_or_init(|| Arc::new(Look::default())).clone()
+ }
+
+ pub fn from_xml(xml: &str) -> Result<Self, ParseLookError> {
+ Ok(from_str::<TableProperties>(xml)
+ .map_err(ParseLookError::from)?
+ .into())
+ }
+
+ pub fn from_binary(tlo: &[u8]) -> Result<Self, ParseLookError> {
+ parse_tlo(tlo).map_err(ParseLookError::from)
+ }
+
+ pub fn from_data(data: &[u8]) -> Result<Self, ParseLookError> {
+ 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<R>(mut reader: R) -> Result<Self, ParseLookError>
+ 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<Axis2> 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<HorzAlign>,
+ 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<Axis2, [i32; 2]>,
+}
+
+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<HorzAlign>) -> 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<Axis2, [i32; 2]>) -> 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<Self, Self::Err> {
+ 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<String>) -> 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<Rgba8> 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<Self, Self::Err> {
+ 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<Srgb> = 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&self.display_css().to_small_string::<32>())
+ }
+}
+
+impl<'de> Deserialize<'de> for Color {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ 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<E>(self, v: &str) -> Result<Self::Value, E>
+ 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ 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<Stroke> 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)
+ }
+}
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;
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),
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,
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},
};
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.
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,
+ },
},
};
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
}
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;
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(),
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),
}
},
},
- font_style: super::FontStyle {
+ font_style: look::FontStyle {
bold: style.weight > 400,
italic: style.italic,
underline: style.underline,
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,
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.
///
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 {
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
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,
use itertools::Itertools;
use crate::{
- output::pivot::{FontStyle, HorzAlign},
+ output::pivot::look::{FontStyle, HorzAlign},
spv::read::html::Style,
};
use std::borrow::Cow;
use crate::{
- output::pivot::{Color, FontStyle, HorzAlign},
+ output::pivot::look::{Color, FontStyle, HorzAlign},
spv::read::css::{Lexer, Token},
};
};
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()) {
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,
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 {
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,
}
impl Areas {
- fn decode(&self, encoding: &'static Encoding) -> EnumMap<pivot::Area, AreaStyle> {
+ fn decode(&self, encoding: &'static Encoding) -> EnumMap<look::Area, AreaStyle> {
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)
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),
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,
}
impl Borders {
- fn decode(&self) -> EnumMap<pivot::Border, pivot::BorderStyle> {
- let mut borders = pivot::Border::default_borders();
+ fn decode(&self) -> EnumMap<look::Border, look::BorderStyle> {
+ let mut borders = look::Border::default_borders();
for border in &self.borders {
if let Some((border, style)) = border.decode() {
borders[border] = style;
}
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,
};
)
.with_alpha((self.color >> 24) as u8);
- Some((border, pivot::BorderStyle { stroke, color }))
+ Some((border, look::BorderStyle { stroke, color }))
}
}
&self,
column_widths: &[i32],
row_heights: &[i32],
- ) -> EnumMap<Axis2, Option<Box<pivot::Sizing>>> {
+ ) -> EnumMap<Axis2, Option<Box<look::Sizing>>> {
fn decode_axis(
widths: &[i32],
breaks: &[u32],
keeps: &[(i32, i32)],
- ) -> Option<Box<pivot::Sizing>> {
+ ) -> Option<Box<look::Sizing>> {
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
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),
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},
},
},