| 1 | // Copyright 2019 the Resvg Authors |
| 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
| 3 | |
| 4 | use std::sync::Arc; |
| 5 | |
| 6 | use kurbo::{ParamCurve, ParamCurveArclen}; |
| 7 | use svgtypes::{parse_font_families, FontFamily, Length, LengthUnit}; |
| 8 | |
| 9 | use super::svgtree::{AId, EId, FromValue, SvgNode}; |
| 10 | use super::{converter, style, OptionLog}; |
| 11 | use crate::*; |
| 12 | |
| 13 | impl<'a, 'input: 'a> FromValue<'a, 'input> for TextAnchor { |
| 14 | fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> { |
| 15 | match value { |
| 16 | "start" => Some(TextAnchor::Start), |
| 17 | "middle" => Some(TextAnchor::Middle), |
| 18 | "end" => Some(TextAnchor::End), |
| 19 | _ => None, |
| 20 | } |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | impl<'a, 'input: 'a> FromValue<'a, 'input> for AlignmentBaseline { |
| 25 | fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> { |
| 26 | match value { |
| 27 | "auto" => Some(AlignmentBaseline::Auto), |
| 28 | "baseline" => Some(AlignmentBaseline::Baseline), |
| 29 | "before-edge" => Some(AlignmentBaseline::BeforeEdge), |
| 30 | "text-before-edge" => Some(AlignmentBaseline::TextBeforeEdge), |
| 31 | "middle" => Some(AlignmentBaseline::Middle), |
| 32 | "central" => Some(AlignmentBaseline::Central), |
| 33 | "after-edge" => Some(AlignmentBaseline::AfterEdge), |
| 34 | "text-after-edge" => Some(AlignmentBaseline::TextAfterEdge), |
| 35 | "ideographic" => Some(AlignmentBaseline::Ideographic), |
| 36 | "alphabetic" => Some(AlignmentBaseline::Alphabetic), |
| 37 | "hanging" => Some(AlignmentBaseline::Hanging), |
| 38 | "mathematical" => Some(AlignmentBaseline::Mathematical), |
| 39 | _ => None, |
| 40 | } |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | impl<'a, 'input: 'a> FromValue<'a, 'input> for DominantBaseline { |
| 45 | fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> { |
| 46 | match value { |
| 47 | "auto" => Some(DominantBaseline::Auto), |
| 48 | "use-script" => Some(DominantBaseline::UseScript), |
| 49 | "no-change" => Some(DominantBaseline::NoChange), |
| 50 | "reset-size" => Some(DominantBaseline::ResetSize), |
| 51 | "ideographic" => Some(DominantBaseline::Ideographic), |
| 52 | "alphabetic" => Some(DominantBaseline::Alphabetic), |
| 53 | "hanging" => Some(DominantBaseline::Hanging), |
| 54 | "mathematical" => Some(DominantBaseline::Mathematical), |
| 55 | "central" => Some(DominantBaseline::Central), |
| 56 | "middle" => Some(DominantBaseline::Middle), |
| 57 | "text-after-edge" => Some(DominantBaseline::TextAfterEdge), |
| 58 | "text-before-edge" => Some(DominantBaseline::TextBeforeEdge), |
| 59 | _ => None, |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | impl<'a, 'input: 'a> FromValue<'a, 'input> for LengthAdjust { |
| 65 | fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> { |
| 66 | match value { |
| 67 | "spacing" => Some(LengthAdjust::Spacing), |
| 68 | "spacingAndGlyphs" => Some(LengthAdjust::SpacingAndGlyphs), |
| 69 | _ => None, |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | impl<'a, 'input: 'a> FromValue<'a, 'input> for FontStyle { |
| 75 | fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> { |
| 76 | match value { |
| 77 | "normal" => Some(FontStyle::Normal), |
| 78 | "italic" => Some(FontStyle::Italic), |
| 79 | "oblique" => Some(FontStyle::Oblique), |
| 80 | _ => None, |
| 81 | } |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | /// A text character position. |
| 86 | /// |
| 87 | /// _Character_ is a Unicode codepoint. |
| 88 | #[derive (Clone, Copy, Debug)] |
| 89 | struct CharacterPosition { |
| 90 | /// An absolute X axis position. |
| 91 | x: Option<f32>, |
| 92 | /// An absolute Y axis position. |
| 93 | y: Option<f32>, |
| 94 | /// A relative X axis offset. |
| 95 | dx: Option<f32>, |
| 96 | /// A relative Y axis offset. |
| 97 | dy: Option<f32>, |
| 98 | } |
| 99 | |
| 100 | pub(crate) fn convert( |
| 101 | text_node: SvgNode, |
| 102 | state: &converter::State, |
| 103 | cache: &mut converter::Cache, |
| 104 | parent: &mut Group, |
| 105 | ) { |
| 106 | let pos_list = resolve_positions_list(text_node, state); |
| 107 | let rotate_list = resolve_rotate_list(text_node); |
| 108 | let writing_mode = convert_writing_mode(text_node); |
| 109 | |
| 110 | let chunks = collect_text_chunks(text_node, &pos_list, state, cache); |
| 111 | |
| 112 | let rendering_mode: TextRendering = text_node |
| 113 | .find_attribute(AId::TextRendering) |
| 114 | .unwrap_or(state.opt.text_rendering); |
| 115 | |
| 116 | // Nodes generated by markers must not have an ID. Otherwise we would have duplicates. |
| 117 | let id = if state.parent_markers.is_empty() { |
| 118 | text_node.element_id().to_string() |
| 119 | } else { |
| 120 | String::new() |
| 121 | }; |
| 122 | |
| 123 | let dummy = Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap(); |
| 124 | |
| 125 | let mut text = Text { |
| 126 | id, |
| 127 | rendering_mode, |
| 128 | dx: pos_list.iter().map(|v| v.dx.unwrap_or(0.0)).collect(), |
| 129 | dy: pos_list.iter().map(|v| v.dy.unwrap_or(0.0)).collect(), |
| 130 | rotate: rotate_list, |
| 131 | writing_mode, |
| 132 | chunks, |
| 133 | abs_transform: parent.abs_transform, |
| 134 | // All fields below will be reset by `text_to_paths`. |
| 135 | bounding_box: dummy, |
| 136 | abs_bounding_box: dummy, |
| 137 | stroke_bounding_box: dummy, |
| 138 | abs_stroke_bounding_box: dummy, |
| 139 | flattened: Box::new(Group::empty()), |
| 140 | layouted: vec![], |
| 141 | }; |
| 142 | |
| 143 | if text::convert(&mut text, &state.opt.font_resolver, &mut cache.fontdb).is_none() { |
| 144 | return; |
| 145 | } |
| 146 | |
| 147 | parent.children.push(Node::Text(Box::new(text))); |
| 148 | } |
| 149 | |
| 150 | struct IterState { |
| 151 | chars_count: usize, |
| 152 | chunk_bytes_count: usize, |
| 153 | split_chunk: bool, |
| 154 | text_flow: TextFlow, |
| 155 | chunks: Vec<TextChunk>, |
| 156 | } |
| 157 | |
| 158 | fn collect_text_chunks( |
| 159 | text_node: SvgNode, |
| 160 | pos_list: &[CharacterPosition], |
| 161 | state: &converter::State, |
| 162 | cache: &mut converter::Cache, |
| 163 | ) -> Vec<TextChunk> { |
| 164 | let mut iter_state: IterState = IterState { |
| 165 | chars_count: 0, |
| 166 | chunk_bytes_count: 0, |
| 167 | split_chunk: false, |
| 168 | text_flow: TextFlow::Linear, |
| 169 | chunks: Vec::new(), |
| 170 | }; |
| 171 | |
| 172 | collect_text_chunks_impl(parent:text_node, pos_list, state, cache, &mut iter_state); |
| 173 | |
| 174 | iter_state.chunks |
| 175 | } |
| 176 | |
| 177 | fn collect_text_chunks_impl( |
| 178 | parent: SvgNode, |
| 179 | pos_list: &[CharacterPosition], |
| 180 | state: &converter::State, |
| 181 | cache: &mut converter::Cache, |
| 182 | iter_state: &mut IterState, |
| 183 | ) { |
| 184 | for child in parent.children() { |
| 185 | if child.is_element() { |
| 186 | if child.tag_name() == Some(EId::TextPath) { |
| 187 | if parent.tag_name() != Some(EId::Text) { |
| 188 | // `textPath` can be set only as a direct `text` element child. |
| 189 | iter_state.chars_count += count_chars(child); |
| 190 | continue; |
| 191 | } |
| 192 | |
| 193 | match resolve_text_flow(child, state) { |
| 194 | Some(v) => { |
| 195 | iter_state.text_flow = v; |
| 196 | } |
| 197 | None => { |
| 198 | // Skip an invalid text path and all it's children. |
| 199 | // We have to update the chars count, |
| 200 | // because `pos_list` was calculated including this text path. |
| 201 | iter_state.chars_count += count_chars(child); |
| 202 | continue; |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | iter_state.split_chunk = true; |
| 207 | } |
| 208 | |
| 209 | collect_text_chunks_impl(child, pos_list, state, cache, iter_state); |
| 210 | |
| 211 | iter_state.text_flow = TextFlow::Linear; |
| 212 | |
| 213 | // Next char after `textPath` should be split too. |
| 214 | if child.tag_name() == Some(EId::TextPath) { |
| 215 | iter_state.split_chunk = true; |
| 216 | } |
| 217 | |
| 218 | continue; |
| 219 | } |
| 220 | |
| 221 | if !parent.is_visible_element(state.opt) { |
| 222 | iter_state.chars_count += child.text().chars().count(); |
| 223 | continue; |
| 224 | } |
| 225 | |
| 226 | let anchor = parent.find_attribute(AId::TextAnchor).unwrap_or_default(); |
| 227 | |
| 228 | // TODO: what to do when <= 0? UB? |
| 229 | let font_size = super::units::resolve_font_size(parent, state); |
| 230 | let font_size = match NonZeroPositiveF32::new(font_size) { |
| 231 | Some(n) => n, |
| 232 | None => { |
| 233 | // Skip this span. |
| 234 | iter_state.chars_count += child.text().chars().count(); |
| 235 | continue; |
| 236 | } |
| 237 | }; |
| 238 | |
| 239 | let font = convert_font(parent, state); |
| 240 | |
| 241 | let raw_paint_order: svgtypes::PaintOrder = |
| 242 | parent.find_attribute(AId::PaintOrder).unwrap_or_default(); |
| 243 | let paint_order = super::converter::svg_paint_order_to_usvg(raw_paint_order); |
| 244 | |
| 245 | let mut dominant_baseline = parent |
| 246 | .find_attribute(AId::DominantBaseline) |
| 247 | .unwrap_or_default(); |
| 248 | |
| 249 | // `no-change` means "use parent". |
| 250 | if dominant_baseline == DominantBaseline::NoChange { |
| 251 | dominant_baseline = parent |
| 252 | .parent_element() |
| 253 | .unwrap() |
| 254 | .find_attribute(AId::DominantBaseline) |
| 255 | .unwrap_or_default(); |
| 256 | } |
| 257 | |
| 258 | let mut apply_kerning = true; |
| 259 | #[allow (clippy::if_same_then_else)] |
| 260 | if parent.resolve_length(AId::Kerning, state, -1.0) == 0.0 { |
| 261 | apply_kerning = false; |
| 262 | } else if parent.find_attribute::<&str>(AId::FontKerning) == Some("none" ) { |
| 263 | apply_kerning = false; |
| 264 | } |
| 265 | |
| 266 | let mut text_length = |
| 267 | parent.try_convert_length(AId::TextLength, Units::UserSpaceOnUse, state); |
| 268 | // Negative values should be ignored. |
| 269 | if let Some(n) = text_length { |
| 270 | if n < 0.0 { |
| 271 | text_length = None; |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | let visibility: Visibility = parent.find_attribute(AId::Visibility).unwrap_or_default(); |
| 276 | |
| 277 | let span = TextSpan { |
| 278 | start: 0, |
| 279 | end: 0, |
| 280 | fill: style::resolve_fill(parent, true, state, cache), |
| 281 | stroke: style::resolve_stroke(parent, true, state, cache), |
| 282 | paint_order, |
| 283 | font, |
| 284 | font_size, |
| 285 | small_caps: parent.find_attribute::<&str>(AId::FontVariant) == Some("small-caps" ), |
| 286 | apply_kerning, |
| 287 | decoration: resolve_decoration(parent, state, cache), |
| 288 | visible: visibility == Visibility::Visible, |
| 289 | dominant_baseline, |
| 290 | alignment_baseline: parent |
| 291 | .find_attribute(AId::AlignmentBaseline) |
| 292 | .unwrap_or_default(), |
| 293 | baseline_shift: convert_baseline_shift(parent, state), |
| 294 | letter_spacing: parent.resolve_length(AId::LetterSpacing, state, 0.0), |
| 295 | word_spacing: parent.resolve_length(AId::WordSpacing, state, 0.0), |
| 296 | text_length, |
| 297 | length_adjust: parent.find_attribute(AId::LengthAdjust).unwrap_or_default(), |
| 298 | }; |
| 299 | |
| 300 | let mut is_new_span = true; |
| 301 | for c in child.text().chars() { |
| 302 | let char_len = c.len_utf8(); |
| 303 | |
| 304 | // Create a new chunk if: |
| 305 | // - this is the first span (yes, position can be None) |
| 306 | // - text character has an absolute coordinate assigned to it (via x/y attribute) |
| 307 | // - `c` is the first char of the `textPath` |
| 308 | // - `c` is the first char after `textPath` |
| 309 | let is_new_chunk = pos_list[iter_state.chars_count].x.is_some() |
| 310 | || pos_list[iter_state.chars_count].y.is_some() |
| 311 | || iter_state.split_chunk |
| 312 | || iter_state.chunks.is_empty(); |
| 313 | |
| 314 | iter_state.split_chunk = false; |
| 315 | |
| 316 | if is_new_chunk { |
| 317 | iter_state.chunk_bytes_count = 0; |
| 318 | |
| 319 | let mut span2 = span.clone(); |
| 320 | span2.start = 0; |
| 321 | span2.end = char_len; |
| 322 | |
| 323 | iter_state.chunks.push(TextChunk { |
| 324 | x: pos_list[iter_state.chars_count].x, |
| 325 | y: pos_list[iter_state.chars_count].y, |
| 326 | anchor, |
| 327 | spans: vec![span2], |
| 328 | text_flow: iter_state.text_flow.clone(), |
| 329 | text: c.to_string(), |
| 330 | }); |
| 331 | } else if is_new_span { |
| 332 | // Add this span to the last text chunk. |
| 333 | let mut span2 = span.clone(); |
| 334 | span2.start = iter_state.chunk_bytes_count; |
| 335 | span2.end = iter_state.chunk_bytes_count + char_len; |
| 336 | |
| 337 | if let Some(chunk) = iter_state.chunks.last_mut() { |
| 338 | chunk.text.push(c); |
| 339 | chunk.spans.push(span2); |
| 340 | } |
| 341 | } else { |
| 342 | // Extend the last span. |
| 343 | if let Some(chunk) = iter_state.chunks.last_mut() { |
| 344 | chunk.text.push(c); |
| 345 | if let Some(span) = chunk.spans.last_mut() { |
| 346 | debug_assert_ne!(span.end, 0); |
| 347 | span.end += char_len; |
| 348 | } |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | is_new_span = false; |
| 353 | iter_state.chars_count += 1; |
| 354 | iter_state.chunk_bytes_count += char_len; |
| 355 | } |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option<TextFlow> { |
| 360 | let linked_node = node.attribute::<SvgNode>(AId::Href)?; |
| 361 | let path = super::shapes::convert(linked_node, state)?; |
| 362 | |
| 363 | // The reference path's transform needs to be applied |
| 364 | let transform = linked_node.resolve_transform(AId::Transform, state); |
| 365 | let path = if !transform.is_identity() { |
| 366 | let mut path_copy = path.as_ref().clone(); |
| 367 | path_copy = path_copy.transform(transform)?; |
| 368 | Arc::new(path_copy) |
| 369 | } else { |
| 370 | path |
| 371 | }; |
| 372 | |
| 373 | let start_offset: Length = node.attribute(AId::StartOffset).unwrap_or_default(); |
| 374 | let start_offset = if start_offset.unit == LengthUnit::Percent { |
| 375 | // 'If a percentage is given, then the `startOffset` represents |
| 376 | // a percentage distance along the entire path.' |
| 377 | let path_len = path_length(&path); |
| 378 | (path_len * (start_offset.number / 100.0)) as f32 |
| 379 | } else { |
| 380 | node.resolve_length(AId::StartOffset, state, 0.0) |
| 381 | }; |
| 382 | |
| 383 | let id = NonEmptyString::new(linked_node.element_id().to_string())?; |
| 384 | Some(TextFlow::Path(Arc::new(TextPath { |
| 385 | id, |
| 386 | start_offset, |
| 387 | path, |
| 388 | }))) |
| 389 | } |
| 390 | |
| 391 | fn convert_font(node: SvgNode, state: &converter::State) -> Font { |
| 392 | let style: FontStyle = node.find_attribute(AId::FontStyle).unwrap_or_default(); |
| 393 | let stretch = conv_font_stretch(node); |
| 394 | let weight = resolve_font_weight(node); |
| 395 | |
| 396 | let font_families = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily)) |
| 397 | { |
| 398 | n.attribute(AId::FontFamily).unwrap_or("" ) |
| 399 | } else { |
| 400 | "" |
| 401 | }; |
| 402 | |
| 403 | let mut families = parse_font_families(font_families) |
| 404 | .ok() |
| 405 | .log_none(|| { |
| 406 | log::warn!( |
| 407 | "Failed to parse {} value: ' {}'. Falling back to {}." , |
| 408 | AId::FontFamily, |
| 409 | font_families, |
| 410 | state.opt.font_family |
| 411 | ) |
| 412 | }) |
| 413 | .unwrap_or_default(); |
| 414 | |
| 415 | if families.is_empty() { |
| 416 | families.push(FontFamily::Named(state.opt.font_family.clone())) |
| 417 | } |
| 418 | |
| 419 | Font { |
| 420 | families, |
| 421 | style, |
| 422 | stretch, |
| 423 | weight, |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | // TODO: properly resolve narrower/wider |
| 428 | fn conv_font_stretch(node: SvgNode) -> FontStretch { |
| 429 | if let Some(n: SvgNode<'_, '_>) = node.ancestors().find(|n: &SvgNode<'_, '_>| n.has_attribute(AId::FontStretch)) { |
| 430 | match n.attribute(AId::FontStretch).unwrap_or(default:"" ) { |
| 431 | "narrower" | "condensed" => FontStretch::Condensed, |
| 432 | "ultra-condensed" => FontStretch::UltraCondensed, |
| 433 | "extra-condensed" => FontStretch::ExtraCondensed, |
| 434 | "semi-condensed" => FontStretch::SemiCondensed, |
| 435 | "semi-expanded" => FontStretch::SemiExpanded, |
| 436 | "wider" | "expanded" => FontStretch::Expanded, |
| 437 | "extra-expanded" => FontStretch::ExtraExpanded, |
| 438 | "ultra-expanded" => FontStretch::UltraExpanded, |
| 439 | _ => FontStretch::Normal, |
| 440 | } |
| 441 | } else { |
| 442 | FontStretch::Normal |
| 443 | } |
| 444 | } |
| 445 | |
| 446 | fn resolve_font_weight(node: SvgNode) -> u16 { |
| 447 | fn bound(min: usize, val: usize, max: usize) -> usize { |
| 448 | std::cmp::max(min, std::cmp::min(max, val)) |
| 449 | } |
| 450 | |
| 451 | let nodes: Vec<_> = node.ancestors().collect(); |
| 452 | let mut weight = 400; |
| 453 | for n in nodes.iter().rev().skip(1) { |
| 454 | // skip Root |
| 455 | weight = match n.attribute(AId::FontWeight).unwrap_or("" ) { |
| 456 | "normal" => 400, |
| 457 | "bold" => 700, |
| 458 | "100" => 100, |
| 459 | "200" => 200, |
| 460 | "300" => 300, |
| 461 | "400" => 400, |
| 462 | "500" => 500, |
| 463 | "600" => 600, |
| 464 | "700" => 700, |
| 465 | "800" => 800, |
| 466 | "900" => 900, |
| 467 | "bolder" => { |
| 468 | // By the CSS2 spec the default value should be 400 |
| 469 | // so `bolder` will result in 500. |
| 470 | // But Chrome and Inkscape will give us 700. |
| 471 | // Have no idea is it a bug or something, but |
| 472 | // we will follow such behavior for now. |
| 473 | let step = if weight == 400 { 300 } else { 100 }; |
| 474 | |
| 475 | bound(100, weight + step, 900) |
| 476 | } |
| 477 | "lighter" => { |
| 478 | // By the CSS2 spec the default value should be 400 |
| 479 | // so `lighter` will result in 300. |
| 480 | // But Chrome and Inkscape will give us 200. |
| 481 | // Have no idea is it a bug or something, but |
| 482 | // we will follow such behavior for now. |
| 483 | let step = if weight == 400 { 200 } else { 100 }; |
| 484 | |
| 485 | bound(100, weight - step, 900) |
| 486 | } |
| 487 | _ => weight, |
| 488 | }; |
| 489 | } |
| 490 | |
| 491 | weight as u16 |
| 492 | } |
| 493 | |
| 494 | /// Resolves text's character positions. |
| 495 | /// |
| 496 | /// This includes: x, y, dx, dy. |
| 497 | /// |
| 498 | /// # The character |
| 499 | /// |
| 500 | /// The first problem with this task is that the *character* itself |
| 501 | /// is basically undefined in the SVG spec. Sometimes it's an *XML character*, |
| 502 | /// sometimes a *glyph*, and sometimes just a *character*. |
| 503 | /// |
| 504 | /// There is an ongoing [discussion](https://github.com/w3c/svgwg/issues/537) |
| 505 | /// on the SVG working group that addresses this by stating that a character |
| 506 | /// is a Unicode code point. But it's not final. |
| 507 | /// |
| 508 | /// Also, according to the SVG 2 spec, *character* is *a Unicode code point*. |
| 509 | /// |
| 510 | /// Anyway, we treat a character as a Unicode code point. |
| 511 | /// |
| 512 | /// # Algorithm |
| 513 | /// |
| 514 | /// To resolve positions, we have to iterate over descendant nodes and |
| 515 | /// if the current node is a `tspan` and has x/y/dx/dy attribute, |
| 516 | /// than the positions from this attribute should be assigned to the characters |
| 517 | /// of this `tspan` and it's descendants. |
| 518 | /// |
| 519 | /// Positions list can have more values than characters in the `tspan`, |
| 520 | /// so we have to clamp it, because values should not overlap, e.g.: |
| 521 | /// |
| 522 | /// (we ignore whitespaces for example purposes, |
| 523 | /// so the `text` content is `Text` and not `T ex t`) |
| 524 | /// |
| 525 | /// ```text |
| 526 | /// <text> |
| 527 | /// a |
| 528 | /// <tspan x="10 20 30"> |
| 529 | /// bc |
| 530 | /// </tspan> |
| 531 | /// d |
| 532 | /// </text> |
| 533 | /// ``` |
| 534 | /// |
| 535 | /// In this example, the `d` position should not be set to `30`. |
| 536 | /// And the result should be: `[None, 10, 20, None]` |
| 537 | /// |
| 538 | /// Another example: |
| 539 | /// |
| 540 | /// ```text |
| 541 | /// <text> |
| 542 | /// <tspan x="100 110 120 130"> |
| 543 | /// a |
| 544 | /// <tspan x="50"> |
| 545 | /// bc |
| 546 | /// </tspan> |
| 547 | /// </tspan> |
| 548 | /// d |
| 549 | /// </text> |
| 550 | /// ``` |
| 551 | /// |
| 552 | /// The result should be: `[100, 50, 120, None]` |
| 553 | fn resolve_positions_list(text_node: SvgNode, state: &converter::State) -> Vec<CharacterPosition> { |
| 554 | // Allocate a list that has all characters positions set to `None`. |
| 555 | let total_chars = count_chars(text_node); |
| 556 | let mut list = vec![ |
| 557 | CharacterPosition { |
| 558 | x: None, |
| 559 | y: None, |
| 560 | dx: None, |
| 561 | dy: None, |
| 562 | }; |
| 563 | total_chars |
| 564 | ]; |
| 565 | |
| 566 | let mut offset = 0; |
| 567 | for child in text_node.descendants() { |
| 568 | if child.is_element() { |
| 569 | // We must ignore text positions on `textPath`. |
| 570 | if !matches!(child.tag_name(), Some(EId::Text) | Some(EId::Tspan)) { |
| 571 | continue; |
| 572 | } |
| 573 | |
| 574 | let child_chars = count_chars(child); |
| 575 | macro_rules! push_list { |
| 576 | ($aid:expr, $field:ident) => { |
| 577 | if let Some(num_list) = super::units::convert_list(child, $aid, state) { |
| 578 | // Note that we are using not the total count, |
| 579 | // but the amount of characters in the current `tspan` (with children). |
| 580 | let len = std::cmp::min(num_list.len(), child_chars); |
| 581 | for i in 0..len { |
| 582 | list[offset + i].$field = Some(num_list[i]); |
| 583 | } |
| 584 | } |
| 585 | }; |
| 586 | } |
| 587 | |
| 588 | push_list!(AId::X, x); |
| 589 | push_list!(AId::Y, y); |
| 590 | push_list!(AId::Dx, dx); |
| 591 | push_list!(AId::Dy, dy); |
| 592 | } else if child.is_text() { |
| 593 | // Advance the offset. |
| 594 | offset += child.text().chars().count(); |
| 595 | } |
| 596 | } |
| 597 | |
| 598 | list |
| 599 | } |
| 600 | |
| 601 | /// Resolves characters rotation. |
| 602 | /// |
| 603 | /// The algorithm is well explained |
| 604 | /// [in the SVG spec](https://www.w3.org/TR/SVG11/text.html#TSpanElement) (scroll down a bit). |
| 605 | /// |
| 606 | ///  |
| 607 | /// |
| 608 | /// Note: this algorithm differs from the position resolving one. |
| 609 | fn resolve_rotate_list(text_node: SvgNode) -> Vec<f32> { |
| 610 | // Allocate a list that has all characters angles set to `0.0`. |
| 611 | let mut list = vec![0.0; count_chars(text_node)]; |
| 612 | let mut last = 0.0; |
| 613 | let mut offset = 0; |
| 614 | for child in text_node.descendants() { |
| 615 | if child.is_element() { |
| 616 | if let Some(rotate) = child.attribute::<Vec<f32>>(AId::Rotate) { |
| 617 | for i in 0..count_chars(child) { |
| 618 | if let Some(a) = rotate.get(i).cloned() { |
| 619 | list[offset + i] = a; |
| 620 | last = a; |
| 621 | } else { |
| 622 | // If the rotate list doesn't specify the rotation for |
| 623 | // this character - use the last one. |
| 624 | list[offset + i] = last; |
| 625 | } |
| 626 | } |
| 627 | } |
| 628 | } else if child.is_text() { |
| 629 | // Advance the offset. |
| 630 | offset += child.text().chars().count(); |
| 631 | } |
| 632 | } |
| 633 | |
| 634 | list |
| 635 | } |
| 636 | |
| 637 | /// Resolves node's `text-decoration` property. |
| 638 | fn resolve_decoration( |
| 639 | tspan: SvgNode, |
| 640 | state: &converter::State, |
| 641 | cache: &mut converter::Cache, |
| 642 | ) -> TextDecoration { |
| 643 | // Checks if a decoration is present in a single node. |
| 644 | fn find_decoration(node: SvgNode, value: &str) -> bool { |
| 645 | if let Some(str_value) = node.attribute::<&str>(AId::TextDecoration) { |
| 646 | str_value.split(' ' ).any(|v| v == value) |
| 647 | } else { |
| 648 | false |
| 649 | } |
| 650 | } |
| 651 | |
| 652 | // The algorithm is as follows: First, we check whether the given text decoration appears in ANY |
| 653 | // ancestor, i.e. it can also appear in ancestors outside of the <text> element. If the text |
| 654 | // decoration is declared somewhere, it means that this tspan will have it. However, we still |
| 655 | // need to find the corresponding fill/stroke for it. To do this, we iterate through all |
| 656 | // ancestors (i.e. tspans) until we find the text decoration declared. If not, we will |
| 657 | // stop at latest at the text node, and use its fill/stroke. |
| 658 | let mut gen_style = |text_decoration: &str| { |
| 659 | if !tspan |
| 660 | .ancestors() |
| 661 | .any(|n| find_decoration(n, text_decoration)) |
| 662 | { |
| 663 | return None; |
| 664 | } |
| 665 | |
| 666 | let mut fill_node = None; |
| 667 | let mut stroke_node = None; |
| 668 | |
| 669 | for node in tspan.ancestors() { |
| 670 | if find_decoration(node, text_decoration) || node.tag_name() == Some(EId::Text) { |
| 671 | fill_node = fill_node.map_or(Some(node), Some); |
| 672 | stroke_node = stroke_node.map_or(Some(node), Some); |
| 673 | break; |
| 674 | } |
| 675 | } |
| 676 | |
| 677 | Some(TextDecorationStyle { |
| 678 | fill: fill_node.and_then(|node| style::resolve_fill(node, true, state, cache)), |
| 679 | stroke: stroke_node.and_then(|node| style::resolve_stroke(node, true, state, cache)), |
| 680 | }) |
| 681 | }; |
| 682 | |
| 683 | TextDecoration { |
| 684 | underline: gen_style("underline" ), |
| 685 | overline: gen_style("overline" ), |
| 686 | line_through: gen_style("line-through" ), |
| 687 | } |
| 688 | } |
| 689 | |
| 690 | fn convert_baseline_shift(node: SvgNode, state: &converter::State) -> Vec<BaselineShift> { |
| 691 | let mut shift = Vec::new(); |
| 692 | let nodes: Vec<_> = node |
| 693 | .ancestors() |
| 694 | .take_while(|n| n.tag_name() != Some(EId::Text)) |
| 695 | .collect(); |
| 696 | for n in nodes { |
| 697 | if let Some(len) = n.try_attribute::<Length>(AId::BaselineShift) { |
| 698 | if len.unit == LengthUnit::Percent { |
| 699 | let n = super::units::resolve_font_size(n, state) * (len.number as f32 / 100.0); |
| 700 | shift.push(BaselineShift::Number(n)); |
| 701 | } else { |
| 702 | let n = super::units::convert_length( |
| 703 | len, |
| 704 | n, |
| 705 | AId::BaselineShift, |
| 706 | Units::ObjectBoundingBox, |
| 707 | state, |
| 708 | ); |
| 709 | shift.push(BaselineShift::Number(n)); |
| 710 | } |
| 711 | } else if let Some(s) = n.attribute(AId::BaselineShift) { |
| 712 | match s { |
| 713 | "sub" => shift.push(BaselineShift::Subscript), |
| 714 | "super" => shift.push(BaselineShift::Superscript), |
| 715 | _ => shift.push(BaselineShift::Baseline), |
| 716 | } |
| 717 | } |
| 718 | } |
| 719 | |
| 720 | if shift |
| 721 | .iter() |
| 722 | .all(|base| matches!(base, BaselineShift::Baseline)) |
| 723 | { |
| 724 | shift.clear(); |
| 725 | } |
| 726 | |
| 727 | shift |
| 728 | } |
| 729 | |
| 730 | fn count_chars(node: SvgNode) -> usize { |
| 731 | node.descendants() |
| 732 | .filter(|n| n.is_text()) |
| 733 | .fold(init:0, |w: usize, n: SvgNode<'_, '_>| w + n.text().chars().count()) |
| 734 | } |
| 735 | |
| 736 | /// Converts the writing mode. |
| 737 | /// |
| 738 | /// [SVG 2] references [CSS Writing Modes Level 3] for the definition of the |
| 739 | /// 'writing-mode' property, there are only two writing modes: |
| 740 | /// horizontal left-to-right and vertical right-to-left. |
| 741 | /// |
| 742 | /// That specification introduces new values for the property. The SVG 1.1 |
| 743 | /// values are obsolete but must still be supported by converting the specified |
| 744 | /// values to computed values as follows: |
| 745 | /// |
| 746 | /// - `lr`, `lr-tb`, `rl`, `rl-tb` => `horizontal-tb` |
| 747 | /// - `tb`, `tb-rl` => `vertical-rl` |
| 748 | /// |
| 749 | /// The current `vertical-lr` behaves exactly the same as `vertical-rl`. |
| 750 | /// |
| 751 | /// Also, looks like no one really supports the `rl` and `rl-tb`, except `Batik`. |
| 752 | /// And I'm not sure if its behaviour is correct. |
| 753 | /// |
| 754 | /// So we will ignore it as well, mainly because I have no idea how exactly |
| 755 | /// it should affect the rendering. |
| 756 | /// |
| 757 | /// [SVG 2]: https://www.w3.org/TR/SVG2/text.html#WritingModeProperty |
| 758 | /// [CSS Writing Modes Level 3]: https://www.w3.org/TR/css-writing-modes-3/#svg-writing-mode-css |
| 759 | fn convert_writing_mode(text_node: SvgNode) -> WritingMode { |
| 760 | if let Some(n: SvgNode<'_, '_>) = text_nodeAncestors<'_, '_> |
| 761 | .ancestors() |
| 762 | .find(|n: &SvgNode<'_, '_>| n.has_attribute(AId::WritingMode)) |
| 763 | { |
| 764 | match n.attribute(AId::WritingMode).unwrap_or(default:"lr-tb" ) { |
| 765 | "tb" | "tb-rl" | "vertical-rl" | "vertical-lr" => WritingMode::TopToBottom, |
| 766 | _ => WritingMode::LeftToRight, |
| 767 | } |
| 768 | } else { |
| 769 | WritingMode::LeftToRight |
| 770 | } |
| 771 | } |
| 772 | |
| 773 | fn path_length(path: &tiny_skia_path::Path) -> f64 { |
| 774 | let mut prev_mx = path.points()[0].x; |
| 775 | let mut prev_my = path.points()[0].y; |
| 776 | let mut prev_x = prev_mx; |
| 777 | let mut prev_y = prev_my; |
| 778 | |
| 779 | fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez { |
| 780 | let line = kurbo::Line::new( |
| 781 | kurbo::Point::new(px as f64, py as f64), |
| 782 | kurbo::Point::new(x as f64, y as f64), |
| 783 | ); |
| 784 | let p1 = line.eval(0.33); |
| 785 | let p2 = line.eval(0.66); |
| 786 | kurbo::CubicBez::new(line.p0, p1, p2, line.p1) |
| 787 | } |
| 788 | |
| 789 | let mut length = 0.0; |
| 790 | for seg in path.segments() { |
| 791 | let curve = match seg { |
| 792 | tiny_skia_path::PathSegment::MoveTo(p) => { |
| 793 | prev_mx = p.x; |
| 794 | prev_my = p.y; |
| 795 | prev_x = p.x; |
| 796 | prev_y = p.y; |
| 797 | continue; |
| 798 | } |
| 799 | tiny_skia_path::PathSegment::LineTo(p) => { |
| 800 | create_curve_from_line(prev_x, prev_y, p.x, p.y) |
| 801 | } |
| 802 | tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez::new( |
| 803 | kurbo::Point::new(prev_x as f64, prev_y as f64), |
| 804 | kurbo::Point::new(p1.x as f64, p1.y as f64), |
| 805 | kurbo::Point::new(p.x as f64, p.y as f64), |
| 806 | ) |
| 807 | .raise(), |
| 808 | tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez::new( |
| 809 | kurbo::Point::new(prev_x as f64, prev_y as f64), |
| 810 | kurbo::Point::new(p1.x as f64, p1.y as f64), |
| 811 | kurbo::Point::new(p2.x as f64, p2.y as f64), |
| 812 | kurbo::Point::new(p.x as f64, p.y as f64), |
| 813 | ), |
| 814 | tiny_skia_path::PathSegment::Close => { |
| 815 | create_curve_from_line(prev_x, prev_y, prev_mx, prev_my) |
| 816 | } |
| 817 | }; |
| 818 | |
| 819 | length += curve.arclen(0.5); |
| 820 | prev_x = curve.p3.x as f32; |
| 821 | prev_y = curve.p3.y as f32; |
| 822 | } |
| 823 | |
| 824 | length |
| 825 | } |
| 826 | |