Merge pull request #148 from storytold/fix/issue-136
Fix #136: SVG export and CSS write each style's numeric font-weight
This commit is contained in:
commit
e94dfed775
|
|
@ -72,21 +72,18 @@ pub(crate) fn font_descriptor(st: &CharStyle) -> (u16, bool) {
|
|||
}
|
||||
|
||||
/// The font of character style `st`: family, size (the vertical scale; writers stretch the
|
||||
/// horizontal one), weight and style, lengths written by `len`. `numeric_weight`: the face's
|
||||
/// weight as a number (embedded faces are told apart by it), else `bold` for bold styles.
|
||||
pub(crate) fn font_props(st: &CharStyle, len: &dyn Fn(f64) -> String, numeric_weight: bool) -> Props {
|
||||
/// horizontal one), weight and style, lengths written by `len`. The weight is the style's number
|
||||
/// (600 for Semibold, 300 for Light), `bold` for 700 and left out for 400.
|
||||
pub(crate) fn font_props(st: &CharStyle, len: &dyn Fn(f64) -> String) -> Props {
|
||||
let mut p = Props::new();
|
||||
let fam = if st.font_family.contains(|c: char| c.is_whitespace() || c == ',') { format!("'{}'", st.font_family) } else { st.font_family.clone() };
|
||||
p.push(("font-family", fam));
|
||||
p.push(("font-size", len(st.size * st.v_scale / 100.0)));
|
||||
let fs = st.font_style.to_ascii_lowercase();
|
||||
let (weight, italic) = font_descriptor(st);
|
||||
if numeric_weight {
|
||||
if weight != 400 {
|
||||
p.push(("font-weight", weight.to_string()));
|
||||
}
|
||||
} else if fs.contains("bold") || fs.contains("black") || fs.contains("heavy") {
|
||||
p.push(("font-weight", "bold".into()));
|
||||
match weight {
|
||||
400 => {}
|
||||
700 => p.push(("font-weight", "bold".into())),
|
||||
w => p.push(("font-weight", w.to_string())),
|
||||
}
|
||||
if italic {
|
||||
p.push(("font-style", "italic".into()));
|
||||
|
|
@ -531,7 +528,7 @@ impl Rules<'_> {
|
|||
self.warn("type with several character styles is written in its first one");
|
||||
}
|
||||
let len = |v: f64| self.len(v);
|
||||
art.props.extend(font_props(&st, &len, false));
|
||||
art.props.extend(font_props(&st, &len));
|
||||
match &st.fill {
|
||||
Paint::Solid { color, .. } => art.props.push(("color", color.to_hex())),
|
||||
Paint::None => art.props.push(("color", "transparent".into())),
|
||||
|
|
|
|||
|
|
@ -411,10 +411,6 @@ impl Writer<'_> {
|
|||
fn tiny(&self) -> bool {
|
||||
self.opts.profile == Profile::Tiny12
|
||||
}
|
||||
/// Are the fonts type uses embedded?
|
||||
fn embeds_fonts(&self) -> bool {
|
||||
self.opts.embed_fonts && !self.opts.outline_text && !self.tiny()
|
||||
}
|
||||
fn num(&self, v: f64) -> String {
|
||||
fmt_num(v, self.opts.decimals)
|
||||
}
|
||||
|
|
@ -1505,7 +1501,7 @@ impl Writer<'_> {
|
|||
fn char_props(&mut self, st: &CharStyle, space: (Rect, Affine)) -> Props {
|
||||
let decimals = self.opts.decimals;
|
||||
let len = |v: f64| fmt_num(v, decimals);
|
||||
let mut p = css::font_props(st, &len, self.embeds_fonts());
|
||||
let mut p = css::font_props(st, &len);
|
||||
// Character paints resolve in the text's space (`to_user` maps it into user space).
|
||||
let (bounds, to_user) = space;
|
||||
let saved = std::mem::replace(&mut self.xf, to_user);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
//! 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.
|
||||
// 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::{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")),
|
||||
] {
|
||||
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");
|
||||
}
|
||||
|
|
@ -133,7 +133,8 @@ lists every option with its default. `encoding` is `utf8`, `utf16` (big-endian a
|
|||
(presentation attributes only; filters, masks, blend modes and embedded fonts left out, symbols as art, each with
|
||||
a warning). `embedFonts: true` embeds the fonts type uses as `@font-face` rules subset to the characters used; a
|
||||
font whose licence (OS/2 `fsType`) forbids subsetting is embedded whole, one that forbids embedding is left out
|
||||
with a warning, and the type then names faces by numeric `font-weight` (600 for Semibold). `svgz` takes the same options and writes the SVG gzipped (`.svgz` files also open,
|
||||
with a warning. Type names its faces by numeric `font-weight` (600 for Semibold, 300 for Light, `bold` for 700),
|
||||
embedded or not. `svgz` takes the same options and writes the SVG gzipped (`.svgz` files also open,
|
||||
place and paste). `run_command document.save {path: "x.svg", svg: {…}}` saves as SVG (or `.svgz`).
|
||||
|
||||
Symbols export as one `<symbol>` with a `<use>` per instance. An instance the def can't stand for is written as its
|
||||
|
|
|
|||
Loading…
Reference in New Issue