Skip to main content

ai_usvg/tree/
text.rs

1// Copyright 2018 the Resvg Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use alloc::boxed::Box;
5use alloc::string::String;
6use alloc::sync::Arc;
7use alloc::vec::Vec;
8
9use strict_num::NonZeroPositiveF32;
10pub use svgtypes::FontFamily;
11
12#[cfg(feature = "text")]
13use crate::layout::Span;
14use crate::{Fill, Group, NonEmptyString, PaintOrder, Rect, Stroke, TextRendering, Transform};
15
16/// A font stretch property.
17#[allow(missing_docs)]
18#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
19pub enum FontStretch {
20    UltraCondensed,
21    ExtraCondensed,
22    Condensed,
23    SemiCondensed,
24    Normal,
25    SemiExpanded,
26    Expanded,
27    ExtraExpanded,
28    UltraExpanded,
29}
30
31impl Default for FontStretch {
32    #[inline]
33    fn default() -> Self {
34        Self::Normal
35    }
36}
37
38#[cfg(feature = "text")]
39impl From<fontdb::Stretch> for FontStretch {
40    fn from(stretch: fontdb::Stretch) -> Self {
41        match stretch {
42            fontdb::Stretch::UltraCondensed => FontStretch::UltraCondensed,
43            fontdb::Stretch::ExtraCondensed => FontStretch::ExtraCondensed,
44            fontdb::Stretch::Condensed => FontStretch::Condensed,
45            fontdb::Stretch::SemiCondensed => FontStretch::SemiCondensed,
46            fontdb::Stretch::Normal => FontStretch::Normal,
47            fontdb::Stretch::SemiExpanded => FontStretch::SemiExpanded,
48            fontdb::Stretch::Expanded => FontStretch::Expanded,
49            fontdb::Stretch::ExtraExpanded => FontStretch::ExtraExpanded,
50            fontdb::Stretch::UltraExpanded => FontStretch::UltraExpanded,
51        }
52    }
53}
54
55#[cfg(feature = "text")]
56impl From<FontStretch> for fontdb::Stretch {
57    fn from(stretch: FontStretch) -> Self {
58        match stretch {
59            FontStretch::UltraCondensed => fontdb::Stretch::UltraCondensed,
60            FontStretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed,
61            FontStretch::Condensed => fontdb::Stretch::Condensed,
62            FontStretch::SemiCondensed => fontdb::Stretch::SemiCondensed,
63            FontStretch::Normal => fontdb::Stretch::Normal,
64            FontStretch::SemiExpanded => fontdb::Stretch::SemiExpanded,
65            FontStretch::Expanded => fontdb::Stretch::Expanded,
66            FontStretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded,
67            FontStretch::UltraExpanded => fontdb::Stretch::UltraExpanded,
68        }
69    }
70}
71
72/// A font variation axis setting.
73///
74/// Used for variable fonts to specify axis values like weight, width, etc.
75#[derive(Clone, Copy, Debug)]
76pub struct FontVariation {
77    /// The 4-byte axis tag (e.g., b"wght" for weight).
78    pub tag: [u8; 4],
79    /// The axis value.
80    pub value: f32,
81}
82
83impl FontVariation {
84    /// Creates a new font variation.
85    pub fn new(tag: [u8; 4], value: f32) -> Self {
86        Self { tag, value }
87    }
88}
89
90impl PartialEq for FontVariation {
91    fn eq(&self, other: &Self) -> bool {
92        self.tag == other.tag && self.value.to_bits() == other.value.to_bits()
93    }
94}
95
96impl Eq for FontVariation {}
97
98impl core::hash::Hash for FontVariation {
99    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
100        self.tag.hash(state);
101        self.value.to_bits().hash(state);
102    }
103}
104
105/// A font style property.
106#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
107pub enum FontStyle {
108    /// A face that is neither italic not obliqued.
109    Normal,
110    /// A form that is generally cursive in nature.
111    Italic,
112    /// A typically-sloped version of the regular face.
113    Oblique,
114}
115
116impl Default for FontStyle {
117    #[inline]
118    fn default() -> FontStyle {
119        Self::Normal
120    }
121}
122
123#[cfg(feature = "text")]
124impl From<fontdb::Style> for FontStyle {
125    fn from(style: fontdb::Style) -> Self {
126        match style {
127            fontdb::Style::Normal => FontStyle::Normal,
128            fontdb::Style::Italic => FontStyle::Italic,
129            fontdb::Style::Oblique => FontStyle::Oblique,
130        }
131    }
132}
133
134#[cfg(feature = "text")]
135impl From<FontStyle> for fontdb::Style {
136    fn from(style: FontStyle) -> Self {
137        match style {
138            FontStyle::Normal => fontdb::Style::Normal,
139            FontStyle::Italic => fontdb::Style::Italic,
140            FontStyle::Oblique => fontdb::Style::Oblique,
141        }
142    }
143}
144
145/// Text font properties.
146#[derive(Clone, Eq, PartialEq, Hash, Debug)]
147pub struct Font {
148    pub(crate) families: Vec<FontFamily>,
149    pub(crate) style: FontStyle,
150    pub(crate) stretch: FontStretch,
151    pub(crate) weight: u16,
152    pub(crate) variations: Vec<FontVariation>,
153}
154
155impl Font {
156    /// A list of family names.
157    ///
158    /// Never empty. Uses `usvg::Options::font_family` as fallback.
159    pub fn families(&self) -> &[FontFamily] {
160        &self.families
161    }
162
163    /// A font style.
164    pub fn style(&self) -> FontStyle {
165        self.style
166    }
167
168    /// A font stretch.
169    pub fn stretch(&self) -> FontStretch {
170        self.stretch
171    }
172
173    /// A font width.
174    pub fn weight(&self) -> u16 {
175        self.weight
176    }
177
178    /// Font variation settings for variable fonts.
179    pub fn variations(&self) -> &[FontVariation] {
180        &self.variations
181    }
182}
183
184/// A dominant baseline property.
185#[allow(missing_docs)]
186#[derive(Clone, Copy, PartialEq, Debug)]
187pub enum DominantBaseline {
188    Auto,
189    UseScript,
190    NoChange,
191    ResetSize,
192    Ideographic,
193    Alphabetic,
194    Hanging,
195    Mathematical,
196    Central,
197    Middle,
198    TextAfterEdge,
199    TextBeforeEdge,
200}
201
202impl Default for DominantBaseline {
203    fn default() -> Self {
204        Self::Auto
205    }
206}
207
208/// An alignment baseline property.
209#[allow(missing_docs)]
210#[derive(Clone, Copy, PartialEq, Debug)]
211pub enum AlignmentBaseline {
212    Auto,
213    Baseline,
214    BeforeEdge,
215    TextBeforeEdge,
216    Middle,
217    Central,
218    AfterEdge,
219    TextAfterEdge,
220    Ideographic,
221    Alphabetic,
222    Hanging,
223    Mathematical,
224}
225
226impl Default for AlignmentBaseline {
227    fn default() -> Self {
228        Self::Auto
229    }
230}
231
232/// A baseline shift property.
233#[allow(missing_docs)]
234#[derive(Clone, Copy, PartialEq, Debug)]
235pub enum BaselineShift {
236    Baseline,
237    Subscript,
238    Superscript,
239    Number(f32),
240}
241
242impl Default for BaselineShift {
243    #[inline]
244    fn default() -> BaselineShift {
245        BaselineShift::Baseline
246    }
247}
248
249/// A length adjust property.
250#[allow(missing_docs)]
251#[derive(Clone, Copy, PartialEq, Debug)]
252pub enum LengthAdjust {
253    Spacing,
254    SpacingAndGlyphs,
255}
256
257impl Default for LengthAdjust {
258    fn default() -> Self {
259        Self::Spacing
260    }
261}
262
263/// A font optical sizing property.
264///
265/// Controls automatic adjustment of the `opsz` axis in variable fonts
266/// based on font size. Matches CSS `font-optical-sizing`.
267#[derive(Clone, Copy, PartialEq, Debug)]
268pub enum FontOpticalSizing {
269    /// Automatically set `opsz` to match font size (browser default).
270    Auto,
271    /// Do not automatically adjust `opsz`.
272    None,
273}
274
275impl Default for FontOpticalSizing {
276    fn default() -> Self {
277        Self::Auto
278    }
279}
280
281/// A text span decoration style.
282///
283/// In SVG, text decoration and text it's applied to can have different styles.
284/// So you can have black text and green underline.
285///
286/// Also, in SVG you can specify text decoration stroking.
287#[derive(Clone, Debug)]
288pub struct TextDecorationStyle {
289    pub(crate) fill: Option<Fill>,
290    pub(crate) stroke: Option<Stroke>,
291}
292
293impl TextDecorationStyle {
294    /// A fill style.
295    pub fn fill(&self) -> Option<&Fill> {
296        self.fill.as_ref()
297    }
298
299    /// A stroke style.
300    pub fn stroke(&self) -> Option<&Stroke> {
301        self.stroke.as_ref()
302    }
303}
304
305/// A text span decoration.
306#[derive(Clone, Debug)]
307pub struct TextDecoration {
308    pub(crate) underline: Option<TextDecorationStyle>,
309    pub(crate) overline: Option<TextDecorationStyle>,
310    pub(crate) line_through: Option<TextDecorationStyle>,
311}
312
313impl TextDecoration {
314    /// An optional underline and its style.
315    pub fn underline(&self) -> Option<&TextDecorationStyle> {
316        self.underline.as_ref()
317    }
318
319    /// An optional overline and its style.
320    pub fn overline(&self) -> Option<&TextDecorationStyle> {
321        self.overline.as_ref()
322    }
323
324    /// An optional line-through and its style.
325    pub fn line_through(&self) -> Option<&TextDecorationStyle> {
326        self.line_through.as_ref()
327    }
328}
329
330/// A text style span.
331///
332/// Spans do not overlap inside a text chunk.
333#[derive(Clone, Debug)]
334pub struct TextSpan {
335    pub(crate) start: usize,
336    pub(crate) end: usize,
337    pub(crate) fill: Option<Fill>,
338    pub(crate) stroke: Option<Stroke>,
339    pub(crate) paint_order: PaintOrder,
340    pub(crate) font: Font,
341    pub(crate) font_size: NonZeroPositiveF32,
342    pub(crate) small_caps: bool,
343    pub(crate) apply_kerning: bool,
344    pub(crate) font_optical_sizing: FontOpticalSizing,
345    pub(crate) decoration: TextDecoration,
346    pub(crate) dominant_baseline: DominantBaseline,
347    pub(crate) alignment_baseline: AlignmentBaseline,
348    pub(crate) baseline_shift: Vec<BaselineShift>,
349    pub(crate) visible: bool,
350    pub(crate) letter_spacing: f32,
351    pub(crate) word_spacing: f32,
352    pub(crate) text_length: Option<f32>,
353    pub(crate) length_adjust: LengthAdjust,
354}
355
356impl TextSpan {
357    /// A span start in bytes.
358    ///
359    /// Offset is relative to the parent text chunk and not the parent text element.
360    pub fn start(&self) -> usize {
361        self.start
362    }
363
364    /// A span end in bytes.
365    ///
366    /// Offset is relative to the parent text chunk and not the parent text element.
367    pub fn end(&self) -> usize {
368        self.end
369    }
370
371    /// A fill style.
372    pub fn fill(&self) -> Option<&Fill> {
373        self.fill.as_ref()
374    }
375
376    /// A stroke style.
377    pub fn stroke(&self) -> Option<&Stroke> {
378        self.stroke.as_ref()
379    }
380
381    /// A paint order style.
382    pub fn paint_order(&self) -> PaintOrder {
383        self.paint_order
384    }
385
386    /// A font.
387    pub fn font(&self) -> &Font {
388        &self.font
389    }
390
391    /// A font size.
392    pub fn font_size(&self) -> NonZeroPositiveF32 {
393        self.font_size
394    }
395
396    /// Indicates that small caps should be used.
397    ///
398    /// Set by `font-variant="small-caps"`
399    pub fn small_caps(&self) -> bool {
400        self.small_caps
401    }
402
403    /// Indicates that a kerning should be applied.
404    ///
405    /// Supports both `kerning` and `font-kerning` properties.
406    pub fn apply_kerning(&self) -> bool {
407        self.apply_kerning
408    }
409
410    /// Font optical sizing mode.
411    ///
412    /// When `Auto` (default), the `opsz` axis will be automatically set
413    /// to match the font size for variable fonts that support it.
414    /// This matches the CSS `font-optical-sizing: auto` behavior.
415    pub fn font_optical_sizing(&self) -> FontOpticalSizing {
416        self.font_optical_sizing
417    }
418
419    /// A span decorations.
420    pub fn decoration(&self) -> &TextDecoration {
421        &self.decoration
422    }
423
424    /// A span dominant baseline.
425    pub fn dominant_baseline(&self) -> DominantBaseline {
426        self.dominant_baseline
427    }
428
429    /// A span alignment baseline.
430    pub fn alignment_baseline(&self) -> AlignmentBaseline {
431        self.alignment_baseline
432    }
433
434    /// A list of all baseline shift that should be applied to this span.
435    ///
436    /// Ordered from `text` element down to the actual `span` element.
437    pub fn baseline_shift(&self) -> &[BaselineShift] {
438        &self.baseline_shift
439    }
440
441    /// A visibility property.
442    pub fn is_visible(&self) -> bool {
443        self.visible
444    }
445
446    /// A letter spacing property.
447    pub fn letter_spacing(&self) -> f32 {
448        self.letter_spacing
449    }
450
451    /// A word spacing property.
452    pub fn word_spacing(&self) -> f32 {
453        self.word_spacing
454    }
455
456    /// A text length property.
457    pub fn text_length(&self) -> Option<f32> {
458        self.text_length
459    }
460
461    /// A length adjust property.
462    pub fn length_adjust(&self) -> LengthAdjust {
463        self.length_adjust
464    }
465}
466
467/// A text chunk anchor property.
468#[allow(missing_docs)]
469#[derive(Clone, Copy, PartialEq, Debug)]
470pub enum TextAnchor {
471    Start,
472    Middle,
473    End,
474}
475
476impl Default for TextAnchor {
477    fn default() -> Self {
478        Self::Start
479    }
480}
481
482/// A path used by text-on-path.
483#[derive(Debug)]
484pub struct TextPath {
485    pub(crate) id: NonEmptyString,
486    pub(crate) start_offset: f32,
487    pub(crate) path: Arc<tiny_skia_path::Path>,
488}
489
490impl TextPath {
491    /// Element's ID.
492    ///
493    /// Taken from the SVG itself.
494    pub fn id(&self) -> &str {
495        self.id.get()
496    }
497
498    /// A text offset in SVG coordinates.
499    ///
500    /// Percentage values already resolved.
501    pub fn start_offset(&self) -> f32 {
502        self.start_offset
503    }
504
505    /// A path.
506    pub fn path(&self) -> &tiny_skia_path::Path {
507        &self.path
508    }
509}
510
511/// A text chunk flow property.
512#[derive(Clone, Debug)]
513pub enum TextFlow {
514    /// A linear layout.
515    ///
516    /// Includes left-to-right, right-to-left and top-to-bottom.
517    Linear,
518    /// A text-on-path layout.
519    Path(Arc<TextPath>),
520}
521
522/// A text chunk.
523///
524/// Text alignment and BIDI reordering can only be done inside a text chunk.
525#[derive(Clone, Debug)]
526pub struct TextChunk {
527    pub(crate) x: Option<f32>,
528    pub(crate) y: Option<f32>,
529    pub(crate) anchor: TextAnchor,
530    pub(crate) spans: Vec<TextSpan>,
531    pub(crate) text_flow: TextFlow,
532    pub(crate) text: String,
533}
534
535impl TextChunk {
536    /// An absolute X axis offset.
537    pub fn x(&self) -> Option<f32> {
538        self.x
539    }
540
541    /// An absolute Y axis offset.
542    pub fn y(&self) -> Option<f32> {
543        self.y
544    }
545
546    /// A text anchor.
547    pub fn anchor(&self) -> TextAnchor {
548        self.anchor
549    }
550
551    /// A list of text chunk style spans.
552    pub fn spans(&self) -> &[TextSpan] {
553        &self.spans
554    }
555
556    /// A text chunk flow.
557    pub fn text_flow(&self) -> TextFlow {
558        self.text_flow.clone()
559    }
560
561    /// A text chunk actual text.
562    pub fn text(&self) -> &str {
563        &self.text
564    }
565}
566
567/// A writing mode.
568#[allow(missing_docs)]
569#[derive(Clone, Copy, PartialEq, Debug)]
570pub enum WritingMode {
571    LeftToRight,
572    TopToBottom,
573}
574
575/// A text element.
576///
577/// `text` element in SVG.
578#[derive(Clone, Debug)]
579pub struct Text {
580    pub(crate) id: String,
581    pub(crate) rendering_mode: TextRendering,
582    pub(crate) dx: Vec<f32>,
583    pub(crate) dy: Vec<f32>,
584    pub(crate) rotate: Vec<f32>,
585    pub(crate) writing_mode: WritingMode,
586    pub(crate) chunks: Vec<TextChunk>,
587    pub(crate) abs_transform: Transform,
588    pub(crate) bounding_box: Rect,
589    pub(crate) abs_bounding_box: Rect,
590    pub(crate) stroke_bounding_box: Rect,
591    pub(crate) abs_stroke_bounding_box: Rect,
592    pub(crate) flattened: Box<Group>,
593    #[cfg(feature = "text")]
594    pub(crate) layouted: Vec<Span>,
595}
596
597impl Text {
598    /// Element's ID.
599    ///
600    /// Taken from the SVG itself.
601    /// Isn't automatically generated.
602    /// Can be empty.
603    pub fn id(&self) -> &str {
604        &self.id
605    }
606
607    /// Rendering mode.
608    ///
609    /// `text-rendering` in SVG.
610    pub fn rendering_mode(&self) -> TextRendering {
611        self.rendering_mode
612    }
613
614    /// A relative X axis offsets.
615    ///
616    /// One offset for each Unicode codepoint. Aka `char` in Rust.
617    pub fn dx(&self) -> &[f32] {
618        &self.dx
619    }
620
621    /// A relative Y axis offsets.
622    ///
623    /// One offset for each Unicode codepoint. Aka `char` in Rust.
624    pub fn dy(&self) -> &[f32] {
625        &self.dy
626    }
627
628    /// A list of rotation angles.
629    ///
630    /// One angle for each Unicode codepoint. Aka `char` in Rust.
631    pub fn rotate(&self) -> &[f32] {
632        &self.rotate
633    }
634
635    /// A writing mode.
636    pub fn writing_mode(&self) -> WritingMode {
637        self.writing_mode
638    }
639
640    /// A list of text chunks.
641    pub fn chunks(&self) -> &[TextChunk] {
642        &self.chunks
643    }
644
645    /// Element's absolute transform.
646    ///
647    /// Contains all ancestors transforms including elements's transform.
648    ///
649    /// Note that this is not the relative transform present in SVG.
650    /// The SVG one would be set only on groups.
651    pub fn abs_transform(&self) -> Transform {
652        self.abs_transform
653    }
654
655    /// Element's text bounding box.
656    ///
657    /// Text bounding box is special in SVG and doesn't represent
658    /// tight bounds of the element's content.
659    /// You can find more about it
660    /// [here](https://razrfalcon.github.io/notes-on-svg-parsing/text/bbox.html).
661    ///
662    /// `objectBoundingBox` in SVG terms. Meaning it isn't affected by parent transforms.
663    ///
664    /// Returns `None` when the `text` build feature was disabled.
665    /// This is because we have to perform a text layout before calculating a bounding box.
666    pub fn bounding_box(&self) -> Rect {
667        self.bounding_box
668    }
669
670    /// Element's text bounding box in canvas coordinates.
671    ///
672    /// `userSpaceOnUse` in SVG terms.
673    pub fn abs_bounding_box(&self) -> Rect {
674        self.abs_bounding_box
675    }
676
677    /// Element's object bounding box including stroke.
678    ///
679    /// Similar to `bounding_box`, but includes stroke.
680    ///
681    /// Will have the same value as `bounding_box` when path has no stroke.
682    pub fn stroke_bounding_box(&self) -> Rect {
683        self.stroke_bounding_box
684    }
685
686    /// Element's bounding box including stroke in canvas coordinates.
687    pub fn abs_stroke_bounding_box(&self) -> Rect {
688        self.abs_stroke_bounding_box
689    }
690
691    /// Text converted into paths, ready to render.
692    ///
693    /// Note that this is only a
694    /// "best-effort" attempt: The text will be converted into group/paths/image
695    /// primitives, so that they can be rendered with the existing infrastructure.
696    /// This process is in general lossless and should lead to correct output, with
697    /// two notable exceptions:
698    /// 1. For glyphs based on the `SVG` table, only glyphs that are pure SVG 1.1/2.0
699    ///    are supported. Glyphs that make use of features in the OpenType specification
700    ///    that are not part of the original SVG specification are not supported.
701    /// 2. For glyphs based on the `COLR` table, there are a certain number of features
702    ///    that are not (correctly) supported, such as conical
703    ///    gradients, certain gradient transforms and some blend modes. But this shouldn't
704    ///    cause any issues in 95% of the cases, as most of those are edge cases.
705    ///    If the two above are not acceptable, then you will need to implement your own
706    ///    glyph rendering logic based on the layouted glyphs (see the `layouted` method).
707    pub fn flattened(&self) -> &Group {
708        &self.flattened
709    }
710
711    /// The positioned glyphs and decoration spans of the text.
712    ///
713    /// This should only be used if you need more low-level access
714    /// to the glyphs that make up the text. If you just need the
715    /// outlines of the text, you should use `flattened` instead.
716    #[cfg(feature = "text")]
717    pub fn layouted(&self) -> &[Span] {
718        &self.layouted
719    }
720
721    pub(crate) fn subroots(&self, f: &mut dyn FnMut(&Group)) {
722        f(&self.flattened);
723    }
724}