swc_css_minifier/compressor/
color.rs

1use swc_atoms::{atom, Atom};
2use swc_common::DUMMY_SP;
3use swc_css_ast::*;
4use swc_css_utils::{angle_to_deg, hsl_to_rgb, hwb_to_rgb, to_rgb255, NAMED_COLORS};
5
6use super::Compressor;
7use crate::compressor::alpha_value::compress_alpha_value;
8
9fn compress_alpha_in_hex(value: &Atom) -> Option<&str> {
10    let length = value.len();
11
12    if length == 3 || length == 6 {
13        return None;
14    }
15
16    let chars = value.as_bytes();
17
18    if length == 8
19        && (chars[6] == b'f' || chars[6] == b'F')
20        && (chars[7] == b'f' || chars[7] == b'F')
21    {
22        return Some(&value[0..6]);
23    } else if length == 4 && chars[3] == b'f' || chars[3] == b'F' {
24        return Some(&value[0..3]);
25    }
26
27    None
28}
29
30fn get_short_hex(v: u32) -> u32 {
31    ((v & 0x0ff00000) >> 12) | ((v & 0x00000ff0) >> 4)
32}
33
34fn get_long_hex(v: u32) -> u32 {
35    ((v & 0xf000) << 16)
36        | ((v & 0xff00) << 12)
37        | ((v & 0x0ff0) << 8)
38        | ((v & 0x00ff) << 4)
39        | (v & 0x000f)
40}
41
42fn get_named_color_by_hex(v: u32) -> Option<&'static str> {
43    // These names are shorter than their hex codes
44    let s = match v {
45        0x000080 => "navy",
46        0x008000 => "green",
47        0x008080 => "teal",
48        0x4b0082 => "indigo",
49        0x800000 => "maroon",
50        0x800080 => "purple",
51        0x808000 => "olive",
52        0x808080 => "gray",
53        0xa0522d => "sienna",
54        0xa52a2a => "brown",
55        0xc0c0c0 => "silver",
56        0xcd853f => "peru",
57        0xd2b48c => "tan",
58        0xda70d6 => "orchid",
59        0xdda0dd => "plum",
60        0xee82ee => "violet",
61        0xf0e68c => "khaki",
62        0xf0ffff => "azure",
63        0xf5deb3 => "wheat",
64        0xf5f5dc => "beige",
65        0xfa8072 => "salmon",
66        0xfaf0e6 => "linen",
67        0xff0000 => "red",
68        0xff6347 => "tomato",
69        0xff7f50 => "coral",
70        0xffa500 => "orange",
71        0xffc0cb => "pink",
72        0xffd700 => "gold",
73        0xffe4c4 => "bisque",
74        0xfffafa => "snow",
75        0xfffff0 => "ivory",
76        _ => return None,
77    };
78
79    Some(s)
80}
81
82macro_rules! make_color {
83    ($span:expr,$r:expr,$g:expr,$b:expr, $a:expr) => {{
84        let need_alpha_value = $a != 1.0;
85
86        let r = $r.round();
87        let g = $g.round();
88        let b = $b.round();
89
90        if need_alpha_value {
91            // TODO improve when we will have browserslist
92            let is_alpha_hex_supported = false;
93
94            if is_alpha_hex_supported {
95                let alpha = (($a * 255.0) as f64).round().max(0.0).min(255.0) as u8;
96                let hex: u32 =
97                    ((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | (alpha as u32);
98
99                let compact = get_short_hex(hex);
100                let value = if hex == get_long_hex(compact) {
101                    format!("{:04x}", compact)
102                } else {
103                    format!("{:08x}", hex)
104                };
105
106                Color::AbsoluteColorBase(AbsoluteColorBase::HexColor(HexColor {
107                    span: $span,
108                    value: value.into(),
109                    raw: None,
110                }))
111            } else {
112                let mut alpha_value = AlphaValue::Number(Number {
113                    span: DUMMY_SP,
114                    value: $a,
115                    raw: None,
116                });
117
118                compress_alpha_value(&mut alpha_value);
119
120                Color::AbsoluteColorBase(AbsoluteColorBase::Function(Function {
121                    span: $span,
122                    name: FunctionName::Ident(Ident {
123                        span: DUMMY_SP,
124                        value: atom!("rgba"),
125                        raw: None,
126                    }),
127                    value: vec![
128                        ComponentValue::Number(Box::new(Number {
129                            span: DUMMY_SP,
130                            value: r,
131                            raw: None,
132                        })),
133                        ComponentValue::Delimiter(Box::new(Delimiter {
134                            span: DUMMY_SP,
135                            value: DelimiterValue::Comma,
136                        })),
137                        ComponentValue::Number(Box::new(Number {
138                            span: DUMMY_SP,
139                            value: g,
140                            raw: None,
141                        })),
142                        ComponentValue::Delimiter(Box::new(Delimiter {
143                            span: DUMMY_SP,
144                            value: DelimiterValue::Comma,
145                        })),
146                        ComponentValue::Number(Box::new(Number {
147                            span: DUMMY_SP,
148                            value: b,
149                            raw: None,
150                        })),
151                        ComponentValue::Delimiter(Box::new(Delimiter {
152                            span: DUMMY_SP,
153                            value: DelimiterValue::Comma,
154                        })),
155                        ComponentValue::AlphaValue(Box::new(alpha_value)),
156                    ],
157                }))
158            }
159        } else {
160            let hex: u32 = ((r as u32) << 16) | ((g as u32) << 8) | (b as u32);
161
162            if let Some(name) = get_named_color_by_hex(hex) {
163                Color::AbsoluteColorBase(AbsoluteColorBase::NamedColorOrTransparent(Ident {
164                    span: $span,
165                    value: name.into(),
166                    raw: None,
167                }))
168            } else {
169                let compact = get_short_hex(hex);
170                let value = if hex == get_long_hex(compact) {
171                    format!("{:03x}", compact)
172                } else {
173                    format!("{:06x}", hex)
174                };
175
176                Color::AbsoluteColorBase(AbsoluteColorBase::HexColor(HexColor {
177                    span: $span,
178                    value: value.into(),
179                    raw: None,
180                }))
181            }
182        }
183    }};
184}
185
186impl Compressor {
187    fn get_named_color_by_hex(&self, hex: &str) -> Option<&'static str> {
188        let name = match hex {
189            "000080" => "navy",
190            "008000" => "green",
191            "008080" => "teal",
192            "4b0082" => "indigo",
193            "800000" => "maroon",
194            "800080" => "purple",
195            "808000" => "olive",
196            "808080" => "gray",
197            "a0522d" => "sienna",
198            "a52a2a" => "brown",
199            "c0c0c0" => "silver",
200            "cd853f" => "peru",
201            "d2b48c" => "tan",
202            "da70d6" => "orchid",
203            "dda0dd" => "plum",
204            "ee82ee" => "violet",
205            "f0e68c" => "khaki",
206            "f0ffff" => "azure",
207            "f5deb3" => "wheat",
208            "f5f5dc" => "beige",
209            "fa8072" => "salmon",
210            "faf0e6" => "linen",
211            "ff0000" => "red",
212            "ff6347" => "tomato",
213            "ff7f50" => "coral",
214            "ffa500" => "orange",
215            "ffc0cb" => "pink",
216            "ffd700" => "gold",
217            "ffe4c4" => "bisque",
218            "fffafa" => "snow",
219            "fffff0" => "ivory",
220            _ => return None,
221        };
222
223        Some(name)
224    }
225
226    fn get_alpha_value(&self, alpha_value: Option<&&ComponentValue>) -> Option<f64> {
227        let Some(alpha_value) = alpha_value else {
228            return Some(1.0);
229        };
230
231        match &alpha_value {
232            ComponentValue::AlphaValue(alpha_value) => match &**alpha_value {
233                AlphaValue::Number(Number { value, .. }) => {
234                    if *value > 1.0 {
235                        return Some(1.0);
236                    } else if *value < 0.0 {
237                        return Some(0.0);
238                    }
239
240                    Some(*value)
241                }
242                AlphaValue::Percentage(Percentage {
243                    value: Number { value, .. },
244                    ..
245                }) => {
246                    if *value > 100.0 {
247                        return Some(1.0);
248                    } else if *value < 0.0 {
249                        return Some(0.0);
250                    }
251
252                    Some(*value / 100.0)
253                }
254            },
255            ComponentValue::Ident(ident) if ident.value.eq_ignore_ascii_case("none") => Some(0.0),
256            _ => None,
257        }
258    }
259
260    fn get_hue(&self, hue: Option<&&ComponentValue>) -> Option<f64> {
261        match hue {
262            Some(ComponentValue::Hue(hue)) => {
263                let mut value = match &**hue {
264                    Hue::Number(Number { value, .. }) => *value,
265                    Hue::Angle(Angle {
266                        value: Number { value, .. },
267                        unit: Ident { value: unit, .. },
268                        ..
269                    }) => angle_to_deg(*value, unit),
270                };
271
272                value %= 360.0;
273
274                if value < 0.0 {
275                    value += 360.0;
276                }
277
278                Some(value)
279            }
280            Some(ComponentValue::Ident(ident)) if ident.value.eq_ignore_ascii_case("none") => {
281                Some(0.0)
282            }
283            _ => None,
284        }
285    }
286
287    fn get_percentage(&self, percentage: Option<&&ComponentValue>) -> Option<f64> {
288        match percentage {
289            Some(ComponentValue::Percentage(percentage)) => {
290                let Number { value, .. } = &percentage.value;
291                if *value > 100.0 {
292                    return Some(1.0);
293                } else if *value < 0.0 {
294                    return Some(0.0);
295                }
296
297                Some(*value / 100.0)
298            }
299            Some(ComponentValue::Ident(ident)) if ident.value.eq_ignore_ascii_case("none") => {
300                Some(0.0)
301            }
302            _ => None,
303        }
304    }
305
306    fn get_number_or_percentage(
307        &self,
308        number_or_percentage: Option<&&ComponentValue>,
309    ) -> Option<f64> {
310        match number_or_percentage {
311            Some(ComponentValue::Number(number)) => {
312                if number.value > 255.0 {
313                    return Some(255.0);
314                } else if number.value < 0.0 {
315                    return Some(0.0);
316                }
317
318                Some(number.value)
319            }
320            Some(ComponentValue::Percentage(percentage)) => {
321                if percentage.value.value > 100.0 {
322                    return Some(255.0);
323                } else if percentage.value.value < 0.0 {
324                    return Some(0.0);
325                }
326
327                Some((2.55 * percentage.value.value).round())
328            }
329            Some(ComponentValue::Ident(ident)) if ident.value.eq_ignore_ascii_case("none") => {
330                Some(0.0)
331            }
332            _ => None,
333        }
334    }
335}
336
337impl Compressor {
338    pub(super) fn compress_color(&self, color: &mut Color) {
339        match color {
340            Color::AbsoluteColorBase(AbsoluteColorBase::NamedColorOrTransparent(Ident {
341                value,
342                span,
343                ..
344            })) => match value.to_ascii_lowercase() {
345                ref s if *s == "transparent" => {
346                    *color = make_color!(*span, 0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64);
347                }
348                name => {
349                    if let Some(value) = NAMED_COLORS.get(&name) {
350                        *color = make_color!(
351                            *span,
352                            value.rgb[0] as f64,
353                            value.rgb[1] as f64,
354                            value.rgb[2] as f64,
355                            1.0
356                        )
357                    }
358                }
359            },
360            Color::AbsoluteColorBase(AbsoluteColorBase::HexColor(HexColor {
361                span, value, ..
362            })) => {
363                if let Some(value) = self.get_named_color_by_hex(value) {
364                    *color = Color::AbsoluteColorBase(AbsoluteColorBase::NamedColorOrTransparent(
365                        Ident {
366                            span: *span,
367                            value: value.into(),
368                            raw: None,
369                        },
370                    ));
371                } else if let Some(new_value) = compress_alpha_in_hex(value) {
372                    *value = new_value.into();
373                }
374            }
375            Color::AbsoluteColorBase(AbsoluteColorBase::Function(Function {
376                span,
377                name,
378                value,
379                ..
380            })) if name == "rgb" || name == "rgba" => {
381                let rgba: Vec<_> = value
382                    .iter()
383                    .filter(|n| {
384                        !n.as_delimiter()
385                            .map(|delimiter| delimiter.value)
386                            .map(|v| matches!(v, DelimiterValue::Comma | DelimiterValue::Solidus))
387                            .unwrap_or_default()
388                    })
389                    .collect();
390
391                let r = match self.get_number_or_percentage(rgba.first()) {
392                    Some(value) => value,
393                    _ => return,
394                };
395                let g = match self.get_number_or_percentage(rgba.get(1)) {
396                    Some(value) => value,
397                    _ => return,
398                };
399                let b = match self.get_number_or_percentage(rgba.get(2)) {
400                    Some(value) => value,
401                    _ => return,
402                };
403                let a = match self.get_alpha_value(rgba.get(3)) {
404                    Some(value) => value,
405                    _ => return,
406                };
407
408                *color = make_color!(*span, r, g, b, a);
409            }
410            Color::AbsoluteColorBase(AbsoluteColorBase::Function(Function {
411                span,
412                name,
413                value,
414                ..
415            })) if name == "hsl" || name == "hsla" => {
416                let hsla: Vec<_> = value
417                    .iter()
418                    .filter(|n| {
419                        !n.as_delimiter()
420                            .map(|delimiter| delimiter.value)
421                            .map(|v| matches!(v, DelimiterValue::Comma | DelimiterValue::Solidus))
422                            .unwrap_or_default()
423                    })
424                    .collect();
425
426                let h = match self.get_hue(hsla.first()) {
427                    Some(value) => value,
428                    _ => return,
429                };
430                let s = match self.get_percentage(hsla.get(1)) {
431                    Some(value) => value,
432                    _ => return,
433                };
434                let l = match self.get_percentage(hsla.get(2)) {
435                    Some(value) => value,
436                    _ => return,
437                };
438                let a = match self.get_alpha_value(hsla.get(3)) {
439                    Some(value) => value,
440                    _ => return,
441                };
442
443                let rgb = to_rgb255(hsl_to_rgb([h, s, l]));
444
445                *color = make_color!(*span, rgb[0], rgb[1], rgb[2], a);
446            }
447            Color::AbsoluteColorBase(AbsoluteColorBase::Function(Function {
448                span,
449                name,
450                value,
451                ..
452            })) if name == "hwb" => {
453                let h = match self.get_hue(value.first().as_ref()) {
454                    Some(value) => value,
455                    _ => return,
456                };
457                let w = match self.get_percentage(value.get(1).as_ref()) {
458                    Some(value) => value,
459                    _ => return,
460                };
461                let b = match self.get_percentage(value.get(2).as_ref()) {
462                    Some(value) => value,
463                    _ => return,
464                };
465                let a = match self.get_alpha_value(value.get(4).as_ref()) {
466                    Some(value) => value,
467                    _ => return,
468                };
469
470                let rgb = to_rgb255(hwb_to_rgb([h, w, b]));
471
472                *color = make_color!(*span, rgb[0], rgb[1], rgb[2], a);
473            }
474            _ => {}
475        }
476    }
477}