mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 20:36:02 +00:00
Merge pull request #7436 from dotCooCoo/fix/svg-masked-shadow
SVG export: shadows in a masked group show in readers that bound it by its shapes
This commit is contained in:
@@ -431,11 +431,75 @@ impl Writer<'_> {
|
||||
self.def(1, "</mask>");
|
||||
self.line(&format!("<g mask=\"url(#{mid})\">"));
|
||||
self.depth += 1;
|
||||
self.spanning_rect(&bare, ®ion);
|
||||
self.node_body(&bare);
|
||||
self.depth -= 1;
|
||||
self.line("</g>");
|
||||
}
|
||||
|
||||
/// Some readers draw a masked group only inside the bounding box of its shapes, which leaves
|
||||
/// out a drop shadow, blur, glow or feather that paints past them. When `n`, written inside a
|
||||
/// `<g mask>`, holds one ([`Self::holds_raster`]), a hidden, unpainted rectangle over the
|
||||
/// mask's `region` makes that box cover what it paints.
|
||||
fn spanning_rect(&mut self, n: &Node, region: &str) {
|
||||
if self.holds_raster(n) {
|
||||
self.line(&format!("<rect {region} fill=\"none\" visibility=\"hidden\"/>"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Do `n`, its fills and strokes, its members at any depth, its symbols' art or the art its
|
||||
/// live objects are written as have a visible raster effect (shadow, glow, blur, feather)?
|
||||
/// Objects that aren't written (hidden ones, unless the options keep them) are skipped, and so
|
||||
/// are fills and strokes that [`Self::shape`] doesn't write (hidden, unpainted or zero-width).
|
||||
/// An instance with overrides is looked at in the art it is written as
|
||||
/// ([`vectorcraft_brush::instance_art`]). The walk keeps its own stack, and looks at each
|
||||
/// symbol's art once, and at each instance's overridden art once.
|
||||
fn holds_raster(&self, n: &Node) -> bool {
|
||||
let mut stack: Vec<std::sync::Arc<Node>> = vec![];
|
||||
let mut symbols: std::collections::HashSet<(String, Option<vectorcraft_doc::NodeId>)> = std::collections::HashSet::new();
|
||||
let paints = |i: &AppearanceItem| match i {
|
||||
AppearanceItem::Fill(f) => f.visible && !f.paint.is_none(),
|
||||
AppearanceItem::Stroke(s) => s.visible && !s.paint.is_none() && s.width > 0.0,
|
||||
};
|
||||
// Whether `n` itself has one; else its members, symbol art or expanded art go on the stack.
|
||||
let mut visit = |n: &Node, stack: &mut Vec<std::sync::Arc<Node>>| {
|
||||
if !(n.visible || self.opts.hidden_layers) {
|
||||
return false;
|
||||
}
|
||||
if has_raster(&n.appearance.effects) || n.appearance.items.iter().any(|i| paints(i) && has_raster(i.effects())) {
|
||||
return true;
|
||||
}
|
||||
// A live object is written as its expanded art ([`Self::node_body`]), which holds no
|
||||
// live objects.
|
||||
if vectorcraft_doc::live::is_live(n) {
|
||||
let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n);
|
||||
if !vectorcraft_doc::live::is_live(&g) {
|
||||
stack.push(std::sync::Arc::new(g));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind {
|
||||
let custom = !n.extras().symbol_overrides.is_empty();
|
||||
if symbols.insert((symbol.clone(), custom.then_some(n.id)))
|
||||
&& let Some(s) = self.doc.symbols.iter().find(|s| s.name == *symbol)
|
||||
{
|
||||
stack.push(if custom { std::sync::Arc::new(vectorcraft_brush::instance_art(&s.art, n)) } else { s.art.clone() });
|
||||
}
|
||||
}
|
||||
stack.extend(n.children().into_iter().flatten().cloned());
|
||||
false
|
||||
};
|
||||
if visit(n, &mut stack) {
|
||||
return true;
|
||||
}
|
||||
while let Some(c) = stack.pop() {
|
||||
if visit(&c, &mut stack) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// What `write` writes to the body, taken out of it (written at `depth`, for `<defs>`).
|
||||
fn detached(&mut self, depth: usize, write: impl FnOnce(&mut Self)) -> String {
|
||||
let (body, saved) = (std::mem::take(&mut self.body), self.depth);
|
||||
@@ -664,7 +728,7 @@ impl Writer<'_> {
|
||||
}
|
||||
}
|
||||
self.symbol_nodes = after;
|
||||
for (above, (_, body)) in above.iter().zip(written) {
|
||||
for ((c, above), (_, body)) in elements.iter().zip(&above).zip(written) {
|
||||
if above.is_empty() {
|
||||
self.body.push_str(&body);
|
||||
continue;
|
||||
@@ -672,6 +736,11 @@ impl Writer<'_> {
|
||||
let ids: Vec<&str> = above.iter().filter_map(|&j| shapes[j].as_deref()).collect();
|
||||
let mid = self.knockout_mask(&ids, ®ion);
|
||||
self.line(&format!("<g mask=\"url(#{mid})\">"));
|
||||
// An element that paints past its shapes starts its mask group with a spanning
|
||||
// rectangle (`Self::spanning_rect`).
|
||||
self.depth += 1;
|
||||
self.spanning_rect(c, ®ion);
|
||||
self.depth -= 1;
|
||||
self.body.push_str(&body);
|
||||
self.line("</g>");
|
||||
}
|
||||
|
||||
@@ -202,6 +202,212 @@ fn a_mask_region_keeps_the_shadows_of_the_masked_art() {
|
||||
}
|
||||
}
|
||||
|
||||
fn drop_shadow() -> vectorcraft_doc::Effect {
|
||||
vectorcraft_doc::Effect {
|
||||
id: "stylize.dropShadow".into(),
|
||||
params: serde_json::json!({"x": 15, "y": 15, "blur": 0, "opacity": 100}),
|
||||
visible: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether every `<g mask>` in `svg` starts with a hidden, unpainted rectangle over its mask's region,
|
||||
/// and whether any does.
|
||||
fn spanning_rects(svg: &str) -> (bool, bool) {
|
||||
let spans = masked_group_spans(svg);
|
||||
(spans.iter().all(|s| *s), spans.iter().any(|s| *s))
|
||||
}
|
||||
|
||||
/// For each `<g mask>` in `svg`, in document order, whether it starts with a hidden, unpainted
|
||||
/// rectangle over its mask's region.
|
||||
fn masked_group_spans(svg: &str) -> Vec<bool> {
|
||||
let xml = roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() }).unwrap();
|
||||
let spans: Vec<bool> = xml
|
||||
.descendants()
|
||||
.filter(|n| n.has_tag_name("g") && n.has_attribute("mask"))
|
||||
.map(|group| {
|
||||
let id = group.attribute("mask").unwrap().trim_start_matches("url(#").trim_end_matches(')');
|
||||
let mask = xml.descendants().find(|n| n.has_tag_name("mask") && n.attribute("id") == Some(id)).unwrap();
|
||||
let first = group.children().find(|n| n.is_element()).unwrap();
|
||||
first.has_tag_name("rect")
|
||||
&& first.attribute("fill") == Some("none")
|
||||
&& first.attribute("visibility") == Some("hidden")
|
||||
&& ["x", "y", "width", "height"].iter().all(|a| first.attribute(*a).is_some() && first.attribute(*a) == mask.attribute(*a))
|
||||
})
|
||||
.collect();
|
||||
assert!(!spans.is_empty(), "no masked group in {svg}");
|
||||
spans
|
||||
}
|
||||
|
||||
/// The number of paths `svg` imports as.
|
||||
fn imported_paths(svg: &str) -> usize {
|
||||
let (back, warnings) = vectorcraft_svg::import_with_report(svg).unwrap();
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
let mut paths = 0;
|
||||
back.walk(|n| paths += usize::from(n.path_data().is_some()));
|
||||
paths
|
||||
}
|
||||
|
||||
/// A masked group whose content holds a raster effect (a drop shadow, blur, glow or feather, which
|
||||
/// paint past the shapes) starts with a hidden, unpainted rectangle over the mask's region, so that
|
||||
/// a reader that draws a masked group only inside the box of its shapes still draws it. Art without
|
||||
/// one gets none, brushed strokes (written as shapes) included, nor does an object whose own shadow
|
||||
/// is drawn outside the mask. Import leaves the rectangle out.
|
||||
#[test]
|
||||
fn a_masked_group_spans_its_region_when_its_art_paints_past_its_shapes() {
|
||||
// How the red square sits under the mask.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Case {
|
||||
Alone,
|
||||
InGroup,
|
||||
InNestedGroup,
|
||||
FillEffect,
|
||||
HiddenFill,
|
||||
UnpaintedStroke,
|
||||
HiddenInGroup,
|
||||
Brushed,
|
||||
InSymbol,
|
||||
OverriddenSymbol,
|
||||
InBlend,
|
||||
}
|
||||
use Case::*;
|
||||
for (case, shadowed, want) in [
|
||||
(InGroup, true, true),
|
||||
(InGroup, false, false),
|
||||
(Alone, true, false),
|
||||
(Alone, false, false),
|
||||
(InNestedGroup, true, true),
|
||||
(FillEffect, true, true),
|
||||
(FillEffect, false, false),
|
||||
// A fill or stroke that isn't written doesn't count.
|
||||
(HiddenFill, true, false),
|
||||
(UnpaintedStroke, true, false),
|
||||
(HiddenInGroup, true, false),
|
||||
(Brushed, false, false),
|
||||
(Brushed, true, true),
|
||||
(InSymbol, true, true),
|
||||
(InSymbol, false, false),
|
||||
// An unpainted fill in a dynamic symbol's art that the instance paints counts.
|
||||
(OverriddenSymbol, true, true),
|
||||
(OverriddenSymbol, false, false),
|
||||
(InBlend, true, true),
|
||||
(InBlend, false, false),
|
||||
] {
|
||||
let mut d = Document::new(100.0, 60.0);
|
||||
let mut red = rect(&mut d, Rect::new(10.0, 10.0, 30.0, 30.0), [255, 0, 0]);
|
||||
if matches!(case, Brushed) {
|
||||
let mut stroke = vectorcraft_doc::StrokeLayer::new(Paint::solid(Color::rgb8(0, 0, 200)), 8.0);
|
||||
stroke.brush = Some("Bristle Round".into());
|
||||
red.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(stroke));
|
||||
}
|
||||
if matches!(case, UnpaintedStroke) {
|
||||
red.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(vectorcraft_doc::StrokeLayer::new(Paint::None, 8.0)));
|
||||
}
|
||||
if shadowed {
|
||||
match case {
|
||||
FillEffect | HiddenFill | OverriddenSymbol => red.appearance.items[0].effects_mut().push(drop_shadow()),
|
||||
UnpaintedStroke => red.appearance.items.last_mut().unwrap().effects_mut().push(drop_shadow()),
|
||||
_ => red.appearance.effects.push(drop_shadow()),
|
||||
}
|
||||
}
|
||||
if matches!(case, HiddenFill)
|
||||
&& let vectorcraft_doc::AppearanceItem::Fill(f) = &mut red.appearance.items[0]
|
||||
{
|
||||
f.visible = false;
|
||||
}
|
||||
if matches!(case, OverriddenSymbol) {
|
||||
red.appearance.set_fill(Paint::None);
|
||||
}
|
||||
let group = |d: &mut Document, n: Node| Node::group(d.alloc_id(), vec![n.into()]);
|
||||
let mut masked = match case {
|
||||
Alone | FillEffect => red,
|
||||
HiddenFill | UnpaintedStroke => group(&mut d, red),
|
||||
InGroup | Brushed => group(&mut d, red),
|
||||
InNestedGroup => {
|
||||
let inner = group(&mut d, red);
|
||||
group(&mut d, inner)
|
||||
}
|
||||
HiddenInGroup => {
|
||||
red.visible = false;
|
||||
group(&mut d, red)
|
||||
}
|
||||
InSymbol => {
|
||||
d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Square".into(), art: std::sync::Arc::new(red) });
|
||||
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Square".into(), xf: vectorcraft_geom::Affine::IDENTITY });
|
||||
group(&mut d, instance)
|
||||
}
|
||||
OverriddenSymbol => {
|
||||
d.symbols.push(vectorcraft_doc::Symbol { dynamic: true, name: "Square".into(), art: std::sync::Arc::new(red) });
|
||||
let mut instance =
|
||||
Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Square".into(), xf: vectorcraft_geom::Affine::IDENTITY });
|
||||
let fill = Some(Paint::solid(Color::rgb8(255, 0, 0)));
|
||||
instance.extras_mut().symbol_overrides.push(vectorcraft_doc::symbol_overrides::SymbolOverride { fill, ..Default::default() });
|
||||
group(&mut d, instance)
|
||||
}
|
||||
InBlend => {
|
||||
let mut blue = rect(&mut d, Rect::new(60.0, 10.0, 80.0, 30.0), [0, 0, 255]);
|
||||
blue.appearance.effects = red.appearance.effects.clone();
|
||||
let blend = Node::new(d.alloc_id(), NodeKind::Blend { children: vec![red.into(), blue.into()], spec: Default::default() });
|
||||
group(&mut d, blend)
|
||||
}
|
||||
};
|
||||
let art = rect(&mut d, Rect::new(10.0, 10.0, 20.0, 30.0), [255, 255, 255]);
|
||||
masked.extras_mut().mask = Some(Box::new(OpacityMask::new(art, false)));
|
||||
add(&mut d, masked);
|
||||
let svg = export(&d, &ExportOptions::default());
|
||||
assert_eq!(spanning_rects(&svg).0, want, "{case:?}, shadowed {shadowed}:\n{svg}");
|
||||
if matches!(case, Alone | InGroup | InNestedGroup | FillEffect) {
|
||||
// The red square alone: no rectangle of the region's size comes back.
|
||||
assert_eq!(imported_paths(&svg), 1, "{case:?}, shadowed {shadowed}:\n{svg}");
|
||||
}
|
||||
}
|
||||
// Type can't be measured, so its mask's region covers any artwork: a group of type gets the
|
||||
// rectangle only when it holds a raster effect, on the type or on a path beside it.
|
||||
for (shadowed, path_shadowed) in [(false, false), (true, false), (false, true)] {
|
||||
let mut d = Document::new(80.0, 40.0);
|
||||
let style = CharStyle { size: 24.0, fill: Paint::solid(Color::BLACK), ..CharStyle::default() };
|
||||
let mut t = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(5.0, 30.0), "Wide", style))));
|
||||
if shadowed {
|
||||
t.appearance.effects.push(drop_shadow());
|
||||
}
|
||||
let mut red = rect(&mut d, Rect::new(50.0, 5.0, 60.0, 15.0), [255, 0, 0]);
|
||||
if path_shadowed {
|
||||
red.appearance.effects.push(drop_shadow());
|
||||
}
|
||||
let mut group = Node::group(d.alloc_id(), vec![t.into(), red.into()]);
|
||||
let art = rect(&mut d, Rect::new(0.0, 0.0, 40.0, 40.0), [255, 255, 255]);
|
||||
group.extras_mut().mask = Some(Box::new(OpacityMask::new(art, false)));
|
||||
add(&mut d, group);
|
||||
let svg = export(&d, &ExportOptions::default());
|
||||
let want = shadowed || path_shadowed;
|
||||
assert_eq!(svg.contains("visibility=\"hidden\""), want, "type {shadowed}, path {path_shadowed}:\n{svg}");
|
||||
}
|
||||
}
|
||||
|
||||
/// In a knockout group, each element is drawn inside one mask of the elements above it. When the
|
||||
/// element inside a mask's group has a drop shadow, that group starts with the same hidden,
|
||||
/// unpainted rectangle over the region, the other groups don't, and import leaves it out.
|
||||
#[test]
|
||||
fn a_knockout_group_spans_its_masks_when_an_element_paints_past_its_shapes() {
|
||||
for shadowed in [false, true] {
|
||||
let mut d = Document::new(60.0, 60.0);
|
||||
let mut below = rect(&mut d, Rect::new(5.0, 5.0, 25.0, 25.0), [200, 0, 0]);
|
||||
if shadowed {
|
||||
below.appearance.effects.push(drop_shadow());
|
||||
}
|
||||
let mut middle = rect(&mut d, Rect::new(15.0, 15.0, 35.0, 35.0), [0, 200, 0]);
|
||||
middle.opacity = 0.5;
|
||||
let mut top = rect(&mut d, Rect::new(20.0, 5.0, 30.0, 40.0), [0, 0, 200]);
|
||||
top.opacity = 0.5;
|
||||
let mut group = Node::group(d.alloc_id(), vec![below.into(), middle.into(), top.into()]);
|
||||
group.knockout = vectorcraft_doc::Knockout::On;
|
||||
add(&mut d, group);
|
||||
let svg = export(&d, &ExportOptions::default());
|
||||
// The bottom and middle elements are masked, in paint order; the top one isn't.
|
||||
assert_eq!(masked_group_spans(&svg), [shadowed, false], "shadowed {shadowed}:\n{svg}");
|
||||
assert_eq!(imported_paths(&svg), 3, "shadowed {shadowed}:\n{svg}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Type is measured by its layout or its path, and a reader sets live type in its own font, so a
|
||||
/// mask or knockout over type keeps the region that covers any artwork.
|
||||
#[test]
|
||||
|
||||
@@ -285,7 +285,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors,
|
||||
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CIDFonts the file doesn't carry not drawn, but for those over a predefined CMap (`UniJIS-UCS2-H`, `90ms-RKSJ-H`, `GBK-EUC-H`, `KSCms-UHC-H`, `ETen-B5-H`), set as type in the font of the CIDFont's name, vertical over a `-V` CMap; a name that ends in the row-and-cell CMap `H` or `V` splits off the CMap only when the file defines the CIDFont of that name, or the name has a hyphen and an installed face that covers Japanese has it; name-keyed fonts in a FontSet set as type by name; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); an embedded Type 1 font's encrypted part is read for its glyphs, which space its type, outline it and draw glyphs without a character, in plain and composite fonts; a Type 42 font that carries its TrueType font gives each code its character by its glyph name or, for names that say nothing (`/g12`), by the glyph's entry in the font's own character map, spaces its type by its own widths and draws glyphs that stand for no character from its outlines; the show operators' spacing along the baseline (`ashow`, `widthshow`, `xshow`, `yshow`, `xyshow`, and the moves of `kshow`'s procedure, run between each two characters) is kept on live type as tracking, so type in a font that isn't installed keeps the file's line lengths (spacing that moves characters off the baseline, as `yshow`, `xyshow` or a `kshow` procedure can, places each character as its own text object); composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write two-byte fonts, draw a Type 42 descendant's glyphs from the TrueType outlines it carries and an embedded Type 1 descendant's from its charstrings, and set other descendants' characters as type at the composite font's size (other mappings are still set as type in the font of their name); CID-keyed fonts (`composefont`, `FMapType` 9 over a CMap built with the CIDInit operators or `Identity-H`/`Identity-V`; `bfchar` and `bfrange` mappings give a base-font descendant its codes or glyph names) draw the glyphs of the Type 0 CIDFonts (Type 1 charstrings in their `StartData` binary data or `GlyphDirectory`) and Type 2 CIDFonts (TrueType glyphs through `CIDMap`) they carry, and the CID-keyed fonts of a FontSet's Compact Font Format data (Type 2 charstrings); a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); sampled functions (type 0) interpolate between their samples, multilinearly or by cubics with `Order 3`, and a sampled axial or radial gradient has a stop at each sample; a gradient the page paints as a row of abutting one-colour strips (as Level 2 writers draw smooth shades) imports as one linear gradient, without the seams the strips leave when each is drawn alone; CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) |
|
||||
| PostScript `.ps` | open, place | yes (the EPS reader reads its first page; `.epsf` files open and place as EPS) | via Print to PostScript | `tests_epsimport.rs` |
|
||||
| DCS 1.0 / 2.0 | place linked | place linked only (one image of the plates; spot plates as spot swatches; the composite's clipping path; each plate on its own ink in Separations Preview and print separations) | no | `crates/eps/src/dcs/tests.rs`, `crates/engine/src/tests/dcs.rs` |
|
||||
| SVG / SVGZ | open, place, save | yes | yes (SVG Options, Preserve Editing, symbols, filters, rich text) | strongest format; tests in `crates/svg/tests`; text on a path follows basic shapes as their SVG 2 equivalent paths (#1527); CSS transforms and the `font` shorthand (#1565), percentage lengths in symbols (#1596), Inkscape flowed text as area type (#1568) and Inkscape pages as artboards (#1601) import; an unsupported filter is named in its warning (#1598); an artboard exported on its own (Export As with Use Artboards, Export for Screens, Asset Export) has the artboard's name as `<title>`, Save the document's (#3544); attributes in other applications' XML namespaces (`plotter:pen="2"`) kept on their objects from import to export, as `data-*` attributes are (`crates/svg/tests/it/foreign_attributes.rs`); simple shapes export as `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polygon>` and `<polyline>` where they draw the same as the path (#4652, `crates/svg/tests/it/basic_shapes.rs`); the regions of exported masks and filters (opacity masks, inverted masks, knockout groups) cover the art they apply to, and over type keep a region that covers any artwork (#6401, `crates/svg/tests/it/blend_export.rs`); SVG Options › Include Variable Data writes the text variables, data sets and bound type objects, and opening the SVG reads them back (`tests::variables::an_svg_with_variable_data_opens_with_its_variables_data_sets_and_bindings`) |
|
||||
| SVG / SVGZ | open, place, save | yes | yes (SVG Options, Preserve Editing, symbols, filters, rich text) | strongest format; tests in `crates/svg/tests`; text on a path follows basic shapes as their SVG 2 equivalent paths (#1527); CSS transforms and the `font` shorthand (#1565), percentage lengths in symbols (#1596), Inkscape flowed text as area type (#1568) and Inkscape pages as artboards (#1601) import; an unsupported filter is named in its warning (#1598); an artboard exported on its own (Export As with Use Artboards, Export for Screens, Asset Export) has the artboard's name as `<title>`, Save the document's (#3544); attributes in other applications' XML namespaces (`plotter:pen="2"`) kept on their objects from import to export, as `data-*` attributes are (`crates/svg/tests/it/foreign_attributes.rs`); simple shapes export as `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polygon>` and `<polyline>` where they draw the same as the path (#4652, `crates/svg/tests/it/basic_shapes.rs`); the regions of exported masks and filters (opacity masks, inverted masks, knockout groups) cover the art they apply to, and over type keep a region that covers any artwork (#6401, `crates/svg/tests/it/blend_export.rs`); a masked group, or a knockout group's masked group, whose content has a shadow, glow, blur or feather starts with a hidden, unpainted rectangle over the mask's region, so readers that draw a masked group only inside the box of its shapes draw those effects too (`crates/svg/tests/it/blend_export.rs`); SVG Options › Include Variable Data writes the text variables, data sets and bound type objects, and opening the SVG reads them back (`tests::variables::an_svg_with_variable_data_opens_with_its_variables_data_sets_and_bindings`) |
|
||||
| DXF | open, export | yes | yes (R12–2018) | R13–2018 closed SPLINE flags and control-point closure on import verified (#1299); circular cubic paths export as native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE while non-circular curves keep the existing fallback (`crates/cad/src/tests.rs`); [Autodesk group 70](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DXF/files/GUID-E1F884F8-AA90-4864-A215-3182D47A9C74.htm). DXF 2000–2018 LAYOUT objects write the paper image origin as 148/149 (was a stray 158), the shade plot codes (2004+) and the model type flag, and the plot style placeholder carries no subclass marker (#3085; structure test `r2000_and_later_files_have_the_structure_cad_apps_require`, checked with ezdxf's audit; not yet confirmed in Autodesk Fusion). DXF Options' Preset menu and `document.exportDxf`'s `dxfPreset: "cnc"` set the CNC / CAM options (#5500); VCarve QA remains open |
|
||||
| HP-GL/2 `.plt`, `.hpgl`, `.hgl`, `.hpg` | — | yes (pen moves, pens and widths, line types and attributes, circles, arcs, wedges and Bézier curves, filled and edged rectangles, wedges and polygons, encoded polylines, scaling and rotation; labels, symbol mode, raster images and hatching aren't drawn) | yes (`.plt`: each visible path once along its line, a pen per color or a pen table, type as outlines, clipping applied, HP-GL or HP-GL/2 with pen colors and encoded polylines) | recognized by its first instructions after any printer escapes, under its own extensions or one no other format reads; `crates/cad/src/hpgl/tests.rs`, `crates/cad/src/hpgl/write/tests.rs` (plots read back by the reader), `tests_hpgl.rs`, `import_fuzz.rs` |
|
||||
| DWG | open, export | no | no | no open spec; DXF instead |
|
||||
|
||||
Reference in New Issue
Block a user