support images in spv files
authorBen Pfaff <blp@cs.stanford.edu>
Thu, 16 Oct 2025 23:13:56 +0000 (16:13 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Thu, 16 Oct 2025 23:13:56 +0000 (16:13 -0700)
rust/pspp/src/output.rs
rust/pspp/src/output/drivers/csv.rs
rust/pspp/src/output/drivers/html.rs
rust/pspp/src/output/drivers/spv.rs
rust/pspp/src/output/drivers/text.rs
rust/pspp/src/output/spv.rs

index 9c14affa47cb81df9dad3919eecea5414d76d265..f82ab49db7bd7b3a0e7c3b63f79ea2ba3f6217b7 100644 (file)
@@ -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<Diagnostic>),
     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,
index e9bd5166b4f0d0bbe902e5c6973373f66f8ad1e0..37b57e6c585ccfec46eac9d4a2dad0b7ea4325a2 100644 (file)
@@ -197,7 +197,7 @@ impl Driver for CsvDriver {
     fn write(&mut self, item: &Arc<Item>) {
         // 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();
index a68304d3a740f0cc8c75282695545118ccade007..c6998a65d5118830877837e82ea8dd286fb11f3a 100644 (file)
@@ -427,7 +427,7 @@ where
 
     fn write(&mut self, item: &Arc<Item>) {
         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) => {
index 2983d4f95d5124c4446420f050efc91989df6613..349657137bee3da778cd0efeca7a7248e70c79e4 100644 (file)
@@ -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::<Attribute>::new();
                 if let Some(command_name) = &item.command_name {
index 7ec73eb349608b67f2e4c62921eee779831ad4c7..2091e3c04b894c51d4fb48dfde5040c557c145fc 100644 (file)
@@ -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 {
index db70db40f32170ce917e09111fcd4ba9bd4e7680..39afb651b3dd717944c0f074743774dfd073f3fe 100644 (file)
@@ -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<R>(
+    archive: &mut ZipArchive<R>,
+    structure_member: &str,
+    command_name: &Option<String>,
+    image_name: &str,
+) -> Result<Item, Error>
+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<String>,
+    data_path: String,
+}
+
+impl Image {
+    fn decode<R>(&self, archive: &mut ZipArchive<R>, structure_member: &str) -> Result<Item, Error>
+    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<String>,
+    #[serde(rename = "@uri")]
+    uri: String,
+}
+
+impl Object {
+    fn decode<R>(&self, archive: &mut ZipArchive<R>, structure_member: &str) -> Result<Item, Error>
+    where
+        R: Read + Seek,
+    {
+        decode_image(archive, structure_member, &self.command_name, &self.uri)
+    }
+}
+
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 struct Table {