1use cssparser::Parser;
3use markup5ever::{expanded_name, local_name, ns};
4use pango::IsAttribute;
5use rctree::NodeEdge;
6
7use crate::element::{set_attribute, Element, ElementData, ElementTrait};
8use crate::error::ParseError;
9use crate::layout::FontProperties;
10use crate::length::{Horizontal, Length, NormalizeParams, Vertical};
11use crate::node::{Node, NodeData};
12use crate::parsers::{CommaSeparatedList, Parse, ParseValue};
13use crate::properties::WhiteSpace;
14use crate::session::Session;
15use crate::text::BidiControl;
16use crate::xml;
17use crate::{parse_identifiers, rsvg_log};
18
19type OptionalLengthList<N> = Option<CommaSeparatedList<Length<N>, 1, 4096>>;
35
36type OptionalRotateList = Option<CommaSeparatedList<f64, 1, 4096>>;
42
43#[derive(Debug, Default, Copy, Clone, PartialEq)]
47enum LengthAdjust {
48 #[default]
49 Spacing,
50 SpacingAndGlyphs,
51}
52
53impl Parse for LengthAdjust {
54 fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
55 Ok(parse_identifiers!(
56 parser,
57 "spacing" => LengthAdjust::Spacing,
58 "spacingAndGlyphs" => LengthAdjust::SpacingAndGlyphs,
59 )?)
60 }
61}
62
63#[allow(dead_code)]
64#[derive(Default)]
65pub struct Text2 {
66 x: OptionalLengthList<Horizontal>,
67 y: OptionalLengthList<Vertical>,
68 dx: OptionalLengthList<Horizontal>,
69 dy: OptionalLengthList<Vertical>,
70 rotate: OptionalRotateList,
71 text_length: Length<Horizontal>,
72 length_adjust: LengthAdjust, }
74
75impl ElementTrait for Text2 {
91 fn set_attributes(&mut self, attrs: &xml::Attributes, session: &Session) {
92 for (attr, value) in attrs.iter() {
93 match attr.expanded() {
94 expanded_name!("", "x") => set_attribute(&mut self.x, attr.parse(value), session),
95 expanded_name!("", "y") => set_attribute(&mut self.y, attr.parse(value), session),
96 expanded_name!("", "dx") => set_attribute(&mut self.dx, attr.parse(value), session),
97 expanded_name!("", "dy") => set_attribute(&mut self.dy, attr.parse(value), session),
98 expanded_name!("", "rotate") => {
99 set_attribute(&mut self.rotate, attr.parse(value), session)
100 }
101 expanded_name!("", "textLength") => {
102 set_attribute(&mut self.text_length, attr.parse(value), session)
103 }
104 expanded_name!("", "lengthAdjust") => {
105 set_attribute(&mut self.length_adjust, attr.parse(value), session)
106 }
107 _ => (),
108 }
109 }
110 }
111}
112
113#[derive(Default)]
114#[allow(dead_code)]
115struct Character {
116 addressable: bool,
125 character: char,
126 }
130
131#[allow(unused)]
140fn collapse_white_space(input: &str, white_space: WhiteSpace) -> Vec<Character> {
141 match white_space {
142 WhiteSpace::Normal | WhiteSpace::NoWrap => compute_normal_nowrap(input),
143 WhiteSpace::Pre | WhiteSpace::PreWrap => compute_pre_prewrap(input),
144 _ => unimplemented!(),
145 }
146}
147
148fn is_bidi_control(ch: char) -> bool {
149 use crate::text::directional_formatting_characters::*;
150 matches!(ch, LRE | RLE | LRO | RLO | PDF | LRI | RLI | FSI | PDI)
151}
152
153fn is_space(ch: char) -> bool {
155 matches!(ch, ' ' | '\t' | '\n')
156}
157
158fn compute_normal_nowrap(input: &str) -> Vec<Character> {
171 let mut result: Vec<Character> = Vec::with_capacity(input.len());
172
173 let mut prev_was_space: bool = false;
174
175 for ch in input.chars() {
176 if is_bidi_control(ch) {
177 result.push(Character {
178 addressable: false,
179 character: ch,
180 });
181 continue;
182 }
183
184 if is_space(ch) {
185 if prev_was_space {
186 result.push(Character {
187 addressable: false,
188 character: ch,
189 });
190 } else {
191 result.push(Character {
192 addressable: true,
193 character: ch,
194 });
195 prev_was_space = true;
196 }
197 } else {
198 result.push(Character {
199 addressable: true,
200 character: ch,
201 });
202
203 prev_was_space = false;
204 }
205 }
206
207 result
208}
209
210fn compute_pre_prewrap(input: &str) -> Vec<Character> {
211 let mut result: Vec<Character> = Vec::with_capacity(input.len());
212
213 for ch in input.chars() {
214 if is_bidi_control(ch) {
215 result.push(Character {
216 addressable: false,
217 character: ch,
218 });
219 } else {
220 result.push(Character {
221 addressable: true,
222 character: ch,
223 });
224 }
225 }
226
227 result
228}
229
230fn get_bidi_control(element: &Element) -> BidiControl {
231 let computed_values = element.get_computed_values();
233
234 let unicode_bidi = computed_values.unicode_bidi();
235 let direction = computed_values.direction();
236
237 BidiControl::from_unicode_bidi_and_direction(unicode_bidi, direction)
238}
239
240#[allow(unused)]
242fn collect_text_from_node(node: &Node) -> String {
243 let mut result = String::new();
244
245 for edge in node.traverse() {
246 match edge {
247 NodeEdge::Start(child_node) => match *child_node.borrow() {
248 NodeData::Text(ref text) => {
249 result.push_str(&text.get_string());
250 }
251
252 NodeData::Element(ref element) => match element.element_data {
253 ElementData::TSpan(_) | ElementData::Text(_) | ElementData::Text2(_) => {
254 let bidi_control = get_bidi_control(element);
255
256 for &ch in bidi_control.start {
257 result.push(ch);
258 }
259 }
260 _ => {}
261 },
262 },
263
264 NodeEdge::End(child_node) => {
265 if let NodeData::Element(ref element) = *child_node.borrow() {
266 match element.element_data {
267 ElementData::TSpan(_) | ElementData::Text(_) | ElementData::Text2(_) => {
268 let bidi_control = get_bidi_control(element);
269
270 for &ch in bidi_control.end {
271 result.push(ch);
272 }
273 }
274
275 _ => {}
276 }
277 }
278 }
279 }
280 }
281
282 result
283}
284
285#[allow(unused)]
290struct Attributes {
291 start_index: usize,
293
294 end_index: usize,
296
297 props: FontProperties,
299}
300
301#[allow(unused)]
305struct FormattedText {
306 text: String,
307 attributes: Vec<Attributes>,
308}
309
310#[allow(unused)]
319fn build_formatted_text(
320 characters: &[Character],
321 text_node: &Node,
322 params: &NormalizeParams,
323) -> FormattedText {
324 let mut indices_stack = Vec::new();
325 let mut byte_index = 0;
326 let mut num_visited_characters = 0;
327 let mut text = String::new();
328 let mut attributes = Vec::new();
329
330 for edge in text_node.traverse() {
331 match edge {
332 NodeEdge::Start(child_node) => match *child_node.borrow() {
333 NodeData::Element(ref element) => match element.element_data {
334 ElementData::TSpan(_) | ElementData::Text(_) | ElementData::Text2(_) => {
335 indices_stack.push(byte_index);
336 let bidi_control = get_bidi_control(element);
337 for &ch in bidi_control.start {
338 byte_index += ch.len_utf8();
339 num_visited_characters += 1;
340 text.push(ch);
341 }
342 }
343 _ => {}
344 },
345 NodeData::Text(_) => {}
346 },
347
348 NodeEdge::End(child_node) => match *child_node.borrow() {
349 NodeData::Element(ref element) => match element.element_data {
350 ElementData::TSpan(_) | ElementData::Text(_) | ElementData::Text2(_) => {
351 let bidi_control = get_bidi_control(element);
352 for &ch in bidi_control.end {
353 byte_index += ch.len_utf8();
354 num_visited_characters += 1;
355 text.push(ch);
356 }
357
358 let start_index = indices_stack
359 .pop()
360 .expect("start_index must be pushed already");
361 let values = element.get_computed_values();
362 let font_props = FontProperties::new(values, params);
363
364 if byte_index > start_index {
365 attributes.push(Attributes {
366 start_index,
367 end_index: byte_index,
368 props: font_props,
369 });
370 }
371 }
372 _ => {}
373 },
374
375 NodeData::Text(ref text_ref) => {
376 let text_len = text_ref.get_string().chars().count();
377 for character in characters
378 .iter()
379 .skip(num_visited_characters)
380 .take(text_len)
381 {
382 if character.addressable {
383 text.push(character.character);
384 byte_index += character.character.len_utf8();
385 }
386 num_visited_characters += 1;
387 }
388 }
389 },
390 }
391 }
392
393 FormattedText { text, attributes }
394}
395
396#[allow(unused)]
401fn build_pango_attr_list(session: &Session, formatted_text: &FormattedText) -> pango::AttrList {
402 let attr_list = pango::AttrList::new();
403
404 if formatted_text.text.is_empty() {
405 return attr_list;
406 }
407
408 for attribute in &formatted_text.attributes {
409 if attribute.start_index >= attribute.end_index {
411 continue;
412 }
413
414 let start_index = attribute.start_index.min(formatted_text.text.len());
416 let end_index = attribute.end_index.min(formatted_text.text.len());
417
418 assert!(start_index <= end_index);
419
420 let start_index =
421 u32::try_from(start_index).expect("Pango attribute index must fit in u32");
422 let end_index = u32::try_from(end_index).expect("Pango attribute index must fit in u32");
423
424 let mut font_desc = pango::FontDescription::new();
426 font_desc.set_family(&attribute.props.font_family.0);
427
428 if let Some(font_size) = PangoUnits::from_pixels(attribute.props.font_size) {
430 font_desc.set_size(font_size.0);
431 } else {
432 rsvg_log!(
433 session,
434 "font-size {} is out of bounds; skipping attribute range",
435 attribute.props.font_size
436 );
437 }
438
439 font_desc.set_weight(pango::Weight::from(attribute.props.font_weight));
440 font_desc.set_style(pango::Style::from(attribute.props.font_style));
441 font_desc.set_stretch(pango::Stretch::from(attribute.props.font_stretch));
442 font_desc.set_variant(pango::Variant::from(attribute.props.font_variant));
443
444 let mut font_attr = pango::AttrFontDesc::new(&font_desc).upcast();
445 font_attr.set_start_index(start_index);
446 font_attr.set_end_index(end_index);
447 attr_list.insert(font_attr);
448
449 if attribute.props.letter_spacing != 0.0 {
451 if let Some(spacing) = PangoUnits::from_pixels(attribute.props.letter_spacing) {
452 let mut spacing_attr = pango::AttrInt::new_letter_spacing(spacing.0).upcast();
453 spacing_attr.set_start_index(start_index);
454 spacing_attr.set_end_index(end_index);
455 attr_list.insert(spacing_attr);
456 } else {
457 rsvg_log!(
458 session,
459 "letter-spacing {} is out of bounds; skipping attribute range",
460 attribute.props.letter_spacing
461 );
462 }
463 }
464
465 if attribute.props.text_decoration.overline {
467 let mut overline_attr = pango::AttrInt::new_overline(pango::Overline::Single).upcast();
468 overline_attr.set_start_index(start_index);
469 overline_attr.set_end_index(end_index);
470 attr_list.insert(overline_attr);
471 }
472
473 if attribute.props.text_decoration.underline {
474 let mut underline_attr =
475 pango::AttrInt::new_underline(pango::Underline::Single).upcast();
476 underline_attr.set_start_index(start_index);
477 underline_attr.set_end_index(end_index);
478 attr_list.insert(underline_attr);
479 }
480
481 if attribute.props.text_decoration.strike {
482 let mut strike_attr = pango::AttrInt::new_strikethrough(true).upcast();
483 strike_attr.set_start_index(start_index);
484 strike_attr.set_end_index(end_index);
485 attr_list.insert(strike_attr);
486 }
487 }
488
489 attr_list
490}
491
492struct PangoUnits(i32);
493
494impl PangoUnits {
495 fn from_pixels(v: f64) -> Option<Self> {
496 cast::i32(v * f64::from(pango::SCALE) + 0.5)
499 .ok()
500 .map(PangoUnits)
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use crate::document::Document;
507 use crate::dpi::Dpi;
508 use crate::element::ElementData;
509 use crate::node::NodeBorrow;
510 use crate::properties::{FontStyle, FontWeight};
511
512 use super::*;
513
514 #[test]
515 fn collects_text_in_a_single_string() {
516 let doc_str = br##"<?xml version="1.0" encoding="UTF-8"?>
517<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
518
519 <text2 id="sample">
520 Hello
521 <tspan font-style="italic">
522 <tspan font-weight="bold">bold</tspan>
523 world!
524 </tspan>
525 How are you.
526 </text2>
527</svg>
528"##;
529
530 let document = Document::load_from_bytes(doc_str);
531
532 let text2_node = document.lookup_internal_node("sample").unwrap();
533 assert!(matches!(
534 *text2_node.borrow_element_data(),
535 ElementData::Text2(_)
536 ));
537
538 let text_string = collect_text_from_node(&text2_node);
539 assert_eq!(
540 text_string,
541 "\n \
542 Hello\n \
543 \n \
544 bold\n \
545 world!\n \
546 \n \
547 How are you.\
548 \n "
549 );
550 }
551
552 #[test]
553 fn adds_bidi_control_characters() {
554 let doc_str = br##"<?xml version="1.0" encoding="UTF-8"?>
555<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
556
557 <text2 id="sample">
558 Hello
559 <tspan direction="rtl" unicode-bidi="embed">
560 <tspan direction="ltr" unicode-bidi="isolate-override">bold</tspan>
561 world!
562 </tspan>
563 How are <tspan direction="rtl" unicode-bidi="isolate">you</tspan>.
564 </text2>
565</svg>
566"##;
567
568 let document = Document::load_from_bytes(doc_str);
569
570 let text2_node = document.lookup_internal_node("sample").unwrap();
571 assert!(matches!(
572 *text2_node.borrow_element_data(),
573 ElementData::Text2(_)
574 ));
575
576 let text_string = collect_text_from_node(&text2_node);
577 assert_eq!(
578 text_string,
579 "\n \
580 Hello\n \
581 \u{202b}\n \
582 \u{2068}\u{202d}bold\u{202c}\u{2069}\n \
583 world!\n \
584 \u{202c}\n \
585 How are \u{2067}you\u{2069}.\
586 \n "
587 );
588 }
589
590 fn check_true_false_template(template: &str, characters: &[Character]) {
593 assert_eq!(characters.len(), template.len());
594
595 for (i, ch) in template.chars().enumerate() {
598 assert_eq!(characters[i].addressable, ch == 't');
599 }
600 }
601
602 fn check_modes_with_identical_processing(
603 string: &str,
604 template: &str,
605 mode1: WhiteSpace,
606 mode2: WhiteSpace,
607 ) {
608 let result1 = collapse_white_space(string, mode1);
609 check_true_false_template(template, &result1);
610
611 let result2 = collapse_white_space(string, mode2);
612 check_true_false_template(template, &result2);
613 }
614
615 #[rustfmt::skip]
618 #[test]
619 fn handles_white_space_normal_trivial_case() {
620 check_modes_with_identical_processing(
621 "hello world",
622 "ttttttfttttt",
623 WhiteSpace::Normal,
624 WhiteSpace::NoWrap
625 );
626 }
627
628 #[rustfmt::skip]
629 #[test]
630 fn handles_white_space_normal_start_of_the_line() {
631 check_modes_with_identical_processing(
632 " hello world",
633 "tffttttttfttttt",
634 WhiteSpace::Normal,
635 WhiteSpace::NoWrap
636 );
637 }
638
639 #[rustfmt::skip]
640 #[test]
641 fn handles_white_space_normal_ignores_bidi_control() {
642 check_modes_with_identical_processing(
643 "A \u{202b} B \u{202c} C",
644 "ttffttfft",
645 WhiteSpace::Normal,
646 WhiteSpace::NoWrap
647 );
648 }
649
650 #[rustfmt::skip]
671 #[test]
672 fn handles_white_space_pre_trivial_case() {
673 check_modes_with_identical_processing(
674 " hello \n \n \n\n\nworld",
675 "tttttttttttttttttttttttt",
676 WhiteSpace::Pre,
677 WhiteSpace::PreWrap
678 );
679 }
680
681 #[rustfmt::skip]
682 #[test]
683 fn handles_white_space_pre_ignores_bidi_control() {
684 check_modes_with_identical_processing(
685 "A \u{202b} \n\n\n B \u{202c} C ",
686 "tttftttttttftttt",
687 WhiteSpace::Pre,
688 WhiteSpace::PreWrap
689 );
690 }
691
692 fn dummy_normalize_params() -> NormalizeParams {
695 NormalizeParams::from_dpi(Dpi::new(96.0, 96.0))
696 }
697
698 #[test]
699 fn builds_non_bidi_formatted_text() {
700 let doc_str = r##"<?xml version="1.0" encoding="UTF-8"?>
701<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
702
703 <text2 id="sample" font-family="Foobar">
704 Hello <tspan font-weight="bold">böld</tspan> world <tspan font-style="italic">in italics</tspan>!
705 </text2>
706</svg>
707"##;
708
709 let document = Document::load_from_bytes(doc_str.as_bytes());
710
711 let text2_node = document.lookup_internal_node("sample").unwrap();
712 assert!(matches!(
713 *text2_node.borrow_element_data(),
714 ElementData::Text2(_)
715 ));
716
717 let collected_text = collect_text_from_node(&text2_node);
718 let collapsed_characters = collapse_white_space(&collected_text, WhiteSpace::Normal);
719
720 let formatted = build_formatted_text(
721 &collapsed_characters,
722 &text2_node,
723 &dummy_normalize_params(),
724 );
725
726 assert_eq!(&formatted.text, "\nHello böld world in italics!\n");
727
728 assert_eq!(formatted.attributes[0].start_index, 7);
730 assert_eq!(formatted.attributes[0].end_index, 12);
731 assert_eq!(formatted.attributes[0].props.font_weight, FontWeight::Bold);
732
733 assert_eq!(formatted.attributes[1].start_index, 19);
735 assert_eq!(formatted.attributes[1].end_index, 29);
736 assert_eq!(formatted.attributes[1].props.font_style, FontStyle::Italic);
737
738 assert_eq!(formatted.attributes[2].start_index, 0);
740 assert_eq!(formatted.attributes[2].end_index, 31);
741 assert_eq!(formatted.attributes[2].props.font_family.0, "Foobar");
742 }
743
744 #[test]
745 fn builds_bidi_formatted_text() {
746 let doc_str = r##"<?xml version="1.0" encoding="UTF-8"?>
747<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="100" height="100">
748
749 <text2 id="sample" font-family="Foobar">
750 LTR<tspan direction="rtl" unicode-bidi="embed" font-style="italic">RTL</tspan><tspan font-weight="bold">LTR</tspan>
751 </text2>
752</svg>
753"##;
754
755 let document = Document::load_from_bytes(doc_str.as_bytes());
756
757 let text2_node = document.lookup_internal_node("sample").unwrap();
758 assert!(matches!(
759 *text2_node.borrow_element_data(),
760 ElementData::Text2(_)
761 ));
762
763 let collected_text = collect_text_from_node(&text2_node);
764 let collapsed_characters = collapse_white_space(&collected_text, WhiteSpace::Normal);
765
766 let formatted = build_formatted_text(
767 &collapsed_characters,
768 &text2_node,
769 &dummy_normalize_params(),
770 );
771
772 assert_eq!(&formatted.text, "\nLTR\u{202b}RTL\u{202c}LTR\n");
773
774 assert_eq!(formatted.attributes[0].start_index, 4);
776 assert_eq!(formatted.attributes[0].end_index, 13);
777 assert_eq!(formatted.attributes[0].props.font_style, FontStyle::Italic);
778
779 assert_eq!(formatted.attributes[1].start_index, 13);
781 assert_eq!(formatted.attributes[1].end_index, 16);
782 assert_eq!(formatted.attributes[1].props.font_weight, FontWeight::Bold);
783
784 assert_eq!(formatted.attributes[2].start_index, 0);
786 assert_eq!(formatted.attributes[2].end_index, 17);
787 assert_eq!(formatted.attributes[2].props.font_family.0, "Foobar");
788 }
789}