Skip to main content

artifact_api/
image_transform.rs

1use serde::{Deserialize, Serialize};
2
3/// Explicit image conversion. Only the converted bytes become an artifact.
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "camelCase", deny_unknown_fields)]
6pub struct ImageTransform {
7    pub format: ImageOutputFormat,
8    #[serde(default, skip_serializing_if = "Option::is_none")]
9    pub max_width: Option<u32>,
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub max_height: Option<u32>,
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    pub quality: Option<u8>,
14    /// Explicit RGB background for transparent input converted to JPEG.
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub background: Option<String>,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "UPPERCASE")]
21pub enum ImageOutputFormat {
22    Jpeg,
23    Png,
24}
25
26impl ImageTransform {
27    pub fn validate(&self) -> Result<(), &'static str> {
28        for size in [self.max_width, self.max_height].into_iter().flatten() {
29            if !(1..=16_384).contains(&size) {
30                return Err("image transform dimensions must be between 1 and 16384");
31            }
32        }
33        if let Some(quality) = self.quality {
34            if self.format != ImageOutputFormat::Jpeg || !(1..=100).contains(&quality) {
35                return Err("image transform quality requires JPEG and a value from 1 to 100");
36            }
37        }
38        if let Some(color) = &self.background {
39            if self.format != ImageOutputFormat::Jpeg
40                || color.len() != 7
41                || !color.starts_with('#')
42                || !color.as_bytes()[1..].iter().all(u8::is_ascii_hexdigit)
43            {
44                return Err("image transform background requires JPEG and #RRGGBB");
45            }
46        }
47        Ok(())
48    }
49}