Repository: vega/vl-convert (language: python)

Issue:
Image export crash when font string includes quoted font family
When a font specification includes a font family quoted in double quotes, image export crashes the Python kernel.

```python
import vl_convert as vlc
spec = {
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "layer": [
    {
      "data": {
        "name": "df",
        "values": [{"a": 1, "b": 1}, {"a": 2, "b": 3}, {"a": 3, "b": 2}]
      },
      "layer": [
        {
          "transform": [],
          "layer": [
            {
              "mark": {"type": "line"},
            }
          ],
          "encoding": {
            "x": {
              "field": "a",
              "type": "quantitative"
            },
            "y": {
              "field": "b",
              "type": "quantitative"
            },
          }
        }
      ]
    }
  ],
  "config": {
    "style": {
      "guide-label": {
        "font": "Arial, sans-serif"
      },
      "guide-title": {
        "font": "Arial, sans-serif"
      }
    }
  }
}

# Works ok
svg = vlc.vegalite_to_svg(spec)

# Update font strings to quote Arial. Crashes kernel
spec["config"]["style"]["guide-label"]["font"] = "\"Arial\", sans-serif"
spec["config"]["style"]["guide-label"]["font"] = "\"Arial\", sans-serif"
svg = vlc.vegalite_to_svg(spec)
```

A little more info in the stack trace is available when running the example in Rust:

```
thread '<unnamed>' panicked at 'Failed to parse text SVG: ParsingFailed(ParserError(InvalidAttribute(InvalidSpace(73, TextPos { row: 3, col: 54 }), TextPos { row: 3, col: 54 })))', vl-convert-rs/src/text.rs:187:59
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fatal runtime error: failed to initiate panic, error 5
Fatal Python error: Aborted
```

The first thing we should do is update error handling to not panic and crash the kernel on SVG parse failure.  Then we should fix the bug 😄 


Discussion / hints:

Fix patch:
diff --git a/vl-convert-rs/src/text.rs b/vl-convert-rs/src/text.rs
index b1f3f0f0..6e3631fb 100644
--- a/vl-convert-rs/src/text.rs
+++ b/vl-convert-rs/src/text.rs
@@ -99,6 +99,8 @@ impl TextInfo {
         text_attrs.push(format!("font-size=\"{}\"", self.size));
 
         if let Some(family) = &self.family {
+            // Remove quotes since usvg can't handle them
+            let family = family.replace(['"', '\''], "");
             text_attrs.push(format!("font-family=\"{}\"", family));
         }
 


Test patch:
diff --git a/vl-convert-rs/tests/test_specs.rs b/vl-convert-rs/tests/test_specs.rs
index 88dc982d..832addc5 100644
--- a/vl-convert-rs/tests/test_specs.rs
+++ b/vl-convert-rs/tests/test_specs.rs
@@ -2,9 +2,10 @@ use rstest::rstest;
 use std::fs;
 use std::path::{Path, PathBuf};
 use vl_convert_rs::text::register_font_directory;
-use vl_convert_rs::VlVersion;
+use vl_convert_rs::{VlConverter, VlVersion};
 
 use std::sync::Once;
+use vl_convert_rs::converter::VlOpts;
 
 static INIT: Once = Once::new();
 const BACKGROUND_COLOR: &str = "#abc";
@@ -286,11 +287,44 @@ mod test_png_theme_config {
         ).unwrap();
         assert_eq!(png_data, expected_png_data);
 
-        // Write out reference image
-        let png_path = make_expected_png_path(name, vl_version, Some(theme));
-        std::fs::write(png_path, png_data).unwrap();
+        // // Write out reference image
+        // let png_path = make_expected_png_path(name, vl_version, Some(theme));
+        // std::fs::write(png_path, png_data).unwrap();
     }
 
     #[test]
     fn test_marker() {} // Help IDE detect test module
 }
+
+#[tokio::test]
+async fn test_font_with_quotes() {
+    let vl_version = VlVersion::v5_5;
+
+    // Load example Vega-Lite spec
+    let name = "font_with_quotes";
+    let vl_spec = load_vl_spec(name);
+
+    // Create Vega-Lite Converter and perform conversion
+    let mut converter = VlConverter::new();
+
+    // Load expected PNG image
+    let expected_png_data = load_expected_png(name, vl_version, None);
+
+    let png_data = converter
+        .vegalite_to_png(
+            vl_spec,
+            VlOpts {
+                vl_version,
+                ..Default::default()
+            },
+            Some(2.0),
+        )
+        .await
+        .unwrap();
+
+    assert_eq!(png_data, expected_png_data);
+
+    // // Write out reference image
+    // let png_path = make_expected_png_path(name, vl_version, None);
+    // std::fs::write(png_path, png_data).unwrap();
+}
diff --git a/vl-convert-rs/tests/vl-specs/expected/v5_5/font_with_quotes.png b/vl-convert-rs/tests/vl-specs/expected/v5_5/font_with_quotes.png
new file mode 100644
index 00000000..ac03bb36
Binary files /dev/null and b/vl-convert-rs/tests/vl-specs/expected/v5_5/font_with_quotes.png differ
diff --git a/vl-convert-rs/tests/vl-specs/font_with_quotes.vl.json b/vl-convert-rs/tests/vl-specs/font_with_quotes.vl.json
new file mode 100644
index 00000000..729a3b50
--- /dev/null
+++ b/vl-convert-rs/tests/vl-specs/font_with_quotes.vl.json
@@ -0,0 +1,27 @@
+{
+  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
+  "layer": [
+    {
+      "data": {
+        "name": "df",
+        "values": [{"a": 1, "b": 1}, {"a": 2, "b": 3}, {"a": 3, "b": 2}]
+      },
+      "layer": [
+        {
+          "transform": [],
+          "layer": [{"mark": {"type": "line"}}],
+          "encoding": {
+            "x": {"field": "a", "type": "quantitative"},
+            "y": {"field": "b", "type": "quantitative"}
+          }
+        }
+      ]
+    }
+  ],
+  "config": {
+    "style": {
+      "guide-label": {"font": "\"Liberation Sans\", sans-serif"},
+      "guide-title": {"font": "\"Liberation Sans\", sans-serif"}
+    }
+  }
+}
\ No newline at end of file