From: Ben Pfaff Date: Tue, 11 Mar 2025 05:36:49 +0000 (-0700) Subject: tlo parser works X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=de7732dc3d8e8c8fbaebf692efa03b202969c02d;p=pspp tlo parser works --- diff --git a/rust/pspp/src/output/pivot/tlo.rs b/rust/pspp/src/output/pivot/tlo.rs index b97be96441..9dfd21aeff 100644 --- a/rust/pspp/src/output/pivot/tlo.rs +++ b/rust/pspp/src/output/pivot/tlo.rs @@ -1,6 +1,13 @@ use std::fmt::Debug; +use crate::output::pivot::{ + Axis2, Border, BoxBorder, FootnoteMarkerPosition, FootnoteMarkerType, HeadingRegion, + RowColBorder, RowLabelPosition, +}; + +use super::{Area, BorderStyle, Color, HorzAlign, Look, Stroke, VertAlign}; use binrw::{binread, BinRead, BinResult, Error as BinError}; +use enum_map::enum_map; #[binread] #[br(little)] @@ -10,6 +17,104 @@ struct TableLook { pv_separator_style: PvSeparatorStyle, pv_cell_style: PvCellStyle, pv_text_style: PvTextStyle, + #[br(parse_with = parse_default)] + v2_styles: V2Styles, +} + +/// Points (72/inch) to pixels (96/inch). +fn pt_to_px(pt: i32) -> usize { + num::cast((pt as f64 * (96.0 / 72.0)).round()).unwrap_or_default() +} + +/// Pixels (96/inch) to pixels (72/inch). +fn px_to_pt(px: i32) -> i32 { + num::cast((px as f64 * (72.0 / 96.0)).round()).unwrap_or_default() +} + +/// 20ths of a point to pixels (96/inch). +fn pt20_to_px(pt20: i32) -> usize { + num::cast((pt20 as f64 * (96.0 / 72.0 / 20.0)).round()).unwrap_or_default() +} + +fn iso8859_to_string(s: &[u8]) -> String { + s.iter().map(|byte| *byte as char).collect() +} + +impl From for Look { + fn from(look: TableLook) -> Self { + let flags = look.pt_table_look.flags; + Self { + name: None, + omit_empty: (flags & 2) != 0, + row_label_position: if look.pt_table_look.nested_row_labels { + RowLabelPosition::Nested + } else { + RowLabelPosition::InCorner + }, + heading_widths: enum_map! { + HeadingRegion::ColumnHeadings => look.v2_styles.min_column_width..=look.v2_styles.max_column_width, + HeadingRegion::RowHeadings => look.v2_styles.min_row_width..=look.v2_styles.max_row_width, + }.map(|_k, range| pt_to_px(*range.start())..=pt_to_px(*range.end())), + footnote_marker_type: if (flags & 4) != 0 { + FootnoteMarkerType::Numeric + } else { + FootnoteMarkerType::Alphabetic + }, + footnote_marker_position: if look.pt_table_look.footnote_marker_subscripts { + FootnoteMarkerPosition::Subscript + } else { + FootnoteMarkerPosition::Superscript + }, + areas: enum_map! { + Area::Title => super::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(), + Area::ColumnLabels => (&look.pv_text_style.column_labels).into(), + Area::RowLabels => (&look.pv_text_style.row_labels).into(), + Area::Data => (&look.pv_text_style.data).into(), + Area::Layers => (&look.pv_text_style.layers).into(), + }, + borders: enum_map! { + Border::Title => look.v2_styles.title, + Border::OuterFrame(BoxBorder::Left) => look.v2_styles.left_outer_frame, + Border::OuterFrame(BoxBorder::Top) => look.v2_styles.top_outer_frame, + Border::OuterFrame(BoxBorder::Right) => look.v2_styles.right_outer_frame, + Border::OuterFrame(BoxBorder::Bottom) => look.v2_styles.bottom_outer_frame, + Border::InnerFrame(BoxBorder::Left) => look.v2_styles.left_inner_frame, + Border::InnerFrame(BoxBorder::Top) => look.v2_styles.top_inner_frame, + Border::InnerFrame(BoxBorder::Right) => look.v2_styles.right_inner_frame, + Border::InnerFrame(BoxBorder::Bottom) => look.v2_styles.bottom_inner_frame, + Border::Dimensions(RowColBorder::ColHorz) => look.pv_separator_style.horizontal_dimension_columns, + Border::Dimensions(RowColBorder::ColVert) => look.pv_separator_style.vertical_dimension_columns, + Border::Dimensions(RowColBorder::RowHorz) => look.pv_separator_style.vertical_dimension_rows, + Border::Dimensions(RowColBorder::RowVert) => look.pv_separator_style.vertical_dimension_rows, + Border::Categories(RowColBorder::ColHorz) => look.pv_separator_style.horizontal_category_columns, + Border::Categories(RowColBorder::ColVert) => look.pv_separator_style.vertical_category_columns, + Border::Categories(RowColBorder::RowHorz) => look.pv_separator_style.vertical_category_rows, + Border::Categories(RowColBorder::RowVert) => look.pv_separator_style.vertical_category_rows, + Border::DataLeft => look.v2_styles.data_left, + Border::DataTop => look.v2_styles.data_top + }, + print_all_layers: (flags & 8) != 0, + paginate_layers: (flags & 0x40) != 0, + shrink_to_fit: enum_map! { + Axis2::X => (flags & 0x10) != 0, + Axis2::Y => (flags & 0x20) != 0 + }, + top_continuation: (flags & 0x80) != 0, + bottom_continuation: (flags & 0x100) != 0, + continuation: { + let s = &look.v2_styles.continuation; + if s.is_empty() { + None + } else { + Some(s.clone()) + } + }, + n_orphan_lines: 0 + } + } } #[binread] @@ -35,7 +140,7 @@ struct PtTableLook { footnote_marker_subscripts: bool, #[br(temp, magic = b"\0\x36\0\0\0\x12\0\0\0")] - _empty: (), + _tmp: (), } #[binread] @@ -46,16 +151,24 @@ struct PvSeparatorStyle { tag: Tag, #[br(magic = b"\0")] - horizontal_dimension_rows: Separator, - vertical_dimension_rows: Separator, - horizontal_category_rows: Separator, - vertical_category_rows: Separator, + #[br(map = |separator: Separator| separator.into())] + horizontal_dimension_rows: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + vertical_dimension_rows: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + horizontal_category_rows: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + vertical_category_rows: BorderStyle, #[br(magic = b"\x03\x80\0")] - horizontal_dimension_columns: Separator, - vertical_dimension_columns: Separator, - horizontal_category_columns: Separator, - vertical_category_columns: Separator, + #[br(map = |separator: Separator| separator.into())] + horizontal_dimension_columns: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + vertical_dimension_columns: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + horizontal_category_columns: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + vertical_category_columns: BorderStyle, } #[binread] @@ -65,7 +178,57 @@ enum Separator { #[br(magic = 0u16)] None, #[br(magic = 1u16)] - Some { color: u32, style: u16, width: u16 }, + Some { + color: Color, + style: u16, + width: u16, + }, +} + +impl Separator { + const THICK: Self = Self::Some { + color: Color::BLACK, + style: 0, + width: 2, + }; +} + +impl From for BorderStyle { + fn from(separator: Separator) -> Self { + match separator { + Separator::None => BorderStyle { + stroke: Stroke::None, + color: Color::BLACK, + }, + Separator::Some { + color, + style, + width, + } => BorderStyle { + stroke: match (style, width) { + (0, 0) => Stroke::Thin, + (0, 2 | 3) => Stroke::Thick, + (1, _) => Stroke::Double, + (2, _) => Stroke::Dashed, + _ => Stroke::Solid, + }, + color, + }, + } + } +} + +impl BinRead for Color { + type Args<'a> = (); + + fn read_options( + reader: &mut R, + endian: binrw::Endian, + _args: (), + ) -> BinResult { + let raw = ::read_options(reader, endian, ())?; + Ok(Color::new(raw as u8, (raw >> 8) as u8, (raw >> 16) as u8)) + } } #[binread] @@ -75,7 +238,8 @@ struct PvCellStyle { #[br(assert(&tag.string == b"PVCellStyle"))] tag: Tag, - title_color: AreaColor, + #[br(map = |src: AreaColor| src.into())] + title_color: Color, } #[binread] @@ -83,11 +247,43 @@ struct PvCellStyle { #[derive(Debug)] struct AreaColor { #[br(magic = b"\0\x01\0")] - color10: u32, - color0: u32, + color10: Color, + color0: Color, shading: u8, #[br(temp, magic = 0u8)] - _empty: (), + _tmp: (), +} + +impl From for Color { + fn from(area_color: AreaColor) -> Self { + match area_color.shading { + 0 => area_color.color0, + x1 @ 1..=9 => { + let Color { + r: r0, + g: g0, + b: b0, + .. + } = area_color.color0; + let Color { + r: r1, + g: g1, + b: b1, + .. + } = area_color.color10; + fn mix(c0: u32, c1: u32, x1: u32) -> u8 { + let x0 = 10 - x1; + ((c0 * x0 + c1 * x1) / 10) as u8 + } + Color::new( + mix(r0 as u32, r1 as u32, x1 as u32), + mix(g0 as u32, g1 as u32, x1 as u32), + mix(b0 as u32, b1 as u32, x1 as u32), + ) + } + _ => area_color.color10, + } + } } #[binread] @@ -113,12 +309,65 @@ struct PvTextStyle { #[derive(Debug)] struct MostAreas { #[br(magic = b"\x06\x80")] - color: AreaColor, + #[br(map = |src: AreaColor| src.into())] + color: Color, #[br(magic = b"\x08\x80\0")] style: AreaStyle, } +impl From<&MostAreas> for super::AreaStyle { + fn from(area: &MostAreas) -> Self { + Self::from_tlo(area.color, &area.style) + } +} + +impl super::AreaStyle { + fn from_tlo(bg: Color, style: &AreaStyle) -> Self { + Self { + cell_style: super::CellStyle { + horz_align: match style.halign { + 0 => Some(HorzAlign::Left), + 1 => Some(HorzAlign::Right), + 2 => Some(HorzAlign::Center), + 4 => Some(HorzAlign::Decimal { + offset: style.decimal_offset as f64 / (72.0 * 20.0) * 96.0, + c: '.', + }), + _ => None, + }, + vert_align: match style.valign { + 0 => VertAlign::Top, + 1 => VertAlign::Bottom, + _ => VertAlign::Middle, + }, + margins: { + fn convert(pt20: u16) -> i32 { + num::cast(pt20_to_px(pt20 as i32)).unwrap_or_default() + } + enum_map! { + Axis2::X => [convert(style.left_margin), convert(style.right_margin)], + Axis2::Y => [convert(style.top_margin), convert(style.bottom_margin)] + } + }, + }, + font_style: super::FontStyle { + bold: style.weight > 400, + italic: style.italic, + underline: style.underline, + markup: false, + font: style.font_name.string.clone(), + fg: { + let fg = style.text_color.into(); + [fg, fg] + }, + bg: [bg, bg], + size: -style.font_size * 3 / 4, + }, + } + } +} + #[binread] #[br(little)] #[derive(Debug)] @@ -147,9 +396,66 @@ struct AreaStyle { rtf_charset_number: u32, x: u8, font_name: U8String, - text_color: u32, - #[br(magic = 0u16)] - _empty: (), + text_color: Color, + #[br(temp, magic = 0u16)] + _tmp: (), +} + +#[binread] +#[br(little)] +#[derive(Debug)] +struct V2Styles { + #[br(map = |separator: Separator| separator.into())] + title: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + left_inner_frame: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + right_inner_frame: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + top_inner_frame: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + bottom_inner_frame: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + left_outer_frame: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + right_outer_frame: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + top_outer_frame: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + bottom_outer_frame: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + data_left: BorderStyle, + #[br(map = |separator: Separator| separator.into())] + data_top: BorderStyle, + #[br(map = |s: U8String| s.string)] + continuation: String, + min_column_width: i32, + max_column_width: i32, + min_row_width: i32, + max_row_width: i32, +} + +impl Default for V2Styles { + fn default() -> Self { + Self { + title: Border::Title.default_border_style(), + left_inner_frame: Border::InnerFrame(BoxBorder::Left).default_border_style(), + right_inner_frame: Border::InnerFrame(BoxBorder::Right).default_border_style(), + top_inner_frame: Border::InnerFrame(BoxBorder::Top).default_border_style(), + bottom_inner_frame: Border::InnerFrame(BoxBorder::Bottom).default_border_style(), + left_outer_frame: Border::OuterFrame(BoxBorder::Left).default_border_style(), + right_outer_frame: Border::OuterFrame(BoxBorder::Right).default_border_style(), + top_outer_frame: Border::OuterFrame(BoxBorder::Top).default_border_style(), + bottom_outer_frame: Border::OuterFrame(BoxBorder::Bottom).default_border_style(), + data_left: Border::DataLeft.default_border_style(), + data_top: Border::DataTop.default_border_style(), + continuation: Default::default(), + min_column_width: 36, + max_column_width: 72, + min_row_width: 36, + max_row_width: 120, + } + } } #[binrw::parser(reader, endian)] @@ -164,6 +470,28 @@ fn parse_bool() -> BinResult { } } +#[binrw::parser(reader, endian)] +fn parse_option<'a, T>(args: T::Args<'a>) -> BinResult> +where + T: BinRead, +{ + match ::read_options(reader, endian, args) { + Err(error) => match error { + BinError::Io(_) => Err(error), + _ => Ok(None), + }, + Ok(inner) => Ok(Some(inner)), + } +} + +#[binrw::parser(reader, endian)] +fn parse_default<'a, T>(args: T::Args<'a>) -> BinResult +where + T: BinRead + Default, +{ + Ok(parse_option(reader, endian, (args,))?.unwrap_or_default()) +} + #[binread] #[br(little)] struct Tag { @@ -187,13 +515,16 @@ struct U8String { #[br(temp)] length: u8, - #[br(count = length)] - string: Vec, + #[br(temp, count = length)] + data: Vec, + + #[br(calc = iso8859_to_string(&data))] + string: String, } impl Debug for U8String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", String::from_utf8_lossy(&self.string)) + write!(f, "{:?}", &self.string) } } @@ -203,12 +534,14 @@ mod test { use binrw::BinRead; - use crate::output::pivot::tlo::TableLook; + use crate::output::pivot::{tlo::TableLook, Look}; #[test] fn parse() { let bytes = include_bytes!("test1.tlo"); let tlo = TableLook::read(&mut Cursor::new(bytes)).unwrap(); println!("{tlo:#?}"); + let look = Look::from(tlo); + println!("{look:#?}"); } }