mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
SVG import kept a <text>'s nominal font-size and left the scale of the transforms and viewBox above it in the type's transform, so the Character panel showed one size and the canvas drew another (7 pt under scale(3) drew at 21 pt). The importer now moves that scale into the type: the scale across the baseline (the mean scale for type on a path) multiplies the size, leading, baseline shift, character stroke weights and dashes and the text path, and the transform keeps only the rest (a stretch along the baseline, a skew, a rotation or a reflection). The type draws exactly as before. Paints resolve in the grown text space, non-scaling strokes and the stroke under the characters are worked out before the fold, the factor is rounded to 6 significant digits (usvg's transforms are single precision) and never pushes sizes past the Character panel's 0.1–1296 pt (the rest stays in the transform). Tests: a group's, the text's own and a viewBox scale; rotated and scaled text equal to the same text written at twice the size; stretch and skew; type on a scaled path; the size limits; gradients on scaled text; SVG export → import of scaled type (stable over two round trips). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
5.2 KiB
Rust
135 lines
5.2 KiB
Rust
//! SVG import units: physical lengths keep their printed size, pixels and user units map 1:1 to
|
||
//! points, and the document's units follow the file's.
|
||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
|
||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||
|
||
use vectorcraft_doc::{Document, Node, NodeKind, Unit};
|
||
use vectorcraft_geom::Rect;
|
||
use vectorcraft_svg::import;
|
||
|
||
fn art(d: &Document) -> Vec<&Node> {
|
||
let mut v = Vec::new();
|
||
d.walk(|n| {
|
||
if !n.is_layer() {
|
||
v.push(n)
|
||
}
|
||
});
|
||
v
|
||
}
|
||
|
||
fn close(a: f64, b: f64) -> bool {
|
||
(a - b).abs() < 0.01
|
||
}
|
||
|
||
fn close_rect(a: Rect, b: Rect) -> bool {
|
||
close(a.x0, b.x0) && close(a.y0, b.y0) && close(a.x1, b.x1) && close(a.y1, b.y1)
|
||
}
|
||
|
||
fn text_sizes(d: &Document) -> Vec<f64> {
|
||
art(d)
|
||
.iter()
|
||
.filter_map(|n| match &n.kind {
|
||
NodeKind::Text(t) => Some(t.runs.iter().map(|r| r.style.size).collect::<Vec<_>>()),
|
||
_ => None,
|
||
})
|
||
.flatten()
|
||
.collect()
|
||
}
|
||
|
||
#[test]
|
||
fn a4_in_millimetres_opens_at_its_printed_size() {
|
||
let d = import(
|
||
r#"<svg xmlns="http://www.w3.org/2000/svg" width="210mm" height="297mm" viewBox="0 0 210 297">
|
||
<rect x="10" y="20" width="100" height="50"/></svg>"#,
|
||
)
|
||
.unwrap();
|
||
let mm = 72.0 / 25.4;
|
||
assert!(close_rect(d.artboards[0].rect, Rect::new(0.0, 0.0, 595.28, 841.89)), "{:?}", d.artboards[0].rect);
|
||
assert_eq!(d.units, Unit::Millimeters);
|
||
let b = art(&d)[0].geometric_bounds().unwrap();
|
||
assert!(close_rect(b, Rect::new(10.0 * mm, 20.0 * mm, 110.0 * mm, 70.0 * mm)), "{b:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn physical_units_without_a_view_box() {
|
||
let d = import(
|
||
r#"<svg xmlns="http://www.w3.org/2000/svg" width="8.5in" height="11in">
|
||
<rect width="1in" height="2cm" stroke="black" stroke-width="1mm"/></svg>"#,
|
||
)
|
||
.unwrap();
|
||
assert!(close_rect(d.artboards[0].rect, Rect::new(0.0, 0.0, 612.0, 792.0)));
|
||
assert_eq!(d.units, Unit::Inches);
|
||
let n = art(&d)[0];
|
||
assert!(close_rect(n.geometric_bounds().unwrap(), Rect::new(0.0, 0.0, 72.0, 72.0 / 2.54 * 2.0)));
|
||
assert!(close(n.appearance.stroke_width(), 72.0 / 25.4));
|
||
for (w, unit) in [("100pt", Unit::Points), ("10pc", Unit::Picas), ("10cm", Unit::Centimeters)] {
|
||
let d = import(&format!(r#"<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="10"/>"#)).unwrap();
|
||
assert_eq!(d.units, unit, "{w}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn unitless_and_pixels_map_one_to_one() {
|
||
for w in ["100", "100px"] {
|
||
let d = import(&format!(r#"<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="50"><rect width="100" height="50"/></svg>"#)).unwrap();
|
||
assert_eq!(d.artboards[0].rect, Rect::new(0.0, 0.0, 100.0, 50.0), "{w}");
|
||
assert_eq!(d.units, Unit::Pixels, "{w}");
|
||
assert!(close_rect(art(&d)[0].geometric_bounds().unwrap(), Rect::new(0.0, 0.0, 100.0, 50.0)));
|
||
}
|
||
// No size at all: the viewBox gives it, in user units.
|
||
let d = import(r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 40"/>"#).unwrap();
|
||
assert_eq!(d.artboards[0].rect, Rect::new(0.0, 0.0, 30.0, 40.0));
|
||
assert_eq!(d.units, Unit::Pixels);
|
||
}
|
||
|
||
#[test]
|
||
fn font_size_units_are_honoured() {
|
||
let d = import(
|
||
r#"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
|
||
<text x="0" y="20" font-size="12pt">a</text>
|
||
<text x="0" y="40" font-size="16px">b</text>
|
||
<text x="0" y="60" font-size="0.5in">c</text>
|
||
<g font-size="10"><text x="0" y="80" font-size="2em">d</text><text x="0" y="100" font-size="150%">e</text></g>
|
||
<text x="0" y="120">f<tspan font-size="larger">g</tspan></text>
|
||
<text x="0" y="140" font-size="x-large">h</text>
|
||
</svg>"#,
|
||
)
|
||
.unwrap();
|
||
let sizes = text_sizes(&d);
|
||
let want = [12.0, 16.0, 36.0, 20.0, 15.0, 12.0, 14.4, 18.0];
|
||
assert_eq!(sizes.len(), want.len(), "{sizes:?}");
|
||
for (s, w) in sizes.iter().zip(want) {
|
||
assert!(close(*s, w), "{sizes:?}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn text_lengths_in_physical_units() {
|
||
let d = import(
|
||
r#"<svg xmlns="http://www.w3.org/2000/svg" width="10in" height="10in">
|
||
<text x="1in" y="2in" font-size="20" letter-spacing="0.1em" stroke="red" stroke-width="1pt">Hi</text></svg>"#,
|
||
)
|
||
.unwrap();
|
||
let NodeKind::Text(t) = &art(&d)[0].kind else { panic!() };
|
||
let o = t.xf.translation();
|
||
assert!(close(o.x, 72.0) && close(o.y, 144.0), "{o:?}");
|
||
let st = t.first_style();
|
||
assert!(close(st.tracking, 100.0), "{}", st.tracking);
|
||
// User units of a physical root are CSS pixels (0.75 pt): that scale moves into the type.
|
||
assert!(close(t.xf.as_coeffs()[0], 1.0), "{:?}", t.xf);
|
||
assert!(close(st.stroke_width, 1.0), "{}", st.stroke_width);
|
||
assert!(close(st.size, 15.0), "{}", st.size);
|
||
}
|
||
|
||
#[test]
|
||
fn absolute_lengths_in_a_pixel_document_keep_their_size() {
|
||
let d = import(
|
||
r#"<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
|
||
<rect width="1in" height="10mm" stroke="black" stroke-width="2pt"/></svg>"#,
|
||
)
|
||
.unwrap();
|
||
let n = art(&d)[0];
|
||
assert!(close_rect(n.geometric_bounds().unwrap(), Rect::new(0.0, 0.0, 72.0, 720.0 / 25.4)), "{:?}", n.geometric_bounds());
|
||
assert!(close(n.appearance.stroke_width(), 2.0));
|
||
}
|