use std::sync::Arc;
use kurbo::{ParamCurve, ParamCurveArclen};
use svgtypes::{parse_font_families, FontFamily, Length, LengthUnit};
use super::svgtree::{AId, EId, FromValue, SvgNode};
use super::{converter, style, OptionLog};
use crate::*;
impl<'a, 'input: 'a> FromValue<'a, 'input> for TextAnchor {
fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
match value {
"start" => Some(TextAnchor::Start),
"middle" => Some(TextAnchor::Middle),
"end" => Some(TextAnchor::End),
_ => None,
}
}
}
impl<'a, 'input: 'a> FromValue<'a, 'input> for AlignmentBaseline {
fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
match value {
"auto" => Some(AlignmentBaseline::Auto),
"baseline" => Some(AlignmentBaseline::Baseline),
"before-edge" => Some(AlignmentBaseline::BeforeEdge),
"text-before-edge" => Some(AlignmentBaseline::TextBeforeEdge),
"middle" => Some(AlignmentBaseline::Middle),
"central" => Some(AlignmentBaseline::Central),
"after-edge" => Some(AlignmentBaseline::AfterEdge),
"text-after-edge" => Some(AlignmentBaseline::TextAfterEdge),
"ideographic" => Some(AlignmentBaseline::Ideographic),
"alphabetic" => Some(AlignmentBaseline::Alphabetic),
"hanging" => Some(AlignmentBaseline::Hanging),
"mathematical" => Some(AlignmentBaseline::Mathematical),
_ => None,
}
}
}
impl<'a, 'input: 'a> FromValue<'a, 'input> for DominantBaseline {
fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
match value {
"auto" => Some(DominantBaseline::Auto),
"use-script" => Some(DominantBaseline::UseScript),
"no-change" => Some(DominantBaseline::NoChange),
"reset-size" => Some(DominantBaseline::ResetSize),
"ideographic" => Some(DominantBaseline::Ideographic),
"alphabetic" => Some(DominantBaseline::Alphabetic),
"hanging" => Some(DominantBaseline::Hanging),
"mathematical" => Some(DominantBaseline::Mathematical),
"central" => Some(DominantBaseline::Central),
"middle" => Some(DominantBaseline::Middle),
"text-after-edge" => Some(DominantBaseline::TextAfterEdge),
"text-before-edge" => Some(DominantBaseline::TextBeforeEdge),
_ => None,
}
}
}
impl<'a, 'input: 'a> FromValue<'a, 'input> for LengthAdjust {
fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
match value {
"spacing" => Some(LengthAdjust::Spacing),
"spacingAndGlyphs" => Some(LengthAdjust::SpacingAndGlyphs),
_ => None,
}
}
}
impl<'a, 'input: 'a> FromValue<'a, 'input> for FontStyle {
fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
match value {
"normal" => Some(FontStyle::Normal),
"italic" => Some(FontStyle::Italic),
"oblique" => Some(FontStyle::Oblique),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug)]
struct CharacterPosition {
x: Option<f32>,
y: Option<f32>,
dx: Option<f32>,
dy: Option<f32>,
}
pub(crate) fn convert(
text_node: SvgNode,
state: &converter::State,
cache: &mut converter::Cache,
parent: &mut Group,
) {
let pos_list = resolve_positions_list(text_node, state);
let rotate_list = resolve_rotate_list(text_node);
let writing_mode = convert_writing_mode(text_node);
let chunks = collect_text_chunks(text_node, &pos_list, state, cache);
let rendering_mode: TextRendering = text_node
.find_attribute(AId::TextRendering)
.unwrap_or(state.opt.text_rendering);
let id = if state.parent_markers.is_empty() {
text_node.element_id().to_string()
} else {
String::new()
};
let dummy = Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap();
let mut text = Text {
id,
rendering_mode,
dx: pos_list.iter().map(|v| v.dx.unwrap_or(0.0)).collect(),
dy: pos_list.iter().map(|v| v.dy.unwrap_or(0.0)).collect(),
rotate: rotate_list,
writing_mode,
chunks,
abs_transform: parent.abs_transform,
bounding_box: dummy,
abs_bounding_box: dummy,
stroke_bounding_box: dummy,
abs_stroke_bounding_box: dummy,
flattened: Box::new(Group::empty()),
layouted: vec![],
};
if text::convert(&mut text, &state.opt.font_resolver, &mut cache.fontdb).is_none() {
return;
}
parent.children.push(Node::Text(Box::new(text)));
}
struct IterState {
chars_count: usize,
chunk_bytes_count: usize,
split_chunk: bool,
text_flow: TextFlow,
chunks: Vec<TextChunk>,
}
fn collect_text_chunks(
text_node: SvgNode,
pos_list: &[CharacterPosition],
state: &converter::State,
cache: &mut converter::Cache,
) -> Vec<TextChunk> {
let mut iter_state = IterState {
chars_count: 0,
chunk_bytes_count: 0,
split_chunk: false,
text_flow: TextFlow::Linear,
chunks: Vec::new(),
};
collect_text_chunks_impl(text_node, pos_list, state, cache, &mut iter_state);
iter_state.chunks
}
fn collect_text_chunks_impl(
parent: SvgNode,
pos_list: &[CharacterPosition],
state: &converter::State,
cache: &mut converter::Cache,
iter_state: &mut IterState,
) {
for child in parent.children() {
if child.is_element() {
if child.tag_name() == Some(EId::TextPath) {
if parent.tag_name() != Some(EId::Text) {
iter_state.chars_count += count_chars(child);
continue;
}
match resolve_text_flow(child, state) {
Some(v) => {
iter_state.text_flow = v;
}
None => {
iter_state.chars_count += count_chars(child);
continue;
}
}
iter_state.split_chunk = true;
}
collect_text_chunks_impl(child, pos_list, state, cache, iter_state);
iter_state.text_flow = TextFlow::Linear;
if child.tag_name() == Some(EId::TextPath) {
iter_state.split_chunk = true;
}
continue;
}
if !parent.is_visible_element(state.opt) {
iter_state.chars_count += child.text().chars().count();
continue;
}
let anchor = parent.find_attribute(AId::TextAnchor).unwrap_or_default();
let font_size = super::units::resolve_font_size(parent, state);
let font_size = match NonZeroPositiveF32::new(font_size) {
Some(n) => n,
None => {
iter_state.chars_count += child.text().chars().count();
continue;
}
};
let font = convert_font(parent, state);
let raw_paint_order: svgtypes::PaintOrder =
parent.find_attribute(AId::PaintOrder).unwrap_or_default();
let paint_order = super::converter::svg_paint_order_to_usvg(raw_paint_order);
let mut dominant_baseline = parent
.find_attribute(AId::DominantBaseline)
.unwrap_or_default();
if dominant_baseline == DominantBaseline::NoChange {
dominant_baseline = parent
.parent_element()
.unwrap()
.find_attribute(AId::DominantBaseline)
.unwrap_or_default();
}
let mut apply_kerning = true;
#[allow(clippy::if_same_then_else)]
if parent.resolve_length(AId::Kerning, state, -1.0) == 0.0 {
apply_kerning = false;
} else if parent.find_attribute::<&str>(AId::FontKerning) == Some("none") {
apply_kerning = false;
}
let mut text_length =
parent.try_convert_length(AId::TextLength, Units::UserSpaceOnUse, state);
if let Some(n) = text_length {
if n < 0.0 {
text_length = None;
}
}
let visibility: Visibility = parent.find_attribute(AId::Visibility).unwrap_or_default();
let span = TextSpan {
start: 0,
end: 0,
fill: style::resolve_fill(parent, true, state, cache),
stroke: style::resolve_stroke(parent, true, state, cache),
paint_order,
font,
font_size,
small_caps: parent.find_attribute::<&str>(AId::FontVariant) == Some("small-caps"),
apply_kerning,
decoration: resolve_decoration(parent, state, cache),
visible: visibility == Visibility::Visible,
dominant_baseline,
alignment_baseline: parent
.find_attribute(AId::AlignmentBaseline)
.unwrap_or_default(),
baseline_shift: convert_baseline_shift(parent, state),
letter_spacing: parent.resolve_length(AId::LetterSpacing, state, 0.0),
word_spacing: parent.resolve_length(AId::WordSpacing, state, 0.0),
text_length,
length_adjust: parent.find_attribute(AId::LengthAdjust).unwrap_or_default(),
};
let mut is_new_span = true;
for c in child.text().chars() {
let char_len = c.len_utf8();
let is_new_chunk = pos_list[iter_state.chars_count].x.is_some()
|| pos_list[iter_state.chars_count].y.is_some()
|| iter_state.split_chunk
|| iter_state.chunks.is_empty();
iter_state.split_chunk = false;
if is_new_chunk {
iter_state.chunk_bytes_count = 0;
let mut span2 = span.clone();
span2.start = 0;
span2.end = char_len;
iter_state.chunks.push(TextChunk {
x: pos_list[iter_state.chars_count].x,
y: pos_list[iter_state.chars_count].y,
anchor,
spans: vec![span2],
text_flow: iter_state.text_flow.clone(),
text: c.to_string(),
});
} else if is_new_span {
let mut span2 = span.clone();
span2.start = iter_state.chunk_bytes_count;
span2.end = iter_state.chunk_bytes_count + char_len;
if let Some(chunk) = iter_state.chunks.last_mut() {
chunk.text.push(c);
chunk.spans.push(span2);
}
} else {
if let Some(chunk) = iter_state.chunks.last_mut() {
chunk.text.push(c);
if let Some(span) = chunk.spans.last_mut() {
debug_assert_ne!(span.end, 0);
span.end += char_len;
}
}
}
is_new_span = false;
iter_state.chars_count += 1;
iter_state.chunk_bytes_count += char_len;
}
}
}
fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option<TextFlow> {
let linked_node = node.attribute::<SvgNode>(AId::Href)?;
let path = super::shapes::convert(linked_node, state)?;
let transform = linked_node.resolve_transform(AId::Transform, state);
let path = if !transform.is_identity() {
let mut path_copy = path.as_ref().clone();
path_copy = path_copy.transform(transform)?;
Arc::new(path_copy)
} else {
path
};
let start_offset: Length = node.attribute(AId::StartOffset).unwrap_or_default();
let start_offset = if start_offset.unit == LengthUnit::Percent {
let path_len = path_length(&path);
(path_len * (start_offset.number / 100.0)) as f32
} else {
node.resolve_length(AId::StartOffset, state, 0.0)
};
let id = NonEmptyString::new(linked_node.element_id().to_string())?;
Some(TextFlow::Path(Arc::new(TextPath {
id,
start_offset,
path,
})))
}
fn convert_font(node: SvgNode, state: &converter::State) -> Font {
let style: FontStyle = node.find_attribute(AId::FontStyle).unwrap_or_default();
let stretch = conv_font_stretch(node);
let weight = resolve_font_weight(node);
let font_families = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily))
{
n.attribute(AId::FontFamily).unwrap_or("")
} else {
""
};
let mut families = parse_font_families(font_families)
.ok()
.log_none(|| {
log::warn!(
"Failed to parse {} value: '{}'. Falling back to {}.",
AId::FontFamily,
font_families,
state.opt.font_family
)
})
.unwrap_or_default();
if families.is_empty() {
families.push(FontFamily::Named(state.opt.font_family.clone()))
}
Font {
families,
style,
stretch,
weight,
}
}
fn conv_font_stretch(node: SvgNode) -> FontStretch {
if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontStretch)) {
match n.attribute(AId::FontStretch).unwrap_or("") {
"narrower" | "condensed" => FontStretch::Condensed,
"ultra-condensed" => FontStretch::UltraCondensed,
"extra-condensed" => FontStretch::ExtraCondensed,
"semi-condensed" => FontStretch::SemiCondensed,
"semi-expanded" => FontStretch::SemiExpanded,
"wider" | "expanded" => FontStretch::Expanded,
"extra-expanded" => FontStretch::ExtraExpanded,
"ultra-expanded" => FontStretch::UltraExpanded,
_ => FontStretch::Normal,
}
} else {
FontStretch::Normal
}
}
fn resolve_font_weight(node: SvgNode) -> u16 {
fn bound(min: usize, val: usize, max: usize) -> usize {
std::cmp::max(min, std::cmp::min(max, val))
}
let nodes: Vec<_> = node.ancestors().collect();
let mut weight = 400;
for n in nodes.iter().rev().skip(1) {
weight = match n.attribute(AId::FontWeight).unwrap_or("") {
"normal" => 400,
"bold" => 700,
"100" => 100,
"200" => 200,
"300" => 300,
"400" => 400,
"500" => 500,
"600" => 600,
"700" => 700,
"800" => 800,
"900" => 900,
"bolder" => {
let step = if weight == 400 { 300 } else { 100 };
bound(100, weight + step, 900)
}
"lighter" => {
let step = if weight == 400 { 200 } else { 100 };
bound(100, weight - step, 900)
}
_ => weight,
};
}
weight as u16
}
fn resolve_positions_list(text_node: SvgNode, state: &converter::State) -> Vec<CharacterPosition> {
let total_chars = count_chars(text_node);
let mut list = vec![
CharacterPosition {
x: None,
y: None,
dx: None,
dy: None,
};
total_chars
];
let mut offset = 0;
for child in text_node.descendants() {
if child.is_element() {
if !matches!(child.tag_name(), Some(EId::Text) | Some(EId::Tspan)) {
continue;
}
let child_chars = count_chars(child);
macro_rules! push_list {
($aid:expr, $field:ident) => {
if let Some(num_list) = super::units::convert_list(child, $aid, state) {
let len = std::cmp::min(num_list.len(), child_chars);
for i in 0..len {
list[offset + i].$field = Some(num_list[i]);
}
}
};
}
push_list!(AId::X, x);
push_list!(AId::Y, y);
push_list!(AId::Dx, dx);
push_list!(AId::Dy, dy);
} else if child.is_text() {
offset += child.text().chars().count();
}
}
list
}
fn resolve_rotate_list(text_node: SvgNode) -> Vec<f32> {
let mut list = vec![0.0; count_chars(text_node)];
let mut last = 0.0;
let mut offset = 0;
for child in text_node.descendants() {
if child.is_element() {
if let Some(rotate) = child.attribute::<Vec<f32>>(AId::Rotate) {
for i in 0..count_chars(child) {
if let Some(a) = rotate.get(i).cloned() {
list[offset + i] = a;
last = a;
} else {
list[offset + i] = last;
}
}
}
} else if child.is_text() {
offset += child.text().chars().count();
}
}
list
}
fn resolve_decoration(
tspan: SvgNode,
state: &converter::State,
cache: &mut converter::Cache,
) -> TextDecoration {
fn find_decoration(node: SvgNode, value: &str) -> bool {
if let Some(str_value) = node.attribute::<&str>(AId::TextDecoration) {
str_value.split(' ').any(|v| v == value)
} else {
false
}
}
let mut gen_style = |text_decoration: &str| {
if !tspan
.ancestors()
.any(|n| find_decoration(n, text_decoration))
{
return None;
}
let mut fill_node = None;
let mut stroke_node = None;
for node in tspan.ancestors() {
if find_decoration(node, text_decoration) || node.tag_name() == Some(EId::Text) {
fill_node = fill_node.map_or(Some(node), Some);
stroke_node = stroke_node.map_or(Some(node), Some);
break;
}
}
Some(TextDecorationStyle {
fill: fill_node.and_then(|node| style::resolve_fill(node, true, state, cache)),
stroke: stroke_node.and_then(|node| style::resolve_stroke(node, true, state, cache)),
})
};
TextDecoration {
underline: gen_style("underline"),
overline: gen_style("overline"),
line_through: gen_style("line-through"),
}
}
fn convert_baseline_shift(node: SvgNode, state: &converter::State) -> Vec<BaselineShift> {
let mut shift = Vec::new();
let nodes: Vec<_> = node
.ancestors()
.take_while(|n| n.tag_name() != Some(EId::Text))
.collect();
for n in nodes {
if let Some(len) = n.try_attribute::<Length>(AId::BaselineShift) {
if len.unit == LengthUnit::Percent {
let n = super::units::resolve_font_size(n, state) * (len.number as f32 / 100.0);
shift.push(BaselineShift::Number(n));
} else {
let n = super::units::convert_length(
len,
n,
AId::BaselineShift,
Units::ObjectBoundingBox,
state,
);
shift.push(BaselineShift::Number(n));
}
} else if let Some(s) = n.attribute(AId::BaselineShift) {
match s {
"sub" => shift.push(BaselineShift::Subscript),
"super" => shift.push(BaselineShift::Superscript),
_ => shift.push(BaselineShift::Baseline),
}
}
}
if shift
.iter()
.all(|base| matches!(base, BaselineShift::Baseline))
{
shift.clear();
}
shift
}
fn count_chars(node: SvgNode) -> usize {
node.descendants()
.filter(|n| n.is_text())
.fold(0, |w, n| w + n.text().chars().count())
}
fn convert_writing_mode(text_node: SvgNode) -> WritingMode {
if let Some(n) = text_node
.ancestors()
.find(|n| n.has_attribute(AId::WritingMode))
{
match n.attribute(AId::WritingMode).unwrap_or("lr-tb") {
"tb" | "tb-rl" | "vertical-rl" | "vertical-lr" => WritingMode::TopToBottom,
_ => WritingMode::LeftToRight,
}
} else {
WritingMode::LeftToRight
}
}
fn path_length(path: &tiny_skia_path::Path) -> f64 {
let mut prev_mx = path.points()[0].x;
let mut prev_my = path.points()[0].y;
let mut prev_x = prev_mx;
let mut prev_y = prev_my;
fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez {
let line = kurbo::Line::new(
kurbo::Point::new(px as f64, py as f64),
kurbo::Point::new(x as f64, y as f64),
);
let p1 = line.eval(0.33);
let p2 = line.eval(0.66);
kurbo::CubicBez::new(line.p0, p1, p2, line.p1)
}
let mut length = 0.0;
for seg in path.segments() {
let curve = match seg {
tiny_skia_path::PathSegment::MoveTo(p) => {
prev_mx = p.x;
prev_my = p.y;
prev_x = p.x;
prev_y = p.y;
continue;
}
tiny_skia_path::PathSegment::LineTo(p) => {
create_curve_from_line(prev_x, prev_y, p.x, p.y)
}
tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez::new(
kurbo::Point::new(prev_x as f64, prev_y as f64),
kurbo::Point::new(p1.x as f64, p1.y as f64),
kurbo::Point::new(p.x as f64, p.y as f64),
)
.raise(),
tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez::new(
kurbo::Point::new(prev_x as f64, prev_y as f64),
kurbo::Point::new(p1.x as f64, p1.y as f64),
kurbo::Point::new(p2.x as f64, p2.y as f64),
kurbo::Point::new(p.x as f64, p.y as f64),
),
tiny_skia_path::PathSegment::Close => {
create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
}
};
length += curve.arclen(0.5);
prev_x = curve.p3.x as f32;
prev_y = curve.p3.y as f32;
}
length
}