Files
vectorcraft/crates/svg/tests/it/font_weights.rs
T
aiongg 8cc69ed45c CI: one test binary per crate, nextest, line tables only (#3855)
* CI: line tables only for workspace crates, no debug info for dependencies

* CI: one integration-test binary per crate

Each crate's tests/*.rs files become modules of tests/it/main.rs. Files that change
process-wide state (Color Settings, the font database, installed plug-ins, the working
directory), the fuzz sweeps and the test targets the Affinity corpus workflow names stay
their own binaries.

* Docs: name the integration tests' new paths

* CI: xtask test with nextest and doctests, one wasm check, clippy --keep-going

cargo xtask ci runs the tests with cargo nextest (cargo test when it is not installed),
builds them with --lib --bins --tests, runs the doctests as their own step, and checks
the wasm crates in one cargo check --keep-going call.
2026-10-23 21:36:10 -04:00

84 lines
3.3 KiB
Rust

//! Font weights in SVG and CSS: every style is written with its numeric weight (Semibold 600,
//! Light 300, Black 900…; Bold as `bold`), and SVG import reads the numbers back to the same face.
use vectorcraft_doc::{CharStyle, Document, Node, NodeKind, TextObject};
use vectorcraft_geom::Point;
use vectorcraft_svg::{CssOptions, ExportOptions, css_rules, export, import};
use vectorcraft_text::FontDb;
/// A document with one text in `family` `style`.
fn doc(family: &str, style: &str) -> (Document, vectorcraft_doc::NodeId) {
let mut d = Document::new(300.0, 100.0);
let st = CharStyle { font_family: family.into(), font_style: style.into(), size: 24.0, ..CharStyle::default() };
let n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(10.0, 50.0), "Weight", st))));
let (l, id) = (d.layers[0].id, n.id);
d.insert(Some(l), 0, n).unwrap();
(d, id)
}
/// The `font-weight` the SVG export and the CSS of the text write.
fn weights(style: &str) -> (Option<String>, Option<String>) {
let (d, id) = doc("Source Sans 3", style);
let svg = export(&d, &ExportOptions::default());
let attr = svg.split("font-weight=\"").nth(1).map(|s| s.split('"').next().unwrap().to_string());
let sheet = css_rules(&d, &[id], &CssOptions::default());
let css = sheet.rules[0].props.iter().find(|(k, _)| *k == "font-weight").map(|(_, v)| v.clone());
(attr, css)
}
fn style_back(family: &str, style: &str) -> String {
let svg = export(&doc(family, style).0, &ExportOptions::default());
let mut found = None;
import(&svg).unwrap().walk(|n| {
if let NodeKind::Text(t) = &n.kind {
found = Some(t.runs[0].style.font_style.clone());
}
});
found.expect("the text comes back")
}
#[test]
fn every_style_is_written_with_its_numeric_weight() {
for (style, want) in [
("Thin", Some("100")),
("ExtraLight", Some("200")),
("Light", Some("300")),
("Regular", None),
("Italic", None),
("Medium", Some("500")),
("Semibold", Some("600")),
("Demibold", Some("600")),
("Bold", Some("bold")),
("Bold Italic", Some("bold")),
("Extrabold", Some("800")),
("Black", Some("900")),
("Heavy", Some("900")),
// Japanese faces' numbered weights (#2873); W4 is the regular weight.
("W0", Some("100")),
("W3", Some("300")),
("W4", None),
("W6", Some("600")),
("W9", Some("900")),
("Wide", None),
] {
let want = want.map(str::to_string);
assert_eq!(weights(style), (want.clone(), want), "{style}");
}
}
#[test]
fn numeric_weights_read_back_to_the_style() {
let db = FontDb::global();
for (family, style) in
[("Source Sans 3", "Semibold"), ("Source Sans 3", "Bold"), ("Source Sans 3", "Italic"), ("Inter", "Medium"), ("Inter", "SemiBold")]
{
let back = style_back(family, style);
let face = |s: &str| db.face(family, s).map(|f| f.style.clone());
assert_eq!(face(&back), face(style), "{family} {style} came back as {back}");
}
// Styles the family doesn't have keep their weight.
assert_eq!(style_back("Source Sans 3", "Light"), "Light");
assert_eq!(style_back("Source Sans 3", "Black Italic"), "Black Italic");
assert_eq!(style_back("Source Sans 3", "Heavy"), "Black");
}