Merge pull request #1048 from storytold/fix/1010-svg-mixed-units
#1010: SVGs with one absolute and one unitless root side import unstretched
This commit is contained in:
commit
145ae2e877
|
|
@ -123,7 +123,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
|
|||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). Select › All on Active Artboard selects on the artboard made active, not always the first (#1006). Reverse Gradient keeps off-centre midpoints, mirrored (#1007). A polar grid with no radial dividers draws none (#1008). Use Precise Cursors also covers the Pen's join pointer (#1009). |
|
||||
| 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). Select › All on Active Artboard selects on the artboard made active, not always the first (#1006). Reverse Gradient keeps off-centre midpoints, mirrored (#1007). A polar grid with no radial dividers draws none (#1008). Use Precise Cursors also covers the Pen's join pointer (#1009). SVGs with one absolute and one unitless root side import at their size, unstretched (#1010). |
|
||||
| 2026-10-10 | Initial live Revolve (Effect › 3D and Materials, #846, #605), in its own `three-d` crate. Progress docs re-measured and restructured to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `ui-parity.md`, the format, hardware, localization, effects and type checklists). Same day: v0.8.0; Effect › Distort, Pixelate and Texture filters (13); German interface; Variables (data merge); Graph Type value axes and tick marks; restart when the first frame never reaches the screen (#964) |
|
||||
| 2026-10-09 | Radial Blur, Smart Blur, Unsharp Mask (gap 2 begins); 340 commits, the busiest day |
|
||||
| 2026-10-08 | v0.5.0–v0.7.0; community contributions: Pen and shape-tool modifiers, Layers, Artboards, Japanese composition, Hebrew/Arabic type, interface languages, saved selections |
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<S
|
|||
usvg::Tree::from_str(&src, &opt).map_err(|e| SvgError::Parse(e.to_string()))?
|
||||
};
|
||||
let size = tree.size();
|
||||
let (kx, ky) = units.k;
|
||||
let mut doc = Document::new(size.width() as f64 * kx, size.height() as f64 * ky);
|
||||
let k = units.k;
|
||||
let mut doc = Document::new(size.width() as f64 * k, size.height() as f64 * k);
|
||||
doc.units = units.unit;
|
||||
let mut im = Importer {
|
||||
doc,
|
||||
|
|
@ -68,7 +68,7 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<S
|
|||
data: found.data,
|
||||
sublayers: found.sublayers,
|
||||
sublayer_groups: HashSet::new(),
|
||||
screen: (kx * ky).sqrt(),
|
||||
screen: k,
|
||||
};
|
||||
if im.files.nested_text() {
|
||||
im.warn("text inside an SVG image isn't imported".into());
|
||||
|
|
@ -76,7 +76,7 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<S
|
|||
|
||||
// usvg wraps everything in an id-less group carrying the viewBox transform when needed.
|
||||
let mut top = tree.root();
|
||||
let mut base = Affine::scale_non_uniform(kx, ky) * aff(top.transform());
|
||||
let mut base = Affine::scale(k) * aff(top.transform());
|
||||
if let Some(g) = im.wrapper(top) {
|
||||
base *= aff(g.transform());
|
||||
top = g;
|
||||
|
|
@ -212,12 +212,14 @@ pub(super) const DEFAULT_FONT_SIZE: f64 = 12.0;
|
|||
/// A pixel (and a unitless user unit) is a point, as we export, and absolute lengths keep their
|
||||
/// physical size at 72 pt per inch: `font-size="12pt"` is 12 pt, `1in` is 72 pt. A root size in
|
||||
/// absolute units (`width="210mm"`) is that physical size, and its user units are CSS pixels of it
|
||||
/// (96 per inch, 0.75 pt each), so a drawing without a `viewBox` keeps its proportions.
|
||||
/// (96 per inch, 0.75 pt each), so a drawing without a `viewBox` keeps its proportions. With one
|
||||
/// side absolute and the other not (`width="96pt" height="96"`), both are: the unitless side is
|
||||
/// CSS pixels too, so the drawing isn't stretched (#1010).
|
||||
struct RootUnits {
|
||||
/// Pixels per inch usvg (and the text pass) convert absolute lengths at.
|
||||
dpi: f64,
|
||||
/// Points per user unit of the root along x and y.
|
||||
k: (f64, f64),
|
||||
/// Points per user unit of the root, along both axes.
|
||||
k: f64,
|
||||
/// Document units: those of the root `width` (pixels when it has none).
|
||||
unit: Unit,
|
||||
}
|
||||
|
|
@ -228,8 +230,7 @@ impl RootUnits {
|
|||
use svgtypes::LengthUnit as U;
|
||||
let physical = |u: Option<U>| matches!(u, Some(U::In | U::Cm | U::Mm | U::Pt | U::Pc));
|
||||
let (w, h) = (unit("width"), unit("height"));
|
||||
let k = |u| if physical(u) { PT_PER_IN / CSS_PX_PER_IN } else { 1.0 };
|
||||
let dpi = if physical(w) || physical(h) { CSS_PX_PER_IN } else { PT_PER_IN };
|
||||
let (dpi, k) = if physical(w) || physical(h) { (CSS_PX_PER_IN, PT_PER_IN / CSS_PX_PER_IN) } else { (PT_PER_IN, 1.0) };
|
||||
let unit = match w {
|
||||
Some(U::Mm) => Unit::Millimeters,
|
||||
Some(U::Cm) => Unit::Centimeters,
|
||||
|
|
@ -238,7 +239,7 @@ impl RootUnits {
|
|||
Some(U::Pc) => Unit::Picas,
|
||||
_ => Unit::Pixels,
|
||||
};
|
||||
Self { dpi, k: (k(w), k(h)), unit }
|
||||
Self { dpi, k, unit }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,8 +41,9 @@
|
|||
//! * One CSS pixel (user unit) is one point, as on export, and absolute lengths (`in`, `cm`, `mm`,
|
||||
//! `pt`, `pc`) keep their physical size (72 pt per inch): `font-size="12pt"` is 12 pt. A root
|
||||
//! `width`/`height` in absolute units keeps its physical size, so a 210 mm SVG opens on a 210 mm
|
||||
//! artboard, its user units being CSS pixels of it (96 per inch). The document's units follow the
|
||||
//! unit of the root `width` (pixels when it has none).
|
||||
//! artboard, its user units being CSS pixels of it (96 per inch), the other side's too when only
|
||||
//! one is absolute. The document's units follow the unit of the root `width` (pixels when it has
|
||||
//! none).
|
||||
//! * `<mask>` imports as a luminance opacity mask; a mask we exported keeps its options and art
|
||||
//! (its `data-vectorcraft-mask="noclip invert"` lists the options that differ from clipping and
|
||||
//! not inverted). Nested clip paths (`<clipPath clip-path>`) are clip groups in clip groups.
|
||||
|
|
|
|||
|
|
@ -811,6 +811,31 @@ fn import_absolute_units_keep_their_physical_size() {
|
|||
}
|
||||
}
|
||||
|
||||
/// #1010: a root with one absolute side and one unitless or px side opens as if both were
|
||||
/// absolute (CSS pixels at 96 per inch), so nothing is stretched and the viewport keeps its size.
|
||||
#[test]
|
||||
fn import_mixed_root_units_scale_both_sides_alike() {
|
||||
let rect = |d: &Document| art(d)[0].geometric_bounds().unwrap();
|
||||
for (size, board) in [
|
||||
(r#"width="96pt" height="96""#, Rect::new(0.0, 0.0, 96.0, 72.0)),
|
||||
(r#"width="96pt" height="96px""#, Rect::new(0.0, 0.0, 96.0, 72.0)),
|
||||
(r#"width="96pt" height="72pt""#, Rect::new(0.0, 0.0, 96.0, 72.0)),
|
||||
(r#"width="128" height="72pt""#, Rect::new(0.0, 0.0, 96.0, 72.0)),
|
||||
] {
|
||||
let svg =
|
||||
|extra: &str| format!(r##"<svg xmlns="http://www.w3.org/2000/svg" {size} {extra}><rect width="32" height="32" fill="#f00"/></svg>"##);
|
||||
let d = import(&svg("")).unwrap();
|
||||
assert!(close_rect(d.artboards[0].rect, board, 1e-6), "{size}: {:?}", d.artboards[0].rect);
|
||||
assert!(close_rect(rect(&d), Rect::new(0.0, 0.0, 24.0, 24.0), 1e-6), "{size}: {:?}", rect(&d));
|
||||
// With a viewBox: fitted uniformly (xMidYMid meet), unless preserveAspectRatio is none.
|
||||
let d = import(&svg(r#"viewBox="0 0 32 32""#)).unwrap();
|
||||
assert!(close_rect(d.artboards[0].rect, board, 1e-6), "{size}: {:?}", d.artboards[0].rect);
|
||||
assert!(close_rect(rect(&d), Rect::new(12.0, 0.0, 84.0, 72.0), 1e-6), "{size} viewBox: {:?}", rect(&d));
|
||||
let d = import(&svg(r#"viewBox="0 0 32 32" preserveAspectRatio="none""#)).unwrap();
|
||||
assert!(close_rect(rect(&d), board, 1e-6), "{size} none: {:?}", rect(&d));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_document_roundtrip_is_stable() {
|
||||
// export → import → export produces identical SVG.
|
||||
|
|
|
|||
Loading…
Reference in New Issue