lightningcss/values/
image.rs

1//! CSS image values.
2
3use super::color::ColorFallbackKind;
4use super::gradient::*;
5use super::resolution::Resolution;
6use crate::compat;
7use crate::dependencies::{Dependency, UrlDependency};
8use crate::error::{ParserError, PrinterError};
9use crate::prefixes::{is_webkit_gradient, Feature};
10use crate::printer::Printer;
11use crate::targets::{Browsers, Targets};
12use crate::traits::{FallbackValues, IsCompatible, Parse, ToCss};
13use crate::values::string::CowArcStr;
14use crate::values::url::Url;
15use crate::vendor_prefix::VendorPrefix;
16#[cfg(feature = "visitor")]
17use crate::visitor::Visit;
18use cssparser::*;
19use smallvec::SmallVec;
20
21/// A CSS [`<image>`](https://www.w3.org/TR/css-images-3/#image-values) value.
22#[derive(Debug, Clone, PartialEq, Parse, ToCss)]
23#[cfg_attr(feature = "visitor", derive(Visit))]
24#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
25#[cfg_attr(feature = "visitor", visit(visit_image, IMAGES))]
26#[cfg_attr(
27  feature = "serde",
28  derive(serde::Serialize, serde::Deserialize),
29  serde(tag = "type", content = "value", rename_all = "kebab-case")
30)]
31#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
32pub enum Image<'i> {
33  /// The `none` keyword.
34  None,
35  /// A `url()`.
36  #[cfg_attr(feature = "serde", serde(borrow))]
37  Url(Url<'i>),
38  /// A gradient.
39  Gradient(Box<Gradient>),
40  /// An `image-set()`.
41  ImageSet(ImageSet<'i>),
42}
43
44impl<'i> Default for Image<'i> {
45  fn default() -> Image<'i> {
46    Image::None
47  }
48}
49
50impl<'i> Image<'i> {
51  /// Returns whether the image includes any vendor prefixed values.
52  pub fn has_vendor_prefix(&self) -> bool {
53    let prefix = self.get_vendor_prefix();
54    !prefix.is_empty() && prefix != VendorPrefix::None
55  }
56
57  /// Returns the vendor prefix used in the image value.
58  pub fn get_vendor_prefix(&self) -> VendorPrefix {
59    match self {
60      Image::Gradient(a) => a.get_vendor_prefix(),
61      Image::ImageSet(a) => a.get_vendor_prefix(),
62      _ => VendorPrefix::empty(),
63    }
64  }
65
66  /// Returns the vendor prefixes that are needed for the given browser targets.
67  pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
68    match self {
69      Image::Gradient(grad) => grad.get_necessary_prefixes(targets),
70      Image::ImageSet(image_set) => image_set.get_necessary_prefixes(targets),
71      _ => VendorPrefix::None,
72    }
73  }
74
75  /// Returns a vendor prefixed version of the image for the given vendor prefixes.
76  pub fn get_prefixed(&self, prefix: VendorPrefix) -> Image<'i> {
77    match self {
78      Image::Gradient(grad) => Image::Gradient(Box::new(grad.get_prefixed(prefix))),
79      Image::ImageSet(image_set) => Image::ImageSet(image_set.get_prefixed(prefix)),
80      _ => self.clone(),
81    }
82  }
83
84  /// Returns a legacy `-webkit-gradient()` value for the image.
85  ///
86  /// May return an error in case the gradient cannot be converted.
87  pub fn get_legacy_webkit(&self) -> Result<Image<'i>, ()> {
88    match self {
89      Image::Gradient(grad) => Ok(Image::Gradient(Box::new(grad.get_legacy_webkit()?))),
90      _ => Ok(self.clone()),
91    }
92  }
93
94  /// Returns the color fallbacks that are needed for the given browser targets.
95  pub fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
96    match self {
97      Image::Gradient(grad) => grad.get_necessary_fallbacks(targets),
98      _ => ColorFallbackKind::empty(),
99    }
100  }
101
102  /// Returns a fallback version of the image for the given color fallback type.
103  pub fn get_fallback(&self, kind: ColorFallbackKind) -> Image<'i> {
104    match self {
105      Image::Gradient(grad) => Image::Gradient(Box::new(grad.get_fallback(kind))),
106      _ => self.clone(),
107    }
108  }
109}
110
111impl<'i> IsCompatible for Image<'i> {
112  fn is_compatible(&self, browsers: Browsers) -> bool {
113    match self {
114      Image::Gradient(g) => match &**g {
115        Gradient::Linear(g) => {
116          compat::Feature::LinearGradient.is_compatible(browsers) && g.is_compatible(browsers)
117        }
118        Gradient::RepeatingLinear(g) => {
119          compat::Feature::RepeatingLinearGradient.is_compatible(browsers) && g.is_compatible(browsers)
120        }
121        Gradient::Radial(g) => {
122          compat::Feature::RadialGradient.is_compatible(browsers) && g.is_compatible(browsers)
123        }
124        Gradient::RepeatingRadial(g) => {
125          compat::Feature::RepeatingRadialGradient.is_compatible(browsers) && g.is_compatible(browsers)
126        }
127        Gradient::Conic(g) => compat::Feature::ConicGradient.is_compatible(browsers) && g.is_compatible(browsers),
128        Gradient::RepeatingConic(g) => {
129          compat::Feature::RepeatingConicGradient.is_compatible(browsers) && g.is_compatible(browsers)
130        }
131        Gradient::WebKitGradient(..) => is_webkit_gradient(browsers),
132      },
133      Image::ImageSet(i) => i.is_compatible(browsers),
134      Image::Url(..) | Image::None => true,
135    }
136  }
137}
138
139pub(crate) trait ImageFallback<'i>: Sized {
140  fn get_image(&self) -> &Image<'i>;
141  fn with_image(&self, image: Image<'i>) -> Self;
142
143  #[inline]
144  fn get_necessary_fallbacks(&self, targets: Targets) -> ColorFallbackKind {
145    self.get_image().get_necessary_fallbacks(targets)
146  }
147
148  #[inline]
149  fn get_fallback(&self, kind: ColorFallbackKind) -> Self {
150    self.with_image(self.get_image().get_fallback(kind))
151  }
152}
153
154impl<'i> ImageFallback<'i> for Image<'i> {
155  #[inline]
156  fn get_image(&self) -> &Image<'i> {
157    self
158  }
159
160  #[inline]
161  fn with_image(&self, image: Image<'i>) -> Self {
162    image
163  }
164}
165
166impl<'i> FallbackValues for Image<'i> {
167  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
168    // Determine which prefixes and color fallbacks are needed.
169    let prefixes = self.get_necessary_prefixes(targets);
170    let fallbacks = self.get_necessary_fallbacks(targets);
171    let mut res = Vec::new();
172
173    // Get RGB fallbacks if needed.
174    let rgb = if fallbacks.contains(ColorFallbackKind::RGB) {
175      Some(self.get_fallback(ColorFallbackKind::RGB))
176    } else {
177      None
178    };
179
180    // Prefixed properties only support RGB.
181    let prefix_image = rgb.as_ref().unwrap_or(self);
182
183    // Legacy -webkit-gradient()
184    if prefixes.contains(VendorPrefix::WebKit)
185      && targets.browsers.map(is_webkit_gradient).unwrap_or(false)
186      && matches!(prefix_image, Image::Gradient(_))
187    {
188      if let Ok(legacy) = prefix_image.get_legacy_webkit() {
189        res.push(legacy);
190      }
191    }
192
193    // Standard syntax, with prefixes.
194    if prefixes.contains(VendorPrefix::WebKit) {
195      res.push(prefix_image.get_prefixed(VendorPrefix::WebKit))
196    }
197
198    if prefixes.contains(VendorPrefix::Moz) {
199      res.push(prefix_image.get_prefixed(VendorPrefix::Moz))
200    }
201
202    if prefixes.contains(VendorPrefix::O) {
203      res.push(prefix_image.get_prefixed(VendorPrefix::O))
204    }
205
206    if prefixes.contains(VendorPrefix::None) {
207      // Unprefixed, rgb fallback.
208      if let Some(rgb) = rgb {
209        res.push(rgb);
210      }
211
212      // P3 fallback.
213      if fallbacks.contains(ColorFallbackKind::P3) {
214        res.push(self.get_fallback(ColorFallbackKind::P3));
215      }
216
217      // Convert original to lab if needed (e.g. if oklab is not supported but lab is).
218      if fallbacks.contains(ColorFallbackKind::LAB) {
219        *self = self.get_fallback(ColorFallbackKind::LAB);
220      }
221    } else if let Some(last) = res.pop() {
222      // Prefixed property with no unprefixed version.
223      // Replace self with the last prefixed version so that it doesn't
224      // get duplicated when the caller pushes the original value.
225      *self = last;
226    }
227
228    res
229  }
230}
231
232impl<'i, T: ImageFallback<'i>> FallbackValues for SmallVec<[T; 1]> {
233  fn get_fallbacks(&mut self, targets: Targets) -> Vec<Self> {
234    // Determine what vendor prefixes and color fallbacks are needed.
235    let mut prefixes = VendorPrefix::empty();
236    let mut fallbacks = ColorFallbackKind::empty();
237    let mut res = Vec::new();
238    for item in self.iter() {
239      prefixes |= item.get_image().get_necessary_prefixes(targets);
240      fallbacks |= item.get_necessary_fallbacks(targets);
241    }
242
243    // Get RGB fallbacks if needed.
244    let rgb: Option<SmallVec<[T; 1]>> = if fallbacks.contains(ColorFallbackKind::RGB) {
245      Some(self.iter().map(|item| item.get_fallback(ColorFallbackKind::RGB)).collect())
246    } else {
247      None
248    };
249
250    // Prefixed properties only support RGB.
251    let prefix_images = rgb.as_ref().unwrap_or(&self);
252
253    // Legacy -webkit-gradient()
254    if prefixes.contains(VendorPrefix::WebKit) && targets.browsers.map(is_webkit_gradient).unwrap_or(false) {
255      let images: SmallVec<[T; 1]> = prefix_images
256        .iter()
257        .map(|item| item.get_image().get_legacy_webkit().map(|image| item.with_image(image)))
258        .flatten()
259        .collect();
260      if !images.is_empty() {
261        res.push(images)
262      }
263    }
264
265    // Standard syntax, with prefixes.
266    macro_rules! prefix {
267      ($prefix: ident) => {
268        if prefixes.contains(VendorPrefix::$prefix) {
269          let images = prefix_images
270            .iter()
271            .map(|item| {
272              let image = item.get_image().get_prefixed(VendorPrefix::$prefix);
273              item.with_image(image)
274            })
275            .collect();
276          res.push(images)
277        }
278      };
279    }
280
281    prefix!(WebKit);
282    prefix!(Moz);
283    prefix!(O);
284    if prefixes.contains(VendorPrefix::None) {
285      if let Some(rgb) = rgb {
286        res.push(rgb);
287      }
288
289      if fallbacks.contains(ColorFallbackKind::P3) {
290        let p3_images = self.iter().map(|item| item.get_fallback(ColorFallbackKind::P3)).collect();
291
292        res.push(p3_images)
293      }
294
295      // Convert to lab if needed (e.g. if oklab is not supported but lab is).
296      if fallbacks.contains(ColorFallbackKind::LAB) {
297        for item in self.iter_mut() {
298          *item = item.get_fallback(ColorFallbackKind::LAB);
299        }
300      }
301    } else if let Some(last) = res.pop() {
302      // Prefixed property with no unprefixed version.
303      // Replace self with the last prefixed version so that it doesn't
304      // get duplicated when the caller pushes the original value.
305      *self = last;
306    }
307
308    res
309  }
310}
311
312/// A CSS [`image-set()`](https://drafts.csswg.org/css-images-4/#image-set-notation) value.
313///
314/// `image-set()` allows the user agent to choose between multiple versions of an image to
315/// display the most appropriate resolution or file type that it supports.
316#[derive(Debug, Clone, PartialEq)]
317#[cfg_attr(feature = "visitor", derive(Visit))]
318#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
319#[cfg_attr(
320  feature = "serde",
321  derive(serde::Serialize, serde::Deserialize),
322  serde(rename_all = "camelCase")
323)]
324#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
325pub struct ImageSet<'i> {
326  /// The image options to choose from.
327  #[cfg_attr(feature = "serde", serde(borrow))]
328  pub options: Vec<ImageSetOption<'i>>,
329  /// The vendor prefix for the `image-set()` function.
330  pub vendor_prefix: VendorPrefix,
331}
332
333impl<'i> ImageSet<'i> {
334  /// Returns the vendor prefix for the `image-set()`.
335  pub fn get_vendor_prefix(&self) -> VendorPrefix {
336    self.vendor_prefix
337  }
338
339  /// Returns the vendor prefixes needed for the given browser targets.
340  pub fn get_necessary_prefixes(&self, targets: Targets) -> VendorPrefix {
341    targets.prefixes(self.vendor_prefix, Feature::ImageSet)
342  }
343
344  /// Returns the `image-set()` value with the given vendor prefix.
345  pub fn get_prefixed(&self, prefix: VendorPrefix) -> ImageSet<'i> {
346    ImageSet {
347      options: self.options.clone(),
348      vendor_prefix: prefix,
349    }
350  }
351}
352
353impl<'i> Parse<'i> for ImageSet<'i> {
354  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
355    let location = input.current_source_location();
356    let f = input.expect_function()?;
357    let vendor_prefix = match_ignore_ascii_case! { &*f,
358      "image-set" => VendorPrefix::None,
359      "-webkit-image-set" => VendorPrefix::WebKit,
360      _ => return Err(location.new_unexpected_token_error(
361        cssparser::Token::Ident(f.clone())
362      ))
363    };
364
365    let options = input.parse_nested_block(|input| input.parse_comma_separated(ImageSetOption::parse))?;
366    Ok(ImageSet { options, vendor_prefix })
367  }
368}
369
370impl<'i> ToCss for ImageSet<'i> {
371  fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
372  where
373    W: std::fmt::Write,
374  {
375    self.vendor_prefix.to_css(dest)?;
376    dest.write_str("image-set(")?;
377    let mut first = true;
378    for option in &self.options {
379      if first {
380        first = false;
381      } else {
382        dest.delim(',', false)?;
383      }
384      option.to_css(dest, self.vendor_prefix != VendorPrefix::None)?;
385    }
386    dest.write_char(')')
387  }
388}
389
390impl<'i> IsCompatible for ImageSet<'i> {
391  fn is_compatible(&self, browsers: Browsers) -> bool {
392    compat::Feature::ImageSet.is_compatible(browsers)
393      && self.options.iter().all(|opt| opt.image.is_compatible(browsers))
394  }
395}
396
397/// An image option within the `image-set()` function. See [ImageSet](ImageSet).
398#[derive(Debug, Clone, PartialEq)]
399#[cfg_attr(feature = "visitor", derive(Visit))]
400#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
401#[cfg_attr(
402  feature = "serde",
403  derive(serde::Serialize, serde::Deserialize),
404  serde(rename_all = "camelCase")
405)]
406#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
407pub struct ImageSetOption<'i> {
408  /// The image for this option.
409  #[cfg_attr(feature = "visitor", skip_type)]
410  pub image: Image<'i>,
411  /// The resolution of the image.
412  pub resolution: Resolution,
413  /// The mime type of the image.
414  #[cfg_attr(feature = "serde", serde(borrow))]
415  pub file_type: Option<CowArcStr<'i>>,
416}
417
418impl<'i> Parse<'i> for ImageSetOption<'i> {
419  fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
420    let loc = input.current_source_location();
421    let image = if let Ok(url) = input.try_parse(|input| input.expect_url_or_string()) {
422      Image::Url(Url {
423        url: url.into(),
424        loc: loc.into(),
425      })
426    } else {
427      Image::parse(input)?
428    };
429
430    let (resolution, file_type) = if let Ok(res) = input.try_parse(Resolution::parse) {
431      let file_type = input.try_parse(parse_file_type).ok();
432      (res, file_type)
433    } else {
434      let file_type = input.try_parse(parse_file_type).ok();
435      let resolution = input.try_parse(Resolution::parse).unwrap_or(Resolution::Dppx(1.0));
436      (resolution, file_type)
437    };
438
439    Ok(ImageSetOption {
440      image,
441      resolution,
442      file_type: file_type.map(|x| x.into()),
443    })
444  }
445}
446
447impl<'i> ImageSetOption<'i> {
448  fn to_css<W>(&self, dest: &mut Printer<W>, is_prefixed: bool) -> Result<(), PrinterError>
449  where
450    W: std::fmt::Write,
451  {
452    match &self.image {
453      // Prefixed syntax didn't allow strings, only url()
454      Image::Url(url) if !is_prefixed => {
455        // Add dependency if needed. Normally this is handled by the Url type.
456        let dep = if dest.dependencies.is_some() {
457          Some(UrlDependency::new(url, dest.filename()))
458        } else {
459          None
460        };
461        if let Some(dep) = dep {
462          serialize_string(&dep.placeholder, dest)?;
463          if let Some(dependencies) = &mut dest.dependencies {
464            dependencies.push(Dependency::Url(dep))
465          }
466        } else {
467          serialize_string(&url.url, dest)?;
468        }
469      }
470      _ => self.image.to_css(dest)?,
471    }
472
473    // TODO: Throwing an error when `self.resolution = Resolution::Dppx(0.0)`
474    // TODO: -webkit-image-set() does not support `<image()> | <image-set()> |
475    // <cross-fade()> | <element()> | <gradient>` and `type(<string>)`.
476    dest.write_char(' ')?;
477
478    // Safari only supports the x resolution unit in image-set().
479    // In other places, x was added as an alias later.
480    // Temporarily ignore the targets while printing here.
481    let targets = std::mem::take(&mut dest.targets);
482    self.resolution.to_css(dest)?;
483    dest.targets = targets;
484
485    if let Some(file_type) = &self.file_type {
486      dest.write_str(" type(")?;
487      serialize_string(&file_type, dest)?;
488      dest.write_char(')')?;
489    }
490
491    Ok(())
492  }
493}
494
495fn parse_file_type<'i, 't>(input: &mut Parser<'i, 't>) -> Result<CowRcStr<'i>, ParseError<'i, ParserError<'i>>> {
496  input.expect_function_matching("type")?;
497  input.parse_nested_block(|input| Ok(input.expect_string_cloned()?))
498}