Skip to main content

apace_dom/
util.rs

1use crate::node::{Node, NodeData};
2use color::{AlphaColor, Srgb};
3use keyboard_types::Modifiers;
4use std::borrow::Cow;
5use style::color::AbsoluteColor;
6
7#[cfg(target_os = "macos")]
8pub(crate) const ACTION_MOD: Modifiers = Modifiers::META;
9#[cfg(not(target_os = "macos"))]
10pub(crate) const ACTION_MOD: Modifiers = Modifiers::CONTROL;
11
12pub type Color = AlphaColor<Srgb>;
13
14/// Decode raw font bytes, decompressing WOFF/WOFF2 if the `woff` feature is enabled.
15/// Returns the original slice unchanged for TTF/OTF input, and also on decompression
16/// failure. With the `woff` feature disabled, all input passes through unchanged.
17pub fn decode_font_bytes(bytes: &[u8]) -> Cow<'_, [u8]> {
18    if bytes.len() < 4 {
19        return Cow::Borrowed(bytes);
20    }
21    match &bytes[0..4] {
22        #[cfg(feature = "woff")]
23        b"wOFF" => wuff::decompress_woff1(bytes)
24            .map(Cow::Owned)
25            .unwrap_or_else(|_| {
26                #[cfg(feature = "tracing")]
27                tracing::warn!("Failed to decompress woff1 font");
28                Cow::Borrowed(bytes)
29            }),
30        #[cfg(feature = "woff")]
31        b"wOF2" => wuff::decompress_woff2(bytes)
32            .map(Cow::Owned)
33            .unwrap_or_else(|_| {
34                #[cfg(feature = "tracing")]
35                tracing::warn!("Failed to decompress woff2 font");
36                Cow::Borrowed(bytes)
37            }),
38        _ => Cow::Borrowed(bytes),
39    }
40}
41
42#[cfg(feature = "svg")]
43use std::sync::{Arc, LazyLock};
44#[cfg(feature = "svg")]
45use usvg::fontdb;
46#[cfg(feature = "svg")]
47pub(crate) static FONT_DB: LazyLock<Arc<fontdb::Database>> = LazyLock::new(|| {
48    let mut db = fontdb::Database::new();
49    db.load_system_fonts();
50    Arc::new(db)
51});
52
53/// Which kind of CSS image layer list (`background-image` or `mask-image`) to
54/// flush from style to dedicated storage on the node.
55#[derive(Clone, Copy, Debug)]
56pub enum ImageLayerKind {
57    Background,
58    Mask,
59}
60
61impl ImageLayerKind {
62    pub fn image_type(self, idx: usize) -> ImageType {
63        match self {
64            Self::Background => ImageType::Background(idx),
65            Self::Mask => ImageType::Mask(idx),
66        }
67    }
68}
69
70#[derive(Clone, Copy, Debug)]
71pub enum ImageType {
72    Image,
73    Background(usize),
74    Mask(usize),
75}
76
77/// A point
78#[derive(Clone, Debug, Copy, Eq, PartialEq)]
79pub struct Point<T> {
80    /// The x coordinate
81    pub x: T,
82    /// The y coordinate
83    pub y: T,
84}
85
86impl Point<f64> {
87    pub const ZERO: Self = Point { x: 0.0, y: 0.0 };
88}
89
90// Debug print an RcDom
91pub fn walk_tree(indent: usize, node: &Node) {
92    // Skip all-whitespace text nodes entirely
93    if let NodeData::Text(data) = &node.data {
94        if data.content.chars().all(|c| c.is_ascii_whitespace()) {
95            return;
96        }
97    }
98
99    print!("{}", " ".repeat(indent));
100    let id = node.id;
101    match &node.data {
102        NodeData::Document(_) => println!("#Document {id}"),
103
104        NodeData::Text(data) => {
105            if data.content.chars().all(|c| c.is_ascii_whitespace()) {
106                println!("{id} #text: <whitespace>");
107            } else {
108                let content = data.content.trim();
109                if content.len() > 10 {
110                    println!(
111                        "#text {id}: {}...",
112                        content
113                            .split_at(content.char_indices().take(10).last().unwrap().0)
114                            .0
115                            .escape_default()
116                    )
117                } else {
118                    println!("#text {id}: {}", data.content.trim().escape_default())
119                }
120            }
121        }
122
123        NodeData::Comment { .. } => println!("<!-- COMMENT {id} -->"),
124
125        NodeData::AnonymousBlock(_) => println!("{id} AnonymousBlock"),
126
127        NodeData::Element(data) => {
128            print!("<{} {id}", data.name.local);
129            for attr in data.attrs.iter() {
130                print!(" {}=\"{}\"", attr.name.local, attr.value);
131            }
132            if !node.children.is_empty() {
133                println!(">");
134            } else {
135                println!("/>");
136            }
137        } // NodeData::Doctype {
138          //     ref name,
139          //     ref public_id,
140          //     ref system_id,
141          // } => println!("<!DOCTYPE {} \"{}\" \"{}\">", name, public_id, system_id),
142          // NodeData::ProcessingInstruction { .. } => unreachable!(),
143    }
144
145    if !node.children.is_empty() {
146        for child_id in node.children.iter() {
147            walk_tree(indent + 2, node.with(*child_id));
148        }
149
150        if let NodeData::Element(data) = &node.data {
151            println!("{}</{}>", " ".repeat(indent), data.name.local);
152        }
153    }
154}
155
156/// Parse an SVG image.
157#[cfg(feature = "svg")]
158pub(crate) fn parse_svg_image(source: &[u8]) -> Result<crate::node::SvgImageData, usvg::Error> {
159    let options = usvg::Options {
160        fontdb: Arc::clone(&*FONT_DB),
161        ..Default::default()
162    };
163    crate::node::SvgImageData::from_data(source, &options)
164}
165
166pub trait ToColorColor {
167    /// Converts a color into the `AlphaColor<Srgb>` type from the `color` crate
168    fn as_color_color(&self) -> Color;
169}
170impl ToColorColor for AbsoluteColor {
171    fn as_color_color(&self) -> Color {
172        Color::new(
173            *self
174                .to_color_space(style::color::ColorSpace::Srgb)
175                .raw_components(),
176        )
177    }
178}
179
180#[cfg(all(test, feature = "svg"))]
181mod svg_tests {
182    use super::parse_svg_image;
183
184    #[test]
185    fn svg_without_xmlns_parses() {
186        let src = br#"<svg viewBox="0 0 24 24"><path d="M0 0h24v24H0z"/></svg>"#;
187        let svg = parse_svg_image(src).unwrap();
188        assert!(!svg.tree.root().children().is_empty());
189    }
190
191    #[test]
192    fn missing_height_is_computed_from_width_and_viewbox_ratio() {
193        let src = br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="200"><rect width="100%" height="100%" fill="green"/></svg>"#;
194        let svg = parse_svg_image(src).unwrap();
195        assert_eq!(svg.intrinsic_width(), Some(200.0));
196        assert_eq!(svg.intrinsic_height(), None);
197        assert_eq!(svg.viewbox_aspect_ratio(), Some(1.0));
198        assert_eq!(svg.tree.size().width(), 200.0);
199        assert_eq!(svg.intrinsic_size(), (200.0, 200.0));
200    }
201
202    #[test]
203    fn viewbox_only_has_no_intrinsic_dimensions() {
204        let src = br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 485 58"></svg>"#;
205        let svg = parse_svg_image(src).unwrap();
206        assert_eq!(svg.intrinsic_width(), None);
207        assert_eq!(svg.intrinsic_height(), None);
208        // The aspect ratio is still available from the viewBox.
209        assert!((svg.aspect_ratio() - (485.0 / 58.0)).abs() < 1e-3);
210    }
211
212    #[test]
213    fn absolute_dimensions_are_intrinsic() {
214        let src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="24" height="16" viewBox="0 0 48 32"></svg>"#;
215        let svg = parse_svg_image(src).unwrap();
216        assert_eq!(svg.intrinsic_width(), Some(24.0));
217        assert_eq!(svg.intrinsic_height(), Some(16.0));
218    }
219
220    #[test]
221    fn percentage_dimensions_are_not_intrinsic() {
222        let src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="50%" viewBox="0 0 200 100"></svg>"#;
223        let svg = parse_svg_image(src).unwrap();
224        assert_eq!(svg.intrinsic_width(), None);
225        assert_eq!(svg.intrinsic_height(), None);
226    }
227
228    #[test]
229    fn unit_lengths_are_intrinsic() {
230        let src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="1.5em" viewBox="0 0 48 32"></svg>"#;
231        let svg = parse_svg_image(src).unwrap();
232        assert!(svg.intrinsic_width().is_some());
233        assert!(svg.intrinsic_height().is_some());
234    }
235
236    #[test]
237    fn non_numeric_dimensions_are_not_intrinsic() {
238        let src = br#"<svg xmlns="http://www.w3.org/2000/svg" width="auto" height="foo" viewBox="0 0 200 100"></svg>"#;
239        let svg = parse_svg_image(src).unwrap();
240        assert_eq!(svg.intrinsic_width(), None);
241        assert_eq!(svg.intrinsic_height(), None);
242    }
243}
244
245/// Creates an markup5ever::QualName.
246/// Given a local name and an optional namespace
247#[macro_export]
248macro_rules! qual_name {
249    ($local:tt $(, $ns:ident)?) => {
250        $crate::QualName {
251            prefix: None,
252            ns: $crate::ns!($($ns)?),
253            local: $crate::local_name!($local),
254        }
255    };
256}