css_image/
lib.rs

1mod error;
2pub mod style;
3
4use cairo::{Context, ImageSurface};
5use error::CssError;
6use lazy_static::lazy_static;
7use rayon::prelude::*;
8use regex::Regex;
9use std::{collections::HashMap, ops::Deref};
10use style::{Parseable, Style};
11
12lazy_static! {
13    static ref RE: Regex =
14        Regex::new(r"(?P<selector>\S+)\s*\{\s*(?P<properties>[^}]+)\s*\}").unwrap();
15    static ref PROPERTY_RE: Regex =
16        Regex::new(r"(?P<property>[\w-]+):\s*(?P<value>[^;]+);").unwrap();
17}
18
19pub fn parse<T>(css: T) -> Result<Vec<Style>, CssError<'static>>
20where
21    T: AsRef<str>,
22{
23    let split = css
24        .as_ref()
25        .split_inclusive('}')
26        .filter_map(|selector| {
27            let selector = selector.trim();
28            if selector.is_empty() {
29                return None;
30            }
31            Some(selector)
32        })
33        .collect::<Vec<&str>>();
34
35    let all_selector = split.iter().find_map(|s| {
36        let mut properties: HashMap<Box<str>, String> = HashMap::new();
37
38        for cap in RE.captures_iter(s) {
39            if &cap["selector"] == "*" {
40                for property_cap in PROPERTY_RE.captures_iter(&cap["properties"]) {
41                    properties.insert(
42                        property_cap["property"].into(),
43                        property_cap["value"].to_string(),
44                    );
45                }
46                return Some(properties);
47            }
48        }
49        None
50    });
51
52    Ok(split
53        .par_iter()
54        .filter_map(|s| {
55            let mut properties = HashMap::with_capacity(split.len() - 1);
56
57            if let Some(cap) = RE.captures_iter(s).next() {
58                let selector = cap["selector"].to_string();
59                PROPERTY_RE
60                    .captures_iter(&cap["properties"])
61                    .for_each(|property_cap| {
62                        properties.insert(
63                            property_cap["property"].into(),
64                            property_cap["value"].to_string(),
65                        );
66                    });
67                return Some((selector, properties));
68            }
69
70            None
71        })
72        .map(|(selector, properties)| Style::new(selector, &properties, all_selector.as_ref()))
73        .collect::<Vec<Style>>())
74}
75
76pub fn render<T>(css: T) -> Result<HashMap<String, Vec<u8>>, CssError<'static>>
77where
78    T: Parseable,
79{
80    let mut styles = css.parse()?;
81
82    styles
83        .par_iter_mut()
84        .map(|style| {
85            let name = &style.selector;
86
87            let mut width = style.width;
88            let mut height = style.height;
89            let mut position = 0;
90
91            let mut text_width = 0;
92
93            if let Some(content) = &style.content {
94                if content.is_empty() {
95                    style.content = None;
96                }
97            }
98            if let Some(content) = &style.content {
99                let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)
100                    .map_err(|_| CssError::ContentError("Failed to create cairo surface"))?;
101                let context = Context::new(&surface)
102                    .map_err(|_| CssError::ContentError("Failed to create cairo context"))?;
103                let font = &style.font;
104
105                context.select_font_face(font.family.deref(), font.style, font.weight);
106                context.set_font_size(font.size);
107                let extents = context
108                    .text_extents(content.deref())
109                    .map_err(|_| CssError::ContentError(""))?;
110
111                if width.is_none() {
112                    width = Some(extents.width() as i32);
113                }
114                if height.is_none() {
115                    height = Some(extents.height() as i32);
116                }
117                text_width = extents.width() as i32;
118                position = extents.y_bearing().abs() as i32;
119            }
120
121            let margin = style.margin;
122            let padding = style.padding;
123
124            let width = width.unwrap_or(5);
125            let height = height.unwrap_or(5);
126
127            let surface = ImageSurface::create(
128                cairo::Format::ARgb32,
129                width + margin[1] + margin[3] + padding[1] + padding[3],
130                height + margin[0] + margin[2] + padding[0] + padding[2],
131            )
132            .map_err(|_| CssError::ContentError("Failed to create cairo surface"))?;
133            let mut img =
134                Vec::with_capacity(surface.width() as usize * surface.height() as usize * 4);
135
136            let context = Context::new(&surface)
137                .map_err(|_| CssError::ContentError("Failed to create cairo context"))?;
138
139            context.set_source_rgba(
140                style.background_color[0],
141                style.background_color[1],
142                style.background_color[2],
143                style.background_color[3],
144            );
145            draw_rectangle(
146                &context,
147                margin[3] as f64,
148                margin[0] as f64,
149                width as f64 + padding[1] as f64 + padding[3] as f64,
150                height as f64 + padding[0] as f64 + padding[2] as f64,
151                style.border_radius,
152            );
153            context
154                .fill_preserve()
155                .map_err(|_| CssError::ContentError("Failed to paint the surface"))?;
156
157            if let Some(text) = &style.content {
158                let font = &style.font;
159                context.select_font_face(font.family.deref(), font.style, font.weight);
160                context.set_font_size(font.size);
161                context.set_source_rgba(font.color[0], font.color[1], font.color[2], 1.0);
162                match font.text_align.deref() {
163                    "center" => {
164                        context.move_to(
165                            (width / 2 - text_width / 2) as f64 + padding[3] as f64,
166                            position as f64 + padding[0] as f64,
167                        );
168                    }
169                    "right" => {
170                        context.move_to(width as f64 - text_width as f64, position as f64);
171                    }
172                    "left" => {
173                        context.move_to(
174                            0.0 + padding[3] as f64 + margin[3] as f64,
175                            position as f64 + padding[0] as f64 + margin[0] as f64,
176                        );
177                    }
178                    _ => return Err(CssError::ContentError("Invalid text-align")),
179                }
180                _ = context.show_text(text.deref());
181            }
182
183            surface
184                .write_to_png(&mut img)
185                .map_err(|_| CssError::ContentError("Failed to write cairo surface as PNG"))?;
186
187            Ok((name.clone(), img))
188        })
189        .collect::<Result<HashMap<_, _>, CssError>>()
190}
191
192fn draw_rectangle(context: &Context, x: f64, y: f64, width: f64, height: f64, border_radius: f64) {
193    let border_radius = match border_radius > 20. {
194        true => 20. / 3.33,
195        false => border_radius / 3.33,
196    };
197    let degrees = std::f64::consts::PI / 180.0;
198
199    context.new_sub_path();
200    context.arc(
201        x + width - border_radius,
202        y + border_radius,
203        border_radius,
204        -90.0 * degrees,
205        0.0 * degrees,
206    );
207    context.arc(
208        x + width - border_radius,
209        y + height - border_radius,
210        border_radius,
211        0.0 * degrees,
212        90.0 * degrees,
213    );
214    context.arc(
215        x + border_radius,
216        y + height - border_radius,
217        border_radius,
218        90.0 * degrees,
219        180.0 * degrees,
220    );
221    context.arc(
222        x + border_radius,
223        y + border_radius,
224        border_radius,
225        180.0 * degrees,
226        270.0 * degrees,
227    );
228    context.close_path();
229}