SVG import: name what was left out instead of dropping it silently (#627) (#1016)

Text, raster images, <use> copies, <style> sheets and rounded rectangle
corners aren't read yet. They were dropped without a word; the reader now
counts them and adds one read note (the channel DWG reading already uses),
which OPEN echoes and returns as a warning. Content that isn't drawn (in
<defs> or hidden) isn't counted.
This commit is contained in:
Gert Pellin - GPE
2026-10-19 02:28:29 -04:00
committed by GitHub
parent 0cc66ad306
commit baa9c00fb7
4 changed files with 97 additions and 6 deletions
+73 -4
View File
@@ -10,9 +10,10 @@
//! Not read (each is a follow-up): CSS class rules (`<style>`), `use` references, fills,
//! gradients and hatch patterns, text, embedded raster images, rounded rectangle corners
//! (`rx`/`ry`), and elliptical arcs under non-uniform transforms (those are flattened into
//! line segments instead of losing the shape).
//! line segments instead of losing the shape). What the drawing shows of those (all but fills,
//! gradients and patterns) is named in a read note, so it is never left out silently.
use std::collections::HashSet;
use std::collections::{BTreeMap, HashSet};
use cadcraft_color::Color;
use cadcraft_doc::*;
@@ -127,6 +128,40 @@ struct Parser {
elements: usize,
/// > 0 while inside a skipped subtree; counts the starts seen there.
skip: usize,
/// What was left out, by kind ([`LeftOut`]).
left_out: BTreeMap<LeftOut, usize>,
}
/// Drawn content the reader leaves out, named in the drawing's read note.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum LeftOut {
Text,
Image,
Use,
Style,
RoundedCorners,
}
impl LeftOut {
/// The kind of a skipped element ([`SKIP`]) that the drawing shows.
fn of(name: &[u8]) -> Option<Self> {
match name {
b"text" => Some(Self::Text),
b"image" => Some(Self::Image),
_ => None,
}
}
fn describe(self, n: usize) -> String {
let (one, many) = match self {
Self::Text => ("text", "texts"),
Self::Image => ("raster image", "raster images"),
Self::Use => ("`<use>` copy", "`<use>` copies"),
Self::Style => ("`<style>` sheet (its class rules aren't applied)", "`<style>` sheets (their class rules aren't applied)"),
Self::RoundedCorners => ("rectangle with rounded corners (drawn square)", "rectangles with rounded corners (drawn square)"),
};
format!("{n} {}", if n == 1 { one } else { many })
}
}
impl Parser {
@@ -139,9 +174,23 @@ impl Parser {
layer_color: 1,
elements: 0,
skip: 0,
left_out: BTreeMap::new(),
}
}
fn left_out(&mut self, kind: LeftOut) {
*self.left_out.entry(kind).or_default() += 1;
}
/// The read note on what was left out, if anything was.
fn note(&self) -> Option<String> {
if self.left_out.is_empty() {
return None;
}
let list: Vec<String> = self.left_out.iter().map(|(k, n)| k.describe(*n)).collect();
Some(format!("SVG: CADCraft doesn't read these yet, so they were left out: {}", list.join(", ")))
}
fn parse(&mut self, text: &str) -> Result<()> {
let mut reader = Reader::from_str(text);
reader.config_mut().trim_text(true);
@@ -163,21 +212,29 @@ impl Parser {
_ => {}
}
}
self.drawing.read_notes.extend(self.note());
Ok(())
}
/// A start tag: open a transform frame, descend into a skipped subtree, or treat a
/// shape written as start+end the same as a self-closed one.
fn on_start(&mut self, name: &[u8], e: &BytesStart) -> Result<()> {
// A style sheet applies wherever it is (Inkscape keeps it in `<defs>`).
if name == b"style" {
self.left_out(LeftOut::Style);
}
if self.skip > 0 {
self.skip += 1;
return Ok(());
}
if SKIP.contains(&name) {
if hidden(e) {
self.skip = 1;
return Ok(());
}
if hidden(e) {
if SKIP.contains(&name) {
if let Some(kind) = LeftOut::of(name) {
self.left_out(kind);
}
self.skip = 1;
return Ok(());
}
@@ -189,9 +246,18 @@ impl Parser {
return Ok(());
}
if SKIP.contains(&name) || hidden(e) {
if !hidden(e)
&& let Some(kind) = LeftOut::of(name)
{
self.left_out(kind);
}
return Ok(());
}
match name {
b"use" => {
self.left_out(LeftOut::Use);
Ok(())
}
b"rect" => self.rect(e),
b"circle" => self.circle(e),
b"ellipse" => self.ellipse(e),
@@ -279,6 +345,9 @@ impl Parser {
if w <= 0.0 || h <= 0.0 {
return Ok(());
}
if num(e, b"rx", 0.0) > 0.0 || num(e, b"ry", 0.0) > 0.0 {
self.left_out(LeftOut::RoundedCorners);
}
let pts = [Vec2::new(x, y), Vec2::new(x + w, y), Vec2::new(x + w, y + h), Vec2::new(x, y + h)];
self.add_polyline(&pts.map(|p| self.map(p)), true)
}
+22
View File
@@ -2120,6 +2120,28 @@ fn svg_hidden_and_defs_subtrees_are_skipped() {
assert_eq!(kinds(&d), ["Polyline"]);
}
/// #627: what the reader doesn't read yet (text, raster images, `<use>` copies, style sheets,
/// rounded corners) is named in a read note instead of being left out silently; what isn't drawn
/// (in `<defs>` or hidden) isn't counted.
#[test]
fn svg_left_out_content_is_noted() {
let svg = format!(
"{SVG_PAGE}<defs><style>.a {{ stroke: red }}</style><text>in defs</text><rect id=\"r\" width=\"1\" height=\"1\"/></defs>\
<text x=\"0\" y=\"5\">one</text><text x=\"0\" y=\"8\"><tspan>two</tspan></text><text display=\"none\">hidden</text>\
<image href=\"a.png\" width=\"2\" height=\"2\"/><use href=\"#r\"/><use href=\"#r\" x=\"3\"></use>\
<rect x=\"1\" y=\"1\" width=\"4\" height=\"3\" rx=\"1\"/><rect x=\"6\" y=\"1\" width=\"4\" height=\"3\"/></svg>"
);
let d = read(svg.as_bytes(), "left-out.svg").unwrap();
assert_eq!(kinds(&d), ["Polyline", "Polyline"]);
assert_eq!(
d.read_notes,
["SVG: CADCraft doesn't read these yet, so they were left out: 2 texts, 1 raster image, 2 `<use>` copies, \
1 `<style>` sheet (its class rules aren't applied), 1 rectangle with rounded corners (drawn square)"]
);
let plain = read(format!("{SVG_PAGE}<rect x=\"1\" y=\"1\" width=\"4\" height=\"3\"/></svg>").as_bytes(), "a.svg").unwrap();
assert!(plain.read_notes.is_empty(), "{:?}", plain.read_notes);
}
#[test]
fn svgz_gunzips_to_the_same_drawing() {
let svg = format!("{SVG_PAGE}<rect x=\"1\" y=\"1\" width=\"4\" height=\"3\"/></svg>");
+1 -1
View File
@@ -117,7 +117,7 @@ early (#175), but the throughput problem remains. **DWG ≈ 30% read, ≈ 15% wr
| Plot styles (`.ctb`, `.stb`), plotter configs (`.pc3`), `.pmp` | yes | plot style tables in our own JSON format ([plot-styles.md](plot-styles.md)) and kept in the drawing; plotter devices with papers and printable areas in our own JSON format ([plotters.md](plotters.md), #564); binary `.ctb`/`.stb`/`.pc3`/`.pmp` not read (no public spec) | 4–8 |
| Scripts (`.scr`) | SCRIPT | script text through the CLI, MCP and control channel; no SCRIPT command reading a `.scr` file | 1–2 |
| Aliases (`.pgp`), CUI(x), workspaces | yes | user alias files and ALIASEDIT (#552); no CUI(x) or workspaces | 4–8 |
| SVG import (`.svg`, `.svgz`) | yes (via scripts) | OPEN reads shapes, paths, groups and Inkscape layers into entities (#636); no CSS classes, `use`, fills, text or images | 2–4 |
| SVG import (`.svg`, `.svgz`) | yes (via scripts) | OPEN reads shapes, paths, groups and Inkscape layers into entities (#636); no CSS classes, `use`, fills, text or images (what the drawing loses is named in a note on open, #627) | 2–4 |
| AutoLISP (`.lsp`, `.fas`, `.vlx`), `.dll`/`.arx` plug-ins | yes | `.lsp` loads (APPLOAD, `(load)`, a dropped file; #758); no Visual LISP ActiveX (`vlax-`), DCL dialogs or reactors; `.fas`/`.vlx` are compiled and undocumented; no `.dll`/`.arx` | see [gaps.md](gaps.md) (E1) |
| Data extraction (`.csv`, `.xls`) and data links | yes | DATAEXTRACTION/EATTEXT to CSV or a table (#433); table data links read CSV (#534); no `.xls`, no `.dxe` templates | 3–5 |
| eTransmit (`.zip` packages) | yes | no | 4–6 |
+1 -1
View File
@@ -81,7 +81,7 @@ Detail: [file-format-parity.md](file-format-parity.md).
| FF11 | DWF/DWFx, DGN, WMF, EPS, ACIS/STL and other exchange formats | Low-medium | 50–100 |
| FF12 | DWG in the web build | Medium | 6–10 |
| FF13 | R12 binary DXF, pre-R13 DWG | Low | 10–20 |
| FF14 | SVG import leftovers (shapes, paths, groups and Inkscape layers landed, #636): no CSS classes (`<style>`), `use` references, fills and gradients, text or embedded images; rounded rectangle corners (`rx`/`ry`) are ignored | Low-medium | 2–4 |
| FF14 | SVG import leftovers (shapes, paths, groups and Inkscape layers landed, #636): no CSS classes (`<style>`), `use` references, fills and gradients, text or embedded images; rounded rectangle corners (`rx`/`ry`) are ignored. Opening such a file names what was left out (#627) | Low-medium | 2–4 |
## Hardware