use std::rc::Rc;
use strict_num::NonZeroPositiveF64;
use crate::svgtree::{AId, EId};
use crate::{converter, style, svgtree, Node, NodeExt, NodeKind, SharedPathData, Units};
use crate::{PaintOrder, PathData, TextRendering, Transform, Visibility};
use svgrtypes::{Length, LengthUnit};
#[allow(missing_docs)]
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
pub enum Stretch {
UltraCondensed,
ExtraCondensed,
Condensed,
SemiCondensed,
Normal,
SemiExpanded,
Expanded,
ExtraExpanded,
UltraExpanded,
}
impl Default for Stretch {
#[inline]
fn default() -> Self {
Stretch::Normal
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Style {
Normal,
Italic,
Oblique,
}
impl Default for Style {
#[inline]
fn default() -> Style {
Style::Normal
}
}
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct Font {
pub families: Vec<String>,
pub style: Style,
pub stretch: Stretch,
pub weight: u16,
}
#[allow(missing_docs)]
#[derive(Clone, Hash, Copy, PartialEq, Debug)]
pub enum DominantBaseline {
Auto,
UseScript,
NoChange,
ResetSize,
Ideographic,
Alphabetic,
Hanging,
Mathematical,
Central,
Middle,
TextAfterEdge,
TextBeforeEdge,
}
#[allow(missing_docs)]
#[derive(Clone, Hash, Copy, PartialEq, Debug)]
pub enum AlignmentBaseline {
Auto,
Baseline,
BeforeEdge,
TextBeforeEdge,
Middle,
Central,
AfterEdge,
TextAfterEdge,
Ideographic,
Alphabetic,
Hanging,
Mathematical,
}
#[allow(missing_docs)]
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum BaselineShift {
Baseline,
Subscript,
Superscript,
Number(f64),
}
impl std::hash::Hash for BaselineShift {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
BaselineShift::Baseline => 0.hash(state),
BaselineShift::Subscript => 1.hash(state),
BaselineShift::Superscript => 2.hash(state),
BaselineShift::Number(v) => (3 + v.to_bits()).hash(state),
}
}
}
impl Default for BaselineShift {
#[inline]
fn default() -> BaselineShift {
BaselineShift::Baseline
}
}
#[allow(missing_docs)]
#[derive(Clone, Hash, Copy, PartialEq, Debug)]
pub enum LengthAdjust {
Spacing,
SpacingAndGlyphs,
}
#[derive(Clone, Debug, Hash)]
pub struct TextDecorationStyle {
pub fill: Option<style::Fill>,
pub stroke: Option<style::Stroke>,
}
#[derive(Clone, Hash, Debug)]
pub struct TextDecoration {
pub underline: Option<TextDecorationStyle>,
pub overline: Option<TextDecorationStyle>,
pub line_through: Option<TextDecorationStyle>,
}
#[derive(Clone, Debug)]
pub struct TextSpan {
pub start: usize,
pub end: usize,
pub fill: Option<style::Fill>,
pub stroke: Option<style::Stroke>,
pub paint_order: PaintOrder,
pub font: Font,
pub font_size: NonZeroPositiveF64,
pub small_caps: bool,
pub apply_kerning: bool,
pub decoration: TextDecoration,
pub dominant_baseline: DominantBaseline,
pub alignment_baseline: AlignmentBaseline,
pub baseline_shift: Vec<BaselineShift>,
pub visibility: Visibility,
pub letter_spacing: f64,
pub word_spacing: f64,
pub text_length: Option<f64>,
pub length_adjust: LengthAdjust,
}
impl std::hash::Hash for TextSpan {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.start.hash(state);
self.end.hash(state);
self.fill.hash(state);
self.stroke.hash(state);
self.paint_order.hash(state);
self.font.hash(state);
self.font_size.hash(state);
self.small_caps.hash(state);
self.apply_kerning.hash(state);
self.decoration.hash(state);
self.dominant_baseline.hash(state);
self.alignment_baseline.hash(state);
self.baseline_shift.hash(state);
self.visibility.hash(state);
self.letter_spacing.to_bits().hash(state);
self.word_spacing.to_bits().hash(state);
self.text_length.map(|f| f.to_bits()).hash(state);
self.length_adjust.hash(state);
}
}
#[allow(missing_docs)]
#[derive(Clone, Hash, Copy, PartialEq, Debug)]
pub enum TextAnchor {
Start,
Middle,
End,
}
#[derive(Clone, Debug)]
pub struct TextPath {
pub start_offset: f64,
pub path: Rc<PathData>,
}
impl std::hash::Hash for TextPath {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.start_offset.to_bits().hash(state);
self.path.hash(state);
}
}
#[derive(Clone, Hash, Debug)]
pub enum TextFlow {
Linear,
Path(Rc<TextPath>),
}
#[derive(Clone, Debug)]
pub struct TextChunk {
pub x: Option<f64>,
pub y: Option<f64>,
pub anchor: TextAnchor,
pub spans: Vec<TextSpan>,
pub text_flow: TextFlow,
pub text: String,
}
impl std::hash::Hash for TextChunk {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.x.map(|f| f.to_bits()).hash(state);
self.y.map(|f| f.to_bits()).hash(state);
self.anchor.hash(state);
self.spans.hash(state);
self.text_flow.hash(state);
self.text.hash(state);
}
}
#[derive(Clone, Copy, Debug)]
pub struct CharacterPosition {
pub x: Option<f64>,
pub y: Option<f64>,
pub dx: Option<f64>,
pub dy: Option<f64>,
}
impl std::hash::Hash for CharacterPosition {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.x.map(|f| f.to_bits()).hash(state);
self.y.map(|f| f.to_bits()).hash(state);
self.dx.map(|f| f.to_bits()).hash(state);
self.dy.map(|f| f.to_bits()).hash(state);
}
}
#[allow(missing_docs)]
#[derive(Clone, Hash, Copy, PartialEq, Debug)]
pub enum WritingMode {
LeftToRight,
TopToBottom,
}
#[derive(Clone, Debug)]
pub struct Text {
pub id: String,
pub transform: Transform,
pub rendering_mode: TextRendering,
pub positions: Vec<CharacterPosition>,
pub rotate: Vec<f64>,
pub writing_mode: WritingMode,
pub chunks: Vec<TextChunk>,
}
impl std::hash::Hash for Text {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.transform.hash(state);
self.rendering_mode.hash(state);
self.positions.hash(state);
self.rotate
.iter()
.map(|f| f.to_bits())
.collect::<Vec<_>>()
.hash(state);
self.writing_mode.hash(state);
self.chunks.hash(state);
}
}
impl_enum_default!(TextAnchor, Start);
impl_enum_from_str!(TextAnchor,
"start" => TextAnchor::Start,
"middle" => TextAnchor::Middle,
"end" => TextAnchor::End
);
impl_enum_default!(AlignmentBaseline, Auto);
impl_enum_from_str!(AlignmentBaseline,
"auto" => AlignmentBaseline::Auto,
"baseline" => AlignmentBaseline::Baseline,
"before-edge" => AlignmentBaseline::BeforeEdge,
"text-before-edge" => AlignmentBaseline::TextBeforeEdge,
"middle" => AlignmentBaseline::Middle,
"central" => AlignmentBaseline::Central,
"after-edge" => AlignmentBaseline::AfterEdge,
"text-after-edge" => AlignmentBaseline::TextAfterEdge,
"ideographic" => AlignmentBaseline::Ideographic,
"alphabetic" => AlignmentBaseline::Alphabetic,
"hanging" => AlignmentBaseline::Hanging,
"mathematical" => AlignmentBaseline::Mathematical
);
impl_enum_default!(DominantBaseline, Auto);
impl_enum_from_str!(DominantBaseline,
"auto" => DominantBaseline::Auto,
"use-script" => DominantBaseline::UseScript,
"no-change" => DominantBaseline::NoChange,
"reset-size" => DominantBaseline::ResetSize,
"ideographic" => DominantBaseline::Ideographic,
"alphabetic" => DominantBaseline::Alphabetic,
"hanging" => DominantBaseline::Hanging,
"mathematical" => DominantBaseline::Mathematical,
"central" => DominantBaseline::Central,
"middle" => DominantBaseline::Middle,
"text-after-edge" => DominantBaseline::TextAfterEdge,
"text-before-edge" => DominantBaseline::TextBeforeEdge
);
impl_enum_default!(LengthAdjust, Spacing);
impl_enum_from_str!(LengthAdjust,
"spacing" => LengthAdjust::Spacing,
"spacingAndGlyphs" => LengthAdjust::SpacingAndGlyphs
);
impl crate::svgtree::EnumFromStr for Style {
fn enum_from_str(s: &str) -> Option<Self> {
match s {
"normal" => Some(Style::Normal),
"italic" => Some(Style::Italic),
"oblique" => Some(Style::Oblique),
_ => None,
}
}
}
pub(crate) fn convert(
text_node: svgtree::Node,
state: &converter::State,
cache: &mut converter::Cache,
parent: &mut Node,
) {
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 text = Text {
id: text_node.element_id().to_string(),
transform: Transform::default(),
rendering_mode,
positions: pos_list,
rotate: rotate_list,
writing_mode,
chunks,
};
parent.append_kind(NodeKind::Text(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: svgtree::Node,
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,
text_node,
pos_list,
state,
cache,
&mut iter_state,
);
iter_state.chunks
}
fn collect_text_chunks_impl(
text_node: svgtree::Node,
parent: svgtree::Node,
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.has_tag_name(EId::TextPath) {
if !parent.has_tag_name(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(text_node, child, pos_list, state, cache, iter_state);
iter_state.text_flow = TextFlow::Linear;
if child.has_tag_name(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 = crate::units::resolve_font_size(parent, state);
let font_size = match NonZeroPositiveF64::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: svgrtypes::PaintOrder = parent
.find_attribute(svgtree::AId::PaintOrder)
.unwrap_or_default();
let paint_order = crate::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;
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 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(AId::FontVariant) == Some("small-caps"),
apply_kerning,
decoration: resolve_decoration(text_node, parent, state, cache),
visibility: parent.find_attribute(AId::Visibility).unwrap_or_default(),
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: svgtree::Node, state: &converter::State) -> Option<TextFlow> {
let linked_node = node.attribute::<svgtree::Node>(AId::Href)?;
let path = match linked_node.tag_name()? {
EId::Rect | EId::Circle | EId::Ellipse | EId::Line | EId::Polyline | EId::Polygon => {
crate::shapes::convert(linked_node, state)?
}
EId::Path => linked_node.attribute::<SharedPathData>(AId::D)?,
_ => return None,
};
let path = if let Some(node_transform) = linked_node.attribute::<Transform>(AId::Transform) {
let mut path_copy = path.as_ref().clone();
path_copy.transform(node_transform);
Rc::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_len * (start_offset.number / 100.0)
} else {
node.resolve_length(AId::StartOffset, state, 0.0)
};
Some(TextFlow::Path(Rc::new(TextPath { start_offset, path })))
}
fn convert_font(node: svgtree::Node, state: &converter::State) -> Font {
let style: Style = node.find_attribute(AId::FontStyle).unwrap_or_default();
let stretch = conv_font_stretch(node);
let weight = resolve_font_weight(node);
let font_family = if let Some(n) = node.find_node_with_attribute(AId::FontFamily) {
n.attribute::<&str>(AId::FontFamily).unwrap_or("")
} else {
""
};
let mut families = Vec::new();
for mut family in font_family.split(',') {
if family.starts_with('\'') {
family = &family[1..];
}
if family.ends_with('\'') {
family = &family[..family.len() - 1];
}
family = family.trim();
if !family.is_empty() {
families.push(family.to_string());
}
}
if families.is_empty() {
families.push(state.opt.font_family.clone())
}
Font {
families,
style,
stretch,
weight,
}
}
fn conv_font_stretch(node: svgtree::Node) -> Stretch {
if let Some(n) = node.find_node_with_attribute(AId::FontStretch) {
match n.attribute(AId::FontStretch).unwrap_or("") {
"narrower" | "condensed" => Stretch::Condensed,
"ultra-condensed" => Stretch::UltraCondensed,
"extra-condensed" => Stretch::ExtraCondensed,
"semi-condensed" => Stretch::SemiCondensed,
"semi-expanded" => Stretch::SemiExpanded,
"wider" | "expanded" => Stretch::Expanded,
"extra-expanded" => Stretch::ExtraExpanded,
"ultra-expanded" => Stretch::UltraExpanded,
_ => Stretch::Normal,
}
} else {
Stretch::Normal
}
}
fn resolve_font_weight(node: svgtree::Node) -> 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: svgtree::Node,
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() {
let child_chars = count_chars(child);
macro_rules! push_list {
($aid:expr, $field:ident) => {
if let Some(num_list) = crate::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: svgtree::Node) -> Vec<f64> {
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<f64>>(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(
text_node: svgtree::Node,
tspan: svgtree::Node,
state: &converter::State,
cache: &mut converter::Cache,
) -> TextDecoration {
let text_dec = conv_text_decoration(text_node);
let tspan_dec = conv_text_decoration2(tspan);
let mut gen_style = |in_tspan: bool, in_text: bool| {
let n = if in_tspan {
tspan
} else if in_text {
text_node
} else {
return None;
};
Some(TextDecorationStyle {
fill: style::resolve_fill(n, true, state, cache),
stroke: style::resolve_stroke(n, true, state, cache),
})
};
TextDecoration {
underline: gen_style(tspan_dec.has_underline, text_dec.has_underline),
overline: gen_style(tspan_dec.has_overline, text_dec.has_overline),
line_through: gen_style(tspan_dec.has_line_through, text_dec.has_line_through),
}
}
struct TextDecorationTypes {
has_underline: bool,
has_overline: bool,
has_line_through: bool,
}
fn conv_text_decoration(text_node: svgtree::Node) -> TextDecorationTypes {
fn find_decoration(node: svgtree::Node, value: &str) -> bool {
node.ancestors().any(|n| {
if let Some(str_value) = n.attribute::<&str>(AId::TextDecoration) {
str_value.split(' ').any(|v| v == value)
} else {
false
}
})
}
TextDecorationTypes {
has_underline: find_decoration(text_node, "underline"),
has_overline: find_decoration(text_node, "overline"),
has_line_through: find_decoration(text_node, "line-through"),
}
}
fn conv_text_decoration2(tspan: svgtree::Node) -> TextDecorationTypes {
let s = tspan.attribute(AId::TextDecoration);
TextDecorationTypes {
has_underline: s == Some("underline"),
has_overline: s == Some("overline"),
has_line_through: s == Some("line-through"),
}
}
fn convert_baseline_shift(node: svgtree::Node, state: &converter::State) -> Vec<BaselineShift> {
let mut shift = Vec::new();
let nodes: Vec<_> = node
.ancestors()
.take_while(|n| !n.has_tag_name(EId::Text))
.collect();
for n in nodes {
if let Some(len) = n.attribute::<Length>(AId::BaselineShift) {
if len.unit == LengthUnit::Percent {
let n = crate::units::resolve_font_size(n, state) * (len.number / 100.0);
shift.push(BaselineShift::Number(n));
} else {
let n = crate::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: svgtree::Node) -> usize {
node.descendants()
.filter(|n| n.is_text())
.fold(0, |w, n| w + n.text().chars().count())
}
fn convert_writing_mode(text_node: svgtree::Node) -> WritingMode {
if let Some(n) = text_node.find_node_with_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
}
}