From: Ben Pfaff Date: Fri, 10 Oct 2025 16:36:12 +0000 (-0700) Subject: work X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=795c1ef80169568656fd9d62bcaf033f78e85c48;p=pspp work --- diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 14e846d3aa..d4c99b8f01 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -528,6 +528,40 @@ dependencies = [ "memchr", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.101", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -688,6 +722,27 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "enumset" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -744,6 +799,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -1151,6 +1212,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1811,6 +1878,7 @@ dependencies = [ "encoding_rs", "enum-iterator", "enum-map", + "enumset", "flagset", "flate2", "hashbrown 0.15.5", diff --git a/rust/doc/src/SUMMARY.md b/rust/doc/src/SUMMARY.md index 66f6dd5e19..456092cf62 100644 --- a/rust/doc/src/SUMMARY.md +++ b/rust/doc/src/SUMMARY.md @@ -8,6 +8,7 @@ - [Inspecting System Files](invoking/pspp-show.md) - [Inspecting Portable Files](invoking/pspp-show-por.md) - [Inspecting SPSS/PC+ Files](invoking/pspp-show-pc.md) + - [Inspecting SPSS Viewer Files](invoking/pspp-show-spv.md) - [Decrypting Files](invoking/pspp-decrypt.md) - [Output Driver Configuration](invoking/output.md) diff --git a/rust/doc/src/invoking/pspp-show-spv.md b/rust/doc/src/invoking/pspp-show-spv.md new file mode 100644 index 0000000000..e5854bb99b --- /dev/null +++ b/rust/doc/src/invoking/pspp-show-spv.md @@ -0,0 +1,125 @@ +# Inspecting SPSS Viewer Files + +The `pspp show-spv` command reads SPSS Viewer (SPV) files, which +usually have `.sav` extension, and produces a report. The basic +syntax is: + +``` +pspp show-spv [OUTPUT] +``` + +where `` is a mode of operation (see below) and `` is the +SPV file to read, and `[OUTPUT]` is the output file name. If +`[OUTPUT]` is omitted, output is written to the terminal. + +The following ``s are accepted: + +* `dir`: Outputs a table of contents for the SPV file, listing every + selected object, which by default is every object except for hidden + ones. + + The following additional option for `dir` is intended mainly for use + by PSPP developers: + + - `--member-names`: Also show the names of the ZIP file members + associated with each object. + +* `get-table-look`: Extracts the TableLook from the first table in the + selected objects and outputs it in TableLook XML format. The output + file should have an `.stt` extension. + + Use `-` for `` to instead write the default TableLook. + +* `convert-table-look`: Reads an `.stt` or `.tlo` TableLook file as + `` and outputs it in TableLook XML format. The output file + should have an `.stt` extension. + + This is useful for converting a TableLook `.tlo` file from SPSS 15 + or earlier into the newer `.stt` format. + +## Input Selection Options + +The `dir` and `convert` commands, by default, operate on all of the +objects in the source SPV file, except for objects that are not visible +in the output viewer window. The user may specify these options to +select a subset of the input objects. When multiple options are used, +only objects that satisfy all of them are selected: + +* `--select=[^]CLASS...` + Include only objects of the given `CLASS`; with leading `^`, include + only objects not in the class. Use commas to separate multiple + classes. The supported classes are: + + * `charts` + * `headings` + * `logs` + * `models` + * `tables` + * `texts` + * `trees` + * `warnings` + * `outlineheaders` + * `pagetitle` + * `notes` + * `unknown` + * `other` + +* `--commands=[^]COMMAND...` + `--subtypes=[^]SUBTYPE...` + `--labels=[^]LABEL...` + Include only objects with the specified `COMMAND`, `SUBTYPE`, or + `LABEL`. With a leading `^`, include only the objects that do not + match. Multiple values may be specified separated by commas. An + asterisk at the end of a value acts as a wildcard. + + The `--command` option matches command identifiers, case + insensitively. All of the objects produced by a single command use + the same, unique command identifier. Command identifiers are + always in English regardless of the language used for output. They + often differ from the command name in PSPP syntax. Use the + `pspp-output` program's `dir` command to print command identifiers + in particular output. + + The `--subtypes` option matches particular tables within a command, + case insensitively. Subtypes are not necessarily unique: two + commands that produce similar output tables may use the same + subtype. Subtypes are always in English and `dir` will print them. + + The `--labels` option matches the labels in table output (that is, + the table titles). Labels are affected by the output language, + variable names and labels, split file settings, and other factors. + +* `--nth-commands=N...` + Include only objects from the `N`th command that matches `--command` + (or the `N`th command overall if `--command` is not specified), + where `N` is 1 for the first command, 2 for the second, and so on. + +* `--instances=INSTANCE...` + Include the specified `INSTANCE` of an object that matches the other + criteria within a single command. `INSTANCE` may be a number (1 for + the first instance, 2 for the second, and so on) or `last` for the + last instance. + +* `--show-hidden` + Include hidden output objects in the output. By default, they are + excluded. + +* `--or` + Separates two sets of selection options. Objects selected by + either set of options are included in the output. + +The following additional input selection options are intended mainly +for use by PSPP developers: + +* `--errors` + Include only objects that cause an error when read. With the + `convert` command, this is most useful in conjunction with the + `--force` option. + +* `--members=MEMBER...` + Include only the objects that include a listed Zip file `MEMBER`. + More than one name may be included, comma-separated. The members + in an SPV file may be listed with the `dir` command by adding the + `--show-members` option or with the `zipinfo` program included with + many operating systems. Error messages that `pspp-output` prints + when it reads SPV files also often include member names. diff --git a/rust/paper-sizes/src/serde.rs b/rust/paper-sizes/src/serde.rs new file mode 100644 index 0000000000..278f423529 --- /dev/null +++ b/rust/paper-sizes/src/serde.rs @@ -0,0 +1,100 @@ +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; + +#[cfg(feature = "serde")] +use crate::PaperSize; +use crate::{Length, Unit}; + +impl<'de> Deserialize<'de> for Length { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(D::Error::custom) + } +} + +impl Serialize for Length { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl Serialize for PaperSize { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for PaperSize { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(D::Error::custom) + } +} + +impl Serialize for Unit { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.to_string().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Unit { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(D::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use crate::{Length, PaperSize, Unit}; + + #[test] + fn unit() { + assert_eq!(serde_json::to_string(&Unit::Point).unwrap(), "\"pt\""); + assert_eq!(serde_json::from_str::("\"pt\"").unwrap(), Unit::Point) + } + + #[test] + fn length() { + assert_eq!( + serde_json::to_string(&Length::new(123.0, Unit::Millimeter)).unwrap(), + "\"123mm\"" + ); + assert_eq!( + serde_json::from_str::("\"123mm\"").unwrap(), + Length::new(123.0, Unit::Millimeter) + ) + } + + #[test] + fn paper_size() { + assert_eq!( + serde_json::to_string(&PaperSize::new(8.5, 11.0, Unit::Inch)).unwrap(), + "\"8.5x11in\"" + ); + assert_eq!( + serde_json::from_str::("\"8.5x11in\"").unwrap(), + PaperSize::new(8.5, 11.0, Unit::Inch) + ) + } +} diff --git a/rust/pspp/Cargo.toml b/rust/pspp/Cargo.toml index a46db9bf0f..818e3b2773 100644 --- a/rust/pspp/Cargo.toml +++ b/rust/pspp/Cargo.toml @@ -58,6 +58,7 @@ codepage-437 = "0.1.0" serde_path_to_error = "0.1.20" html_parser = "0.7.0" paper-sizes = { path = "../paper-sizes", features = ["serde"] } +enumset = "1.1.10" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.48.0", features = ["Win32_Globalization"] } diff --git a/rust/pspp/src/main.rs b/rust/pspp/src/main.rs index 4fa6f80b55..de9293200f 100644 --- a/rust/pspp/src/main.rs +++ b/rust/pspp/src/main.rs @@ -19,13 +19,17 @@ use clap::{Parser, Subcommand}; use encoding_rs::Encoding; use thiserror::Error as ThisError; -use crate::{convert::Convert, decrypt::Decrypt, show::Show, show_pc::ShowPc, show_por::ShowPor}; +use crate::{ + convert::Convert, decrypt::Decrypt, show::Show, show_pc::ShowPc, show_por::ShowPor, + show_spv::ShowSpv, +}; mod convert; mod decrypt; mod show; mod show_pc; mod show_por; +mod show_spv; /// PSPP, a program for statistical analysis of sampled data. #[derive(Parser, Debug)] @@ -42,6 +46,7 @@ enum Command { Show(Show), ShowPor(ShowPor), ShowPc(ShowPc), + ShowSpv(ShowSpv), } impl Command { @@ -52,6 +57,7 @@ impl Command { Command::Show(show) => show.run(), Command::ShowPor(show_por) => show_por.run(), Command::ShowPc(show_pc) => show_pc.run(), + Command::ShowSpv(show_spv) => show_spv.run(), } } } diff --git a/rust/pspp/src/output.rs b/rust/pspp/src/output.rs index 77f097fc9c..6e60673776 100644 --- a/rust/pspp/src/output.rs +++ b/rust/pspp/src/output.rs @@ -17,15 +17,17 @@ #![allow(dead_code)] use std::{ borrow::Cow, + str::FromStr, sync::{Arc, OnceLock}, }; use enum_map::EnumMap; +use enumset::{EnumSet, EnumSetType}; use pivot::PivotTable; use serde::Serialize; use crate::{ - message::Diagnostic, + message::{Diagnostic, Severity}, output::pivot::{Axis3, BorderStyle, Dimension, Group, Look}, }; @@ -61,6 +63,11 @@ pub struct Item { /// Item details. details: Details, + + /// If the item was read from an SPV file, this is additional information + /// related to that file. + #[serde(skip_serializing)] + spv_info: Option>, } impl Item { @@ -71,6 +78,7 @@ impl Item { command_name: details.command_name().cloned(), show: true, details, + spv_info: None, } } @@ -91,6 +99,13 @@ impl Item { ..self } } + + pub fn with_spv_info(self, spv_info: SpvInfo) -> Self { + Self { + spv_info: Some(Box::new(spv_info)), + ..self + } + } } impl From for Item @@ -320,3 +335,218 @@ impl ItemCursor { } } } + +/// Information for output items that were read from an SPV file. +/// +/// This is for debugging and troubleshooting purposes. +#[derive(Debug)] +pub struct SpvInfo { + /// True if there was an error reading the output item (e.g. because of + /// corruption or because PSPP doesn't understand the format). + pub error: bool, + + /// Name of structure member in ZIP file. + pub structure_member: String, + + /// Additional members based on item type. + pub members: Option, +} + +impl SpvInfo { + pub fn new(structure_member: &str) -> Self { + Self { + error: false, + structure_member: structure_member.into(), + members: None, + } + } + + pub fn with_members(self, members: SpvMembers) -> Self { + Self { + members: Some(members), + ..self + } + } +} + +/// Identifies ZIP file members for one kind of output item in an SPV file. +#[derive(Debug)] +pub enum SpvMembers { + /// Light detail members. + Light( + /// `.bin` member name. + String, + ), + /// Legacy detail members. + Legacy { + /// `.xml` member name. + xml: String, + /// `.bin` member name. + binary: String, + }, + /// Image members. + Image( + /// `.png` file. + String, + ), +} + +/// Classifications for output items. These only roughly correspond to the +/// output item types; for example, "warnings" are a subset of text items. +#[derive(Debug, EnumSetType)] +pub enum Class { + Charts, + Headings, + Logs, + Models, + Tables, + Texts, + Trees, + Warnings, + OutlineHeaders, + PageTitle, + Notes, + Unknown, + Other, +} + +impl FromStr for Class { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "charts" => Ok(Self::Charts), + "headings" => Ok(Self::Headings), + "logs" => Ok(Self::Logs), + "models" => Ok(Self::Models), + "tables" => Ok(Self::Tables), + "texts" => Ok(Self::Texts), + "trees" => Ok(Self::Trees), + "warnings" => Ok(Self::Warnings), + "outlineheaders" => Ok(Self::OutlineHeaders), + "pagetitle" => Ok(Self::PageTitle), + "notes" => Ok(Self::Notes), + "unknown" => Ok(Self::Unknown), + "other" => Ok(Self::Other), + _ => Err(()), + } + } +} + +impl Item { + fn class(&self) -> Class { + let label = self.label.as_ref().map(|s| s.as_str()); + match &self.details { + Details::Chart => Class::Charts, + Details::Image => Class::Other, + Details::Group(_) => Class::OutlineHeaders, + Details::Message(diagnostic) => match diagnostic.severity { + Severity::Note => Class::Notes, + Severity::Error | Severity::Warning => Class::Warnings, + }, + Details::PageBreak => Class::Other, + Details::Table(_) => match label { + Some("Warnings") => Class::Warnings, + Some("Notes") => Class::Notes, + _ => Class::Tables, + }, + Details::Text(_) => match label { + Some("Title") => Class::Headings, + Some("Log") => Class::Logs, + Some("Page Title") => Class::PageTitle, + _ => Class::Texts, + }, + } + } +} + +pub struct Match { + /// - `None`: Include all objects. + /// - `Some(false)`: Include only visible objects. + /// - `Some(true)`: Include only hidden objects. + hidden: Option, + + /// - `None`: Include all objects. + /// - `Some(false)`: Include only objects with no error on loading. + /// - `Some(true)`: Include only objects with an error on loading. + error: Option, + + /// Classes to include. + classes: EnumSet, + + /// Command names to match. + commands: StringMatch, + + /// Subtypes to match. + subtypes: StringMatch, + + /// Labels to match. + labels: StringMatch, + + /// Include objects under commands with indexes listed in COMMANDS. Indexes + /// are 1-based. Everything is included if N_COMMANDS is 0. + nth_commands: Vec, + + /// Include XML and binary member names that match (except that everything + /// is included by default if empty). + members: Vec, + + /// Include the objects with indexes listed in INSTANCES within each of the + /// commands that are included. Indexes are 1-based. Index -1 means the + /// last object within a command. + instances: Vec, +} + +pub enum StringMatch { + Include(Vec), + Exclude(Vec), +} + +impl Default for StringMatch { + fn default() -> Self { + // Include everything, by excluding nothing. + Self::Exclude(Vec::new()) + } +} + +impl StringMatch { + fn matches(&self, s: &str) -> bool { + fn inner(items: &[String], s: &str) -> bool { + items.iter().any(|item| match item.strip_suffix('*') { + Some(prefix) => s.starts_with(prefix), + None => s == item, + }) + } + + match self { + StringMatch::Include(items) => inner(items, s), + StringMatch::Exclude(items) => !inner(items, s), + } + } +} + +#[cfg(test)] +mod tests { + use crate::output::StringMatch; + + #[test] + fn string_matches() { + assert!(StringMatch::default().matches("xyzzy")); + assert!(StringMatch::Exclude(Vec::new()).matches("xyzzy")); + assert!(!StringMatch::Include(Vec::new()).matches("xyzzy")); + + let m = StringMatch::Include(vec![String::from("xyz"), String::from("abc*")]); + assert!(m.matches("xyz")); + assert!(!m.matches("xyzzy")); + assert!(!m.matches("ab")); + assert!(m.matches("abc")); + assert!(m.matches("abcd")); + + let m = StringMatch::Exclude(vec![String::from("xyz"), String::from("abc*")]); + assert!(!m.matches("xyz")); + assert!(m.matches("xyzzy")); + assert!(m.matches("ab")); + assert!(!m.matches("abc")); + assert!(!m.matches("abcd")); + } +} diff --git a/rust/pspp/src/output/drivers/cairo/fsm.rs b/rust/pspp/src/output/drivers/cairo/fsm.rs index 89d5d61c53..e4b8002a50 100644 --- a/rust/pspp/src/output/drivers/cairo/fsm.rs +++ b/rust/pspp/src/output/drivers/cairo/fsm.rs @@ -220,7 +220,7 @@ fn xr_clip(context: &Context, clip: &Rect2) { } } -fn xr_set_color(context: &Context, color: &Color) { +fn xr_set_color(context: &Context, color: Color) { fn as_frac(x: u8) -> f64 { x as f64 / 255.0 } @@ -490,7 +490,7 @@ impl CairoDevice<'_> { self.context.move_to(xr_to_pt(x0), xr_to_pt(y0)); self.context.line_to(xr_to_pt(x1), xr_to_pt(y1)); if !self.style.use_system_colors { - xr_set_color(self.context, &color); + xr_set_color(self.context, color); } if stroke == Stroke::Dashed { self.context.set_dash(&[2.0], 0.0); @@ -698,15 +698,12 @@ impl Device for CairoDevice<'_> { fn draw_cell( &mut self, draw_cell: &DrawCell, - alternate_row: bool, mut bb: Rect2, valign_offset: usize, spill: EnumMap, clip: &Rect2, ) { - let fg = &draw_cell.font_style.fg[alternate_row as usize]; - let bg = &draw_cell.font_style.bg[alternate_row as usize]; - + let bg = draw_cell.font_style.bg; if (bg.r != 255 || bg.g != 255 || bg.b != 255) && bg.alpha != 0 { self.context.save().unwrap(); let bg_clip = Rect2::from_fn(|axis| { @@ -733,7 +730,7 @@ impl Device for CairoDevice<'_> { } if !self.style.use_system_colors { - xr_set_color(self.context, fg); + xr_set_color(self.context, draw_cell.font_style.fg); } self.context.save().unwrap(); diff --git a/rust/pspp/src/output/drivers/html.rs b/rust/pspp/src/output/drivers/html.rs index 411f7db8af..2d3492ef0e 100644 --- a/rust/pspp/src/output/drivers/html.rs +++ b/rust/pspp/src/output/drivers/html.rs @@ -90,7 +90,6 @@ where self.put_cell( DrawCell::new(cell.inner(), &title), Rect2::new(0..1, 0..1), - false, "caption", None, )?; @@ -103,7 +102,6 @@ where self.put_cell( DrawCell::new(cell.inner(), &layers), Rect2::new(0..output.body.n[Axis2::X], 0..1), - false, "td", None, )?; @@ -120,13 +118,9 @@ where if cell.is_top_left() { let is_header = x < output.body.h[Axis2::X] || y < output.body.h[Axis2::Y]; let tag = if is_header { "th" } else { "td" }; - let alternate_row = y - .checked_sub(output.body.h[Axis2::Y]) - .is_some_and(|y| y % 2 == 1); self.put_cell( DrawCell::new(cell.inner(), &output.body), cell.rect(), - alternate_row, tag, Some(&output.body), )?; @@ -143,7 +137,6 @@ where self.put_cell( DrawCell::new(caption.get(Coord2::new(0, 0)).inner(), &caption), Rect2::new(0..output.body.n[Axis2::X], 0..1), - false, "td", None, )?; @@ -156,7 +149,6 @@ where self.put_cell( DrawCell::new(cell.inner(), &footnotes), Rect2::new(0..output.body.n[Axis2::X], 0..1), - false, "td", None, )?; @@ -173,7 +165,6 @@ where &mut self, cell: DrawCell<'_>, rect: Rect2, - alternate_row: bool, tag: &str, table: Option<&Table>, ) -> std::io::Result<()> { @@ -202,12 +193,12 @@ where if let Some(vert_align) = vert_align { write!(&mut style, "vertical-align: {vert_align}; ").unwrap(); } - let bg = cell.font_style.bg[alternate_row as usize]; + let bg = cell.font_style.bg; if bg != Color::WHITE { write!(&mut style, "background: {}; ", bg.display_css()).unwrap(); } - let fg = cell.font_style.fg[alternate_row as usize]; + let fg = cell.font_style.fg; if fg != Color::BLACK { write!(&mut style, "color: {}; ", fg.display_css()).unwrap(); } diff --git a/rust/pspp/src/output/drivers/spv.rs b/rust/pspp/src/output/drivers/spv.rs index 3a465420df..907f96cd64 100644 --- a/rust/pspp/src/output/drivers/spv.rs +++ b/rust/pspp/src/output/drivers/spv.rs @@ -46,7 +46,7 @@ use crate::{ Area, AreaStyle, Axis2, Axis3, Border, BorderStyle, BoxBorder, Category, CellStyle, Color, Dimension, FontStyle, Footnote, FootnoteMarkerPosition, FootnoteMarkerType, Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Leaf, PivotTable, - RowColBorder, Stroke, Value, ValueInner, ValueStyle, VertAlign, + RowColBorder, RowParity, Stroke, Value, ValueInner, ValueStyle, VertAlign, }, }, settings::Show, @@ -306,11 +306,16 @@ impl BinWrite for PivotTable { Area::Corner, Area::Labels(Axis2::X), Area::Labels(Axis2::Y), - Area::Data, + Area::Data(RowParity::Even), Area::Layers, ]; for (index, area) in SPV_AREAS.into_iter().enumerate() { - self.style.look.areas[area].write_le_args(writer, index)?; + let odd_data_style = if let Area::Data(_) = area { + Some(&self.style.look.areas[Area::Data(RowParity::Odd)]) + } else { + None + }; + self.style.look.areas[area].write_le_args(writer, (index, odd_data_style))?; } // Borders. @@ -811,13 +816,13 @@ impl BinWrite for Footnotes { } impl BinWrite for AreaStyle { - type Args<'a> = usize; + type Args<'a> = (usize, Option<&'a AreaStyle>); fn write_options( &self, writer: &mut W, endian: Endian, - index: usize, + (index, odd_data_style): (usize, Option<&AreaStyle>), ) -> binrw::BinResult<()> { let typeface = if self.font_style.font.is_empty() { "SansSerif" @@ -835,19 +840,15 @@ impl BinWrite for AreaStyle { .horz_align .map_or(64173, |horz_align| horz_align.as_spv(61453)), self.cell_style.vert_align.as_spv(), - self.font_style.fg[0], - self.font_style.bg[0], + self.font_style.fg, + self.font_style.bg, ) .write_options(writer, endian, ())?; - if self.font_style.fg[0] != self.font_style.fg[1] - || self.font_style.bg[0] != self.font_style.bg[1] - { - (SpvBool(true), self.font_style.fg[1], self.font_style.bg[1]).write_options( - writer, - endian, - (), - )?; + let alt_fg = odd_data_style.map_or(self.font_style.fg, |style| style.font_style.fg); + let alt_bg = odd_data_style.map_or(self.font_style.bg, |style| style.font_style.bg); + if self.font_style.fg != alt_fg || self.font_style.bg != alt_bg { + (SpvBool(true), alt_fg, alt_bg).write_options(writer, endian, ())?; } else { (SpvBool(false), SpvString(""), SpvString("")).write_options(writer, endian, ())?; } @@ -1080,8 +1081,8 @@ impl BinWrite for FontStyle { SpvBool(self.italic), SpvBool(self.underline), SpvBool(true), - self.fg[0], - self.bg[0], + self.fg, + self.bg, SpvString(typeface), (self.size as f64 * 1.33).ceil() as u8, ) diff --git a/rust/pspp/src/output/drivers/text.rs b/rust/pspp/src/output/drivers/text.rs index 6c3e0a4afa..b9d4e7ee76 100644 --- a/rust/pspp/src/output/drivers/text.rs +++ b/rust/pspp/src/output/drivers/text.rs @@ -612,7 +612,6 @@ impl Device for TextRenderer { fn draw_cell( &mut self, cell: &DrawCell, - _alternate_row: bool, bb: Rect2, valign_offset: usize, _spill: EnumMap, diff --git a/rust/pspp/src/output/pivot.rs b/rust/pspp/src/output/pivot.rs index 967ea845d1..4dce028d29 100644 --- a/rust/pspp/src/output/pivot.rs +++ b/rust/pspp/src/output/pivot.rs @@ -89,9 +89,12 @@ pub mod tests; mod tlo; /// Areas of a pivot table for styling purposes. -#[derive(Copy, Clone, Debug, Default, Enum, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)] pub enum Area { + /// Title. Title, + + /// Caption. Caption, /// Footnotes, @@ -100,16 +103,32 @@ pub enum Area { // Top-left corner. Corner, - /// Labels for columns ([Axis2::X]) and rows ([Axis2::Y]). - Labels(Axis2), - - #[default] - Data, + /// 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 { @@ -118,7 +137,7 @@ impl Display for Area { Area::Footer => write!(f, "footer"), Area::Corner => write!(f, "corner"), Area::Labels(axis2) => write!(f, "labels({axis2})"), - Area::Data => write!(f, "data"), + Area::Data(row) => write!(f, "data({row})"), Area::Layers => write!(f, "layers"), } } @@ -144,7 +163,7 @@ impl Area { Area::Corner => (Some(Left), Bottom, [8, 11], [1, 1]), Area::Labels(Axis2::X) => (Some(Center), Top, [8, 11], [1, 3]), Area::Labels(Axis2::Y) => (Some(Left), Top, [8, 11], [1, 3]), - Area::Data => (None, Top, [8, 11], [1, 1]), + Area::Data(_) => (None, Top, [8, 11], [1, 1]), Area::Layers => (Some(Left), Bottom, [8, 11], [1, 3]), }; CellStyle { @@ -166,6 +185,37 @@ impl Area { } } +/// Distinguishes [Area::Data] for even-numbered and odd-numbered rows. +#[derive(Copy, Clone, Debug, Default, Enum, PartialEq, Eq)] +pub enum RowParity { + /// Even-numbered rows. + /// + /// The first row is row 0, hence even. + #[default] + Even, + /// Odd-numbered rows. + Odd, +} + +impl From for RowParity { + fn from(value: usize) -> Self { + if value % 2 == 1 { + Self::Odd + } else { + Self::Even + } + } +} + +impl Display for RowParity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RowParity::Even => write!(f, "even"), + RowParity::Odd => write!(f, "odd"), + } + } +} + /// Table borders for styling purposes. #[derive(Copy, Clone, Debug, Enum, PartialEq, Eq)] pub enum Border { @@ -1045,16 +1095,8 @@ pub struct FontStyle { pub underline: bool, pub markup: bool, pub font: String, - - /// `fg[0]` is the usual foreground color. - /// - /// `fg[1]` is used only in [Area::Data] for odd-numbered rows. - pub fg: [Color; 2], - - /// `bg[0]` is the usual background color. - /// - /// `bg[1]` is used only in [Area::Data] for odd-numbered rows. - pub bg: [Color; 2], + pub fg: Color, + pub bg: Color, /// In 1/72" units. pub size: i32, @@ -1068,8 +1110,8 @@ impl Default for FontStyle { underline: false, markup: false, font: String::from("Sans Serif"), - fg: [Color::BLACK; 2], - bg: [Color::WHITE; 2], + fg: Color::BLACK, + bg: Color::WHITE, size: 9, } } @@ -1098,22 +1140,10 @@ impl FontStyle { } } pub fn with_fg(self, fg: Color) -> Self { - Self { - fg: [fg, fg], - ..self - } - } - pub fn with_alternate_fg(self, fg: [Color; 2]) -> Self { Self { fg, ..self } } pub fn with_bg(self, fg: Color) -> Self { - Self { - fg: [fg, fg], - ..self - } - } - pub fn with_alternate_bg(self, bg: [Color; 2]) -> Self { - Self { bg, ..self } + Self { fg, ..self } } } diff --git a/rust/pspp/src/output/pivot/look_xml.rs b/rust/pspp/src/output/pivot/look_xml.rs index a9e1264f55..9a00bfe1c9 100644 --- a/rust/pspp/src/output/pivot/look_xml.rs +++ b/rust/pspp/src/output/pivot/look_xml.rs @@ -23,7 +23,8 @@ use crate::{ format::Decimal, output::pivot::{ Area, AreaStyle, Axis2, Border, BorderStyle, BoxBorder, Color, FootnoteMarkerPosition, - FootnoteMarkerType, HeadingRegion, HorzAlign, LabelPosition, Look, RowColBorder, VertAlign, + FootnoteMarkerType, HeadingRegion, HorzAlign, LabelPosition, Look, RowColBorder, RowParity, + VertAlign, }, }; use thiserror::Error as ThisError; @@ -53,14 +54,14 @@ impl From for Look { 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::Labels(Axis2::X) => table_properties.cell_format_properties.column_labels.style.as_area_style(), - Area::Labels(Axis2::Y) => 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(), + Area::Title => table_properties.cell_format_properties.title.style.as_area_style(RowParity::Even), + Area::Caption => table_properties.cell_format_properties.caption.style.as_area_style(RowParity::Even), + Area::Footer => table_properties.cell_format_properties.footnotes.style.as_area_style(RowParity::Even), + Area::Corner => table_properties.cell_format_properties.corner_labels.style.as_area_style(RowParity::Even), + Area::Labels(Axis2::X) => table_properties.cell_format_properties.column_labels.style.as_area_style(RowParity::Even), + Area::Labels(Axis2::Y) => table_properties.cell_format_properties.row_labels.style.as_area_style(RowParity::Even), + Area::Data(row) => table_properties.cell_format_properties.data.style.as_area_style(row), + Area::Layers => table_properties.cell_format_properties.layers.style.as_area_style(RowParity::Even), }, borders: enum_map! { Border::Title => table_properties.border_properties.title_layer_separator, @@ -202,7 +203,7 @@ struct CellStyle { } impl CellStyle { - fn as_area_style(&self) -> AreaStyle { + fn as_area_style(&self, data_row: RowParity) -> AreaStyle { AreaStyle { cell_style: super::CellStyle { horz_align: match self.text_alignment { @@ -231,14 +232,14 @@ impl CellStyle { 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), - ], + fg: match data_row { + RowParity::Even => self.color.unwrap_or(Color::BLACK), + RowParity::Odd => self.alternating_text_color.unwrap_or(Color::BLACK), + }, + bg: match data_row { + RowParity::Even => self.color2.unwrap_or(Color::BLACK), + RowParity::Odd => self.alternating_color.unwrap_or(Color::BLACK), + }, size: self.font_size.as_pt_i32(), }, } diff --git a/rust/pspp/src/output/pivot/output.rs b/rust/pspp/src/output/pivot/output.rs index 316634a12d..e784063b09 100644 --- a/rust/pspp/src/output/pivot/output.rs +++ b/rust/pspp/src/output/pivot/output.rs @@ -20,7 +20,7 @@ use enum_map::{EnumMap, enum_map}; use itertools::Itertools; use crate::output::{ - pivot::{HeadingRegion, LabelPosition, Path}, + pivot::{HeadingRegion, LabelPosition, Path, RowParity}, table::{CellInner, Table}, }; @@ -206,11 +206,10 @@ impl PivotTable { let value = self.get(&*data_indexes); body.put( Rect2::new(x..x + 1, y..y + 1), - CellInner { - rotate: false, - area: Area::Data, - value: Box::new(value.cloned().unwrap_or_default()), - }, + CellInner::new( + Area::Data(RowParity::from(y - stub[Axis2::Y])), + Box::new(value.cloned().unwrap_or_default()), + ), ); } } @@ -458,18 +457,14 @@ impl<'a> Heading<'a> { for (Range { start: x1, end: x2 }, name) in categories.iter().cloned() { let y1 = v_ofs + row; let y2 = y1 + 1; + + let is_outer_row = y1 == 0; + let is_inner_row = y2 == self.height; + let rotate = + (rotate_inner_labels && is_inner_row) || (rotate_outer_labels && is_outer_row); table.put( Rect2::for_ranges((h, x1 + h_ofs..x2 + h_ofs), y1..y2), - CellInner { - rotate: { - let is_outer_row = y1 == 0; - let is_inner_row = y2 == self.height; - (rotate_inner_labels && is_inner_row) - || (rotate_outer_labels && is_outer_row) - }, - area: Area::Labels(h), - value: Box::new(name.clone()), - }, + CellInner::new(Area::Labels(h), Box::new(name.clone())).with_rotate(rotate), ); // Draw all the vertical lines in our running example, other @@ -540,11 +535,7 @@ impl<'a> Heading<'a> { if dimension_label_position == LabelPosition::Corner { table.put( Rect2::new(v_ofs..v_ofs + 1, 0..h_ofs), - CellInner { - rotate: false, - area: Area::Corner, - value: self.dimension.root.name.clone(), - }, + CellInner::new(Area::Corner, self.dimension.root.name.clone()), ); } } diff --git a/rust/pspp/src/output/pivot/tlo.rs b/rust/pspp/src/output/pivot/tlo.rs index ca5fc629f5..12f474f12c 100644 --- a/rust/pspp/src/output/pivot/tlo.rs +++ b/rust/pspp/src/output/pivot/tlo.rs @@ -103,7 +103,7 @@ impl From for Look { Area::Corner => (&look.pv_text_style.corner).into(), Area::Labels(Axis2::X) => (&look.pv_text_style.column_labels).into(), Area::Labels(Axis2::Y) => (&look.pv_text_style.row_labels).into(), - Area::Data => (&look.pv_text_style.data).into(), + Area::Data(_) => (&look.pv_text_style.data).into(), Area::Layers => (&look.pv_text_style.layers).into(), }, borders: enum_map! { @@ -373,11 +373,8 @@ impl super::AreaStyle { underline: style.underline, markup: false, font: style.font_name.string.clone(), - fg: { - let fg = style.text_color; - [fg, fg] - }, - bg: [bg, bg], + fg: style.text_color, + bg, size: -style.font_size * 3 / 4, }, } diff --git a/rust/pspp/src/output/render.rs b/rust/pspp/src/output/render.rs index ef67425bda..e8f900d971 100644 --- a/rust/pspp/src/output/render.rs +++ b/rust/pspp/src/output/render.rs @@ -151,7 +151,6 @@ pub trait Device { fn draw_cell( &mut self, draw_cell: &DrawCell, - alternate_row: bool, bb: Rect2, valign_offset: usize, spill: EnumMap, @@ -931,8 +930,6 @@ impl Page { usize::saturating_sub(bb[Y].len(), height) } fn draw_cell(&self, device: &mut dyn Device, ofs: Coord2, cell: &RenderCell) { - use Axis2::*; - let mut bb = Rect2::from_fn(|a| { self.cp[a][cell.rect[a].start * 2 + 1]..self.cp[a][cell.rect[a].end * 2] }) @@ -969,17 +966,13 @@ impl Page { bb.clone() }; - // Header rows are never alternate rows. - let alternate_row = - usize::checked_sub(cell.rect[Y].start, self.h[Y]).is_some_and(|row| row % 2 == 1); - let draw_cell = DrawCell::new(cell.content.inner(), &self.table); let valign_offset = match draw_cell.cell_style.vert_align { VertAlign::Top => 0, VertAlign::Middle => self.extra_height(device, &bb, &draw_cell) / 2, VertAlign::Bottom => self.extra_height(device, &bb, &draw_cell), }; - device.draw_cell(&draw_cell, alternate_row, bb, valign_offset, spill, &clip) + device.draw_cell(&draw_cell, bb, valign_offset, spill, &clip) } } diff --git a/rust/pspp/src/output/spv.rs b/rust/pspp/src/output/spv.rs index 0036bdb2aa..b24f506da7 100644 --- a/rust/pspp/src/output/spv.rs +++ b/rust/pspp/src/output/spv.rs @@ -27,7 +27,7 @@ use serde::Deserialize; use zip::{ZipArchive, result::ZipError}; use crate::output::{ - Details, Item, Text, + Details, Item, SpvInfo, SpvMembers, Text, page::PageSetup, pivot::{TableProperties, Value}, spv::light::{LightError, LightTable}, @@ -63,16 +63,12 @@ impl Item { Self::from_spv_reader(File::open(path.as_ref())?) } - fn from_spv_reader(reader: R) -> Result<(Self, Option), Error> + fn from_spv_zip_archive( + archive: &mut ZipArchive, + ) -> Result<(Self, Option), Error> where R: Read + Seek, { - // Open archive. - let mut archive = ZipArchive::new(reader).map_err(|error| match error { - ZipError::InvalidArchive(_) => Error::NotSpv, - other => other.into(), - })?; - // Check manifest. let mut file = archive .by_name("META-INF/MANIFEST.MF") @@ -87,10 +83,9 @@ impl Item { let mut items = Vec::new(); let mut page_setup = None; for i in 0..archive.len() { - let name = archive.name_for_index(i).unwrap(); + let name = String::from(archive.name_for_index(i).unwrap()); if name.starts_with("outputViewer") && name.ends_with(".xml") { - let name = String::from(name); - let (mut new_items, ps) = read_heading(&mut archive, i)?; + let (mut new_items, ps) = read_heading(archive, i, &name)?; items.append(&mut new_items); page_setup = page_setup.or(ps); } @@ -101,16 +96,29 @@ impl Item { page_setup, )) } + + fn from_spv_reader(reader: R) -> Result<(Self, Option), Error> + where + R: Read + Seek, + { + // Open archive. + let mut archive = ZipArchive::new(reader).map_err(|error| match error { + ZipError::InvalidArchive(_) => Error::NotSpv, + other => other.into(), + })?; + Self::from_spv_zip_archive(&mut archive) + } } fn read_heading( archive: &mut ZipArchive, file_number: usize, + structure_member: &str, ) -> Result<(Vec, Option), Error> where R: Read + Seek, { - println!("{}", archive.by_index(file_number)?.name()); + println!("{structure_member}"); let member = BufReader::new(archive.by_index(file_number)?); let mut heading: Heading = match serde_path_to_error::deserialize( &mut quick_xml::de::Deserializer::from_reader(member), @@ -120,7 +128,7 @@ where }; dbg!(&heading); let page_setup = heading.page_setup.take(); - Ok((heading.decode(archive)?, page_setup)) + Ok((heading.decode(archive, structure_member)?, page_setup)) } #[derive(Deserialize, Debug)] @@ -137,7 +145,11 @@ struct Heading { } impl Heading { - fn decode(self, archive: &mut ZipArchive) -> Result, Error> + fn decode( + self, + archive: &mut ZipArchive, + structure_member: &str, + ) -> Result, Error> where R: Read + Seek, { @@ -146,18 +158,24 @@ impl Heading { match child { HeadingContent::Container(container) => { if container.page_break_before { - items.push(Item::new(Details::PageBreak)); + items.push( + Item::new(Details::PageBreak) + .with_spv_info(SpvInfo::new(structure_member)), + ); } match container.content { ContainerContent::Table(table) => { - items.push(table.decode(archive).unwrap() /* XXX*/); + items.push( + table.decode(archive, structure_member).unwrap(), /* XXX*/ + ); } ContainerContent::Text(container_text) => { items.push( Item::new(Details::Text(Box::new(Text::new_log( container_text.decode(), )))) - .with_command_name(container_text.command_name), + .with_command_name(container_text.command_name) + .with_spv_info(SpvInfo::new(structure_member)), ); } } @@ -166,9 +184,14 @@ impl Heading { let show = !heading.visibility.is_some(); items.push( Item::new(Details::Group( - heading.decode(archive)?.into_iter().map_into().collect(), + heading + .decode(archive, structure_member)? + .into_iter() + .map_into() + .collect(), )) - .with_show(show), + .with_show(show) + .with_spv_info(SpvInfo::new(structure_member)), ); } } @@ -253,7 +276,7 @@ struct Table { } impl Table { - fn decode(&self, archive: &mut ZipArchive) -> Result + fn decode(&self, archive: &mut ZipArchive, structure_member: &str) -> Result where R: Read + Seek, { @@ -264,7 +287,12 @@ impl Table { let table = LightTable::read(&mut Cursor::new(data))?; let pivot_table = table.decode()?; println!("{}", &pivot_table); - Ok(Item::new(Details::Table(Box::new(pivot_table)))) + Ok( + Item::new(Details::Table(Box::new(pivot_table))).with_spv_info( + SpvInfo::new(structure_member) + .with_members(SpvMembers::Light(self.table_structure.data_path.clone())), + ), + ) } else { todo!() } diff --git a/rust/pspp/src/output/spv/css.rs b/rust/pspp/src/output/spv/css.rs index 861182fb37..4248f28ae1 100644 --- a/rust/pspp/src/output/spv/css.rs +++ b/rust/pspp/src/output/spv/css.rs @@ -106,7 +106,7 @@ impl FontStyle { match key.as_ref() { "color" => { if let Ok(color) = value.parse() { - self.fg = [color; 2]; + self.fg = color; } } "font-weight" => self.bold = value == "bold", @@ -160,8 +160,8 @@ impl FontStyle { if self.size != base.size { settings.push(format!("font-size: {}", self.size as i64 * 4 / 3)); } - if self.fg[0] != base.fg[0] { - settings.push(format!("color: {}", self.fg[0].display_css())); + if self.fg != base.fg { + settings.push(format!("color: {}", self.fg.display_css())); } settings .is_empty() @@ -248,45 +248,30 @@ xyz'"#, assert_eq!(FontStyle::from_css(""), FontStyle::default()); assert_eq!( FontStyle::from_css(r#"p{color:ff0000}"#), - FontStyle { - fg: [Color::RED; 2], - ..FontStyle::default() - } + FontStyle::default().with_fg(Color::RED) ); assert_eq!( FontStyle::from_css("p {font-weight: bold; text-decoration: underline}"), - FontStyle { - bold: true, - underline: true, - ..FontStyle::default() - } + FontStyle::default().with_bold(true).with_underline(true) ); assert_eq!( FontStyle::from_css("p {font-family: Monospace}"), - FontStyle { - font: String::from("Monospace"), - ..FontStyle::default() - } + FontStyle::default().with_font("Monospace") ); assert_eq!( FontStyle::from_css("p {font-size: 24}"), - FontStyle { - size: 18, - ..FontStyle::default() - } + FontStyle::default().with_size(18) ); assert_eq!( FontStyle::from_css( "" ), - FontStyle { - fg: [Color::RED, Color::RED], - bold: true, - italic: true, - underline: true, - font: String::from("Serif"), - ..FontStyle::default() - } + FontStyle::default() + .with_fg(Color::RED) + .with_bold(true) + .with_italic(true) + .with_underline(true) + .with_font("Serif") ); } diff --git a/rust/pspp/src/output/spv/light.rs b/rust/pspp/src/output/spv/light.rs index cc38c8d8c2..348932137e 100644 --- a/rust/pspp/src/output/spv/light.rs +++ b/rust/pspp/src/output/spv/light.rs @@ -19,7 +19,7 @@ use crate::{ output::pivot::{ self, AreaStyle, Axis2, Axis3, BoxBorder, Color, FootnoteMarkerPosition, FootnoteMarkerType, Footnotes, Group, HeadingRegion, HorzAlign, LabelPosition, Look, - PivotTable, PivotTableMetadata, PivotTableStyle, PrecomputedIndex, RowColBorder, + PivotTable, PivotTableMetadata, PivotTableStyle, PrecomputedIndex, RowColBorder, RowParity, StringValue, Stroke, TemplateValue, ValueStyle, VariableValue, VertAlign, parse_bool, }, settings::Show, @@ -346,10 +346,14 @@ impl Areas { pivot::Area::Corner => 3, pivot::Area::Labels(Axis2::X) => 4, pivot::Area::Labels(Axis2::Y) => 5, - pivot::Area::Data => 6, + pivot::Area::Data(_) => 6, pivot::Area::Layers => 7, }; - self.areas[index].decode(encoding) + let data_row = match area { + pivot::Area::Data(row) => row, + _ => RowParity::default(), + }; + self.areas[index].decode(encoding, data_row) }) } } @@ -399,7 +403,7 @@ struct Area { } impl Area { - fn decode(&self, encoding: &'static Encoding) -> AreaStyle { + fn decode(&self, encoding: &'static Encoding, data_row: RowParity) -> AreaStyle { AreaStyle { cell_style: pivot::CellStyle { horz_align: match self.halign { @@ -424,8 +428,14 @@ impl Area { underline: self.underline, markup: false, font: self.typeface.decode(encoding), - fg: [self.fg, if self.alternate { self.alt_fg } else { self.fg }], - bg: [self.bg, if self.alternate { self.alt_bg } else { self.bg }], + fg: match data_row { + RowParity::Even => self.fg, + RowParity::Odd => self.alt_fg, + }, + bg: match data_row { + RowParity::Even => self.bg, + RowParity::Odd => self.alt_bg, + }, size: (self.size / 1.33) as i32, }, } @@ -1277,8 +1287,8 @@ impl ValueMods { underline: font_style.underline, markup: false, font: font_style.typeface.decode(encoding), - fg: [font_style.fg, font_style.fg], - bg: [font_style.bg, font_style.bg], + fg: font_style.fg, + bg: font_style.bg, size: (font_style.size as i32) * 4 / 3, }); let cell_style = style_pair diff --git a/rust/pspp/src/output/table.rs b/rust/pspp/src/output/table.rs index dfc148390d..5597ae794b 100644 --- a/rust/pspp/src/output/table.rs +++ b/rust/pspp/src/output/table.rs @@ -178,6 +178,10 @@ impl CellInner { } } + pub fn with_rotate(self, rotate: bool) -> Self { + Self { rotate, ..self } + } + pub fn is_empty(&self) -> bool { self.value.inner.is_empty() } diff --git a/rust/pspp/src/show_spv.rs b/rust/pspp/src/show_spv.rs new file mode 100644 index 0000000000..179ef79e97 --- /dev/null +++ b/rust/pspp/src/show_spv.rs @@ -0,0 +1,142 @@ +// PSPP - a program for statistical analysis. +// Copyright (C) 2025 Free Software Foundation, Inc. +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +use anyhow::{Result, anyhow}; +use clap::{Args, ValueEnum}; +use enumset::EnumSet; +use pspp::output::Class; +use std::{fmt::Display, path::PathBuf}; + +/// Show information about SPSS viewer files (SPV files). +#[derive(Args, Clone, Debug)] +pub struct ShowSpv { + /// What to show. + #[arg(value_enum)] + mode: Mode, + + /// File to show. + /// + /// For most modes, this should be a `.spv` file. For `convert-table-look`, + /// this should be a `.tlo` or `.stt` file. + #[arg(required = true)] + input: PathBuf, + + /// Classes of objects to include or, with leading `^`, to exclude. The + /// supported classes are: charts, headings, logs, models, tables, texts, + /// trees, warnings, outlineheaders, pagetitle, notes, unknown, other. + #[arg(long, required = false, value_parser = parse_select, help_heading = "Input selection options")] + select: EnumSet, + + /// Identifiers of commands to include or, with leading `^`, to exclude. + #[arg(long, required = false)] + commands: String, + + /// Table subtypes to include or, with leading `^`, to exclude. + #[arg(long, required = false)] + subtypes: String, + + /// Labels (table titles) to include or, with leading `^`, to exclude. + #[arg(long, required = false)] + labels: String, +} + +fn parse_select(s: &str) -> Result, anyhow::Error> { + if s.is_empty() { + return Ok(EnumSet::all()); + } + let (s, invert) = match s.strip_prefix('^') { + Some(rest) => (rest, true), + None => (s, false), + }; + let mut classes = EnumSet::empty(); + for name in s.split(',') { + if name == "all" { + classes = EnumSet::all(); + } else { + classes.insert( + name.trim() + .parse() + .map_err(|_| anyhow!("unknown output class `{name}`"))?, + ); + } + } + if invert { + classes = !classes; + } + Ok(classes) +} + +/// What to show in a system file. +#[derive(Clone, Copy, Debug, PartialEq, ValueEnum)] +enum Mode { + /// List tables and other items. + #[value(alias = "dir")] + Directory, + + /// Copies first selected TableLook into output in `.stt` format. + GetTableLook, + + /// Reads `.tlo` or `.stt` TableLook and outputs as `.stt` format. + ConvertTableLook, +} + +impl Mode { + fn as_str(&self) -> &'static str { + match self { + Mode::Directory => "directory", + Mode::GetTableLook => "get-table-look", + Mode::ConvertTableLook => "convert-table-look", + } + } +} + +impl Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl ShowSpv { + pub fn run(self) -> Result<()> { + todo!() + } +} + +#[cfg(test)] +mod tests { + use enumset::EnumSet; + + use crate::show_spv::parse_select; + + #[test] + fn test_parse_select() { + assert_eq!(parse_select("").unwrap(), EnumSet::all()); + assert_eq!( + parse_select("tables").unwrap(), + EnumSet::only(pspp::output::Class::Tables) + ); + assert_eq!( + parse_select("tables,pagetitle").unwrap(), + EnumSet::only(pspp::output::Class::Tables) + | EnumSet::only(pspp::output::Class::PageTitle) + ); + assert_eq!( + parse_select("^tables,pagetitle").unwrap(), + !(EnumSet::only(pspp::output::Class::Tables) + | EnumSet::only(pspp::output::Class::PageTitle)) + ); + } +}