From e3c37b7c2ac0210aee4bcba0aea79708a2278ee7 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Thu, 16 Oct 2025 16:13:56 -0700 Subject: [PATCH] support images in spv files --- rust/pspp/src/output.rs | 18 +++++-- rust/pspp/src/output/drivers/csv.rs | 2 +- rust/pspp/src/output/drivers/html.rs | 2 +- rust/pspp/src/output/drivers/spv.rs | 2 +- rust/pspp/src/output/drivers/text.rs | 2 +- rust/pspp/src/output/spv.rs | 77 +++++++++++++++++++++++++--- 6 files changed, 88 insertions(+), 15 deletions(-) diff --git a/rust/pspp/src/output.rs b/rust/pspp/src/output.rs index 9c14affa47..f82ab49db7 100644 --- a/rust/pspp/src/output.rs +++ b/rust/pspp/src/output.rs @@ -27,6 +27,7 @@ use std::{ use anyhow::anyhow; use bit_vec::BitVec; +use cairo::ImageSurface; use clap::{ArgAction, ArgMatches, Args, FromArgMatches, value_parser}; use enum_map::EnumMap; use enumset::{EnumSet, EnumSetType}; @@ -212,7 +213,7 @@ impl Display for ItemKind { #[derive(Clone, Debug, Serialize)] pub enum Details { Chart, - Image, + Image(#[serde(skip_serializing)] ImageSurface), Heading(Heading), Message(Box), PageBreak, @@ -260,10 +261,17 @@ impl Details { } } + pub fn as_image(&self) -> Option<&ImageSurface> { + match self { + Self::Image(image_surface) => Some(image_surface), + _ => None, + } + } + pub fn command_name(&self) -> Option<&String> { match self { Details::Chart - | Details::Image + | Details::Image(_) | Details::Heading(_) | Details::Message(_) | Details::PageBreak @@ -275,7 +283,7 @@ impl Details { pub fn label(&self) -> Cow<'static, str> { match self { Details::Chart => Cow::from("chart"), - Details::Image => Cow::from("Image"), + Details::Image(_) => Cow::from("Image"), Details::Heading(_) => Cow::from("Group"), Details::Message(diagnostic) => Cow::from(diagnostic.severity.as_title_str()), Details::PageBreak => Cow::from("Page Break"), @@ -307,7 +315,7 @@ impl Details { pub fn kind(&self) -> ItemKind { match self { Details::Chart => ItemKind::Chart, - Details::Image => ItemKind::Image, + Details::Image(_) => ItemKind::Image, Details::Heading(_) => ItemKind::Heading, Details::Message(_) => ItemKind::Message, Details::PageBreak => ItemKind::PageBreak, @@ -644,7 +652,7 @@ impl Item { let label = self.label.as_ref().map(|s| s.as_str()); match &self.details { Details::Chart => Class::Charts, - Details::Image => Class::Other, + Details::Image(_) => Class::Other, Details::Heading(_) => Class::OutlineHeaders, Details::Message(diagnostic) => match diagnostic.severity { Severity::Note => Class::Notes, diff --git a/rust/pspp/src/output/drivers/csv.rs b/rust/pspp/src/output/drivers/csv.rs index e9bd5166b4..37b57e6c58 100644 --- a/rust/pspp/src/output/drivers/csv.rs +++ b/rust/pspp/src/output/drivers/csv.rs @@ -197,7 +197,7 @@ impl Driver for CsvDriver { fn write(&mut self, item: &Arc) { // todo: error handling (should not unwrap) match &item.details { - Details::Chart | Details::Image | Details::Heading(_) => (), + Details::Chart | Details::Image(_) | Details::Heading(_) => (), Details::Message(diagnostic) => { self.start_item(); let text = diagnostic.to_string(); diff --git a/rust/pspp/src/output/drivers/html.rs b/rust/pspp/src/output/drivers/html.rs index a68304d3a7..c6998a65d5 100644 --- a/rust/pspp/src/output/drivers/html.rs +++ b/rust/pspp/src/output/drivers/html.rs @@ -427,7 +427,7 @@ where fn write(&mut self, item: &Arc) { match &item.details { - Details::Chart | Details::Image | Details::Heading(_) => todo!(), + Details::Chart | Details::Image(_) | Details::Heading(_) => todo!(), Details::Message(_diagnostic) => todo!(), Details::PageBreak => (), Details::Table(pivot_table) => { diff --git a/rust/pspp/src/output/drivers/spv.rs b/rust/pspp/src/output/drivers/spv.rs index 2983d4f95d..349657137b 100644 --- a/rust/pspp/src/output/drivers/spv.rs +++ b/rust/pspp/src/output/drivers/spv.rs @@ -190,7 +190,7 @@ where X: Write, { match &item.details { - Details::Chart | Details::Image => todo!(), + Details::Chart | Details::Image(_) => todo!(), Details::Heading(children) => { let mut attributes = Vec::::new(); if let Some(command_name) = &item.command_name { diff --git a/rust/pspp/src/output/drivers/text.rs b/rust/pspp/src/output/drivers/text.rs index 7ec73eb349..2091e3c04b 100644 --- a/rust/pspp/src/output/drivers/text.rs +++ b/rust/pspp/src/output/drivers/text.rs @@ -382,7 +382,7 @@ impl TextRenderer { W: FmtWrite, { match &item.details { - Details::Chart | Details::Image => todo!(), + Details::Chart | Details::Image(_) => todo!(), Details::Heading(children) => { for (index, child) in children.0.iter().enumerate() { if index > 0 { diff --git a/rust/pspp/src/output/spv.rs b/rust/pspp/src/output/spv.rs index db70db40f3..39afb651b3 100644 --- a/rust/pspp/src/output/spv.rs +++ b/rust/pspp/src/output/spv.rs @@ -22,6 +22,7 @@ use std::{ use anyhow::Context; use binrw::{BinRead, error::ContextExt}; +use cairo::ImageSurface; use displaydoc::Display; use serde::Deserialize; use zip::{ZipArchive, result::ZipError}; @@ -56,6 +57,9 @@ pub enum Error { /// {0} LightError(#[from] LightError), + + /// {0} + CairoError(#[from] cairo::IoError), } impl Item { @@ -183,12 +187,14 @@ impl Heading { .into_item() .with_command_name(container_text.command_name) .with_spv_info(SpvInfo::new(structure_member)), + ContainerContent::Image(image) => { + image.decode(archive, structure_member).unwrap() + } /*XXX*/, + ContainerContent::Object(object) => { + object.decode(archive, structure_member).unwrap() + } /*XXX*/, ContainerContent::Model => new_error_item("models not yet implemented") .with_spv_info(SpvInfo::new(structure_member).with_error()), - ContainerContent::Object => new_error_item("objects not yet implemented") - .with_spv_info(SpvInfo::new(structure_member).with_error()), - ContainerContent::Image => new_error_item("images not yet implemented") - .with_spv_info(SpvInfo::new(structure_member).with_error()), ContainerContent::Tree => new_error_item("trees not yet implemented") .with_spv_info(SpvInfo::new(structure_member).with_error()), }; @@ -282,8 +288,8 @@ enum ContainerContent { Text(ContainerText), Graph(Graph), Model, - Object, - Image, + Object(Object), + Image(Image), Tree, } @@ -311,6 +317,65 @@ impl Graph { } } +fn decode_image( + archive: &mut ZipArchive, + structure_member: &str, + command_name: &Option, + image_name: &str, +) -> Result +where + R: Read + Seek, +{ + let mut png = archive.by_name(image_name)?; + let image = ImageSurface::create_from_png(&mut png)?; + Ok(Details::Image(image) + .into_item() + .with_command_name(command_name.clone()) + .with_spv_info( + SpvInfo::new(structure_member).with_members(SpvMembers::Image(image_name.into())), + )) +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Image { + #[serde(rename = "@commandName")] + command_name: Option, + data_path: String, +} + +impl Image { + fn decode(&self, archive: &mut ZipArchive, structure_member: &str) -> Result + where + R: Read + Seek, + { + decode_image( + archive, + structure_member, + &self.command_name, + &self.data_path, + ) + } +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Object { + #[serde(rename = "@commandName")] + command_name: Option, + #[serde(rename = "@uri")] + uri: String, +} + +impl Object { + fn decode(&self, archive: &mut ZipArchive, structure_member: &str) -> Result + where + R: Read + Seek, + { + decode_image(archive, structure_member, &self.command_name, &self.uri) + } +} + #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct Table { -- 2.30.2