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 | |