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