use super::fonts::FontSink;
use super::rules::RuleSet;
use super::values::{escape_xml, num};
use crate::Options;
use crate::font::{Font, Kind};
use crate::layout::approx_width;
use crate::ledger::consts::TEXT_LEADING;
use crate::resolve::{AttrMap, ResolvedValue};
use std::fmt::Write;
fn effective_font(
attrs: &AttrMap,
classes: &[String],
ruleset: &RuleSet,
sink: &FontSink,
) -> (Font, f64) {
let kind = match attrs.get("font-family") {
Some(v) => Kind::of_family(Some(v)),
None => match ruleset.provided(classes, "font-family") {
Some(css) => Kind::of_family(Some(&ResolvedValue::RawCss(css.into()))),
None => sink.root_font.kind,
},
};
let font = if attrs.get("font-weight").is_some() {
Font::of(attrs).with_kind(kind)
} else {
match ruleset
.provided(classes, "font-weight")
.map(str::trim)
.unwrap_or("")
{
"500" | "medium" => Font::medium(kind),
"600" | "semibold" => Font::semibold(kind),
"700" | "bold" => Font::bold(kind),
"400" | "normal" => Font::regular(kind),
_ => sink.root_font.with_kind(kind),
}
};
let size = attrs
.number("font-size")
.or_else(|| {
ruleset
.provided(classes, "font-size")
.and_then(|v| v.trim().trim_end_matches("px").parse().ok())
})
.unwrap_or(sink.root_size);
(font, size)
}
#[allow(clippy::too_many_arguments)] pub(crate) fn emit(
out: &mut String,
indent: &str,
classes: &[String],
content: &str,
pos: (f64, f64),
attrs: &AttrMap,
style: &str,
ruleset: &RuleSet,
opts: &Options,
sink: &FontSink,
) {
let (font, size) = effective_font(attrs, classes, ruleset, sink);
#[cfg(feature = "font")]
if opts.static_mode {
let content = apply_text_transform(content, attrs);
sink.register(font, &content, true);
return emit_outlined(
out, indent, classes, &content, pos, attrs, style, font, size,
);
}
if opts.embed_font {
sink.register(font, content, false);
}
let class = classes.join(" ");
let (x, y) = pos;
let (xs, ys) = (num(x), num(y));
let cap_dy = format!(" dy=\"{}em\"", num(font.cap_height_em() / 2.0));
let ls = attrs.number("letter-spacing").unwrap_or(0.0);
let xform = match attrs.number("rotate") {
Some(r) if r != 0.0 => format!(r#" transform="rotate({} {} {})""#, num(r), xs, ys),
_ => String::new(),
};
let lines: Vec<&str> = content.split('\n').collect();
if lines.len() <= 1 {
writeln!(
out,
r#"{indent}<text class="{class}" x="{xs}" y="{ys}"{cap_dy}{}{style}{xform}>{}</text>"#,
dx_attr(content, ls),
escape_xml(content),
)
.unwrap();
return;
}
let size = attrs.number("font-size").unwrap_or(0.0);
let spacing = size * TEXT_LEADING + attrs.number("line-spacing").unwrap_or(0.0);
let top = y - spacing * (lines.len() as f64 - 1.0) / 2.0 + font.cap_height_em() / 2.0 * size;
let line_x: Box<dyn Fn(&str) -> f64> = match attrs.get("line-align") {
Some(ResolvedValue::Ident(a)) if a == "start" || a == "end" => {
let font = crate::font::Font::of(attrs);
let block = approx_width(content, font, size, ls);
if a == "start" {
let left = x - block / 2.0;
Box::new(move |line| left + approx_width(line, font, size, ls) / 2.0)
} else {
let right = x + block / 2.0;
Box::new(move |line| right - approx_width(line, font, size, ls) / 2.0)
}
}
_ => Box::new(move |_| x),
};
write!(
out,
r#"{indent}<text class="{class}" x="{xs}" y="{ys}"{style}{xform}>"#
)
.unwrap();
for (i, line) in lines.iter().enumerate() {
if i == 0 {
write!(
out,
r#"<tspan x="{}" y="{}"{}>{}</tspan>"#,
num(line_x(line)),
num(top),
dx_attr(line, ls),
escape_xml(line)
)
.unwrap();
} else {
write!(
out,
r#"<tspan x="{}" dy="{}"{}>{}</tspan>"#,
num(line_x(line)),
num(spacing),
dx_attr(line, ls),
escape_xml(line)
)
.unwrap();
}
}
writeln!(out, "</text>").unwrap();
}
fn dx_attr(line: &str, letter_spacing: f64) -> String {
let count = line.chars().count();
if letter_spacing == 0.0 || count < 2 {
return String::new();
}
let mut s = String::from(r#" dx="0"#);
for _ in 1..count {
s.push(' ');
s.push_str(&num(letter_spacing));
}
s.push('"');
s
}
#[cfg(feature = "font")]
fn apply_text_transform(content: &str, attrs: &AttrMap) -> String {
let t = match attrs.get("text-transform") {
Some(ResolvedValue::Ident(s)) => s.as_str(),
_ => "",
};
match t {
"uppercase" => content.to_uppercase(),
"lowercase" => content.to_lowercase(),
"capitalize" => content
.split_inclusive(char::is_whitespace)
.map(|w| {
let mut c = w.chars();
match c.next() {
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
None => String::new(),
}
})
.collect(),
_ => content.to_string(),
}
}
#[cfg(feature = "font")]
#[allow(clippy::too_many_arguments)] fn emit_outlined(
out: &mut String,
indent: &str,
classes: &[String],
content: &str,
pos: (f64, f64),
attrs: &AttrMap,
style: &str,
font: Font,
size: f64,
) {
use super::fonts;
let (x, y) = pos;
let ls = attrs.number("letter-spacing").unwrap_or(0.0);
let ident_of = |name: &str| match attrs.get(name) {
Some(ResolvedValue::Ident(s)) => Some(s.as_str()),
_ => None,
};
let mut xform = match attrs.number("rotate") {
Some(r) if r != 0.0 => format!(" transform=\"rotate({} {} {})\"", num(r), num(x), num(y)),
_ => String::new(),
};
let oblique = ident_of("font-style") == Some("italic");
if oblique && xform.is_empty() {
xform = String::new(); }
writeln!(
out,
r#"{indent}<g class="{}"{style}{xform}>"#,
classes.join(" ")
)
.unwrap();
let lines: Vec<&str> = content.split('\n').collect();
let spacing = size * TEXT_LEADING + attrs.number("line-spacing").unwrap_or(0.0);
let top = y - spacing * (lines.len() as f64 - 1.0) / 2.0 + font.cap_height_em() / 2.0 * size;
let line_x: Box<dyn Fn(&str) -> f64> = match attrs.get("line-align") {
Some(ResolvedValue::Ident(a)) if a == "start" || a == "end" => {
let mfont = Font::of(attrs);
let block = approx_width(content, mfont, size, ls);
if a == "start" {
let left = x - block / 2.0;
Box::new(move |line| left + approx_width(line, mfont, size, ls) / 2.0)
} else {
let right = x + block / 2.0;
Box::new(move |line| right - approx_width(line, mfont, size, ls) / 2.0)
}
}
_ => Box::new(move |_| x),
};
let scale = size / font.face().upem as f64;
let skew = if oblique { " skewX(-12)" } else { "" };
for (i, line) in lines.iter().enumerate() {
let baseline = top + spacing * i as f64;
for (ch, gx) in fonts::glyph_starts(line, font, size, ls, line_x(line)) {
if ch.is_whitespace() {
continue;
}
writeln!(
out,
r##"{indent} <use href="#{}" transform="translate({} {}){} scale({} {})"/>"##,
fonts::glyph_ref(font, fonts::glyph_id(font, ch)),
num(gx),
num(baseline),
skew,
num(scale),
num(-scale),
)
.unwrap();
}
let deco = ident_of("text-decoration");
if matches!(deco, Some("underline") | Some("line-through")) {
let strike = deco == Some("line-through");
let (off, th) = fonts::decoration_band(font, size, strike);
let w = approx_width(line, font, size, ls);
let lx = line_x(line);
writeln!(
out,
r#"{indent} <rect x="{}" y="{}" width="{}" height="{}"/>"#,
num(lx - w / 2.0),
num(baseline + off - th / 2.0),
num(w),
num(th),
)
.unwrap();
}
}
writeln!(out, "{indent}</g>").unwrap();
}