Skip to main content

pebble_cms/services/
image.rs

1use anyhow::{bail, Result};
2use image::codecs::webp::WebPEncoder;
3use image::{DynamicImage, GenericImageView, ImageFormat};
4use std::io::Cursor;
5
6const MAX_WIDTH: u32 = 1600;
7const THUMBNAIL_SIZE: u32 = 200;
8
9pub struct OptimizedImage {
10    pub original: Vec<u8>,
11    pub original_format: ImageFormat,
12    pub webp: Vec<u8>,
13    pub width: u32,
14    pub height: u32,
15}
16
17pub struct ImageVariant {
18    pub width: u32,
19    pub data: Vec<u8>,
20    pub suffix: String,
21}
22
23pub fn is_optimizable_image(mime_type: &str) -> bool {
24    matches!(
25        mime_type,
26        "image/jpeg" | "image/png" | "image/gif" | "image/webp"
27    )
28}
29
30pub fn optimize_image(
31    data: &[u8],
32    mime_type: &str,
33    max_width: Option<u32>,
34) -> Result<OptimizedImage> {
35    let format = match mime_type {
36        "image/jpeg" => ImageFormat::Jpeg,
37        "image/png" => ImageFormat::Png,
38        "image/gif" => ImageFormat::Gif,
39        "image/webp" => ImageFormat::WebP,
40        _ => bail!("Unsupported image format: {}", mime_type),
41    };
42
43    let img = image::load_from_memory_with_format(data, format)?;
44    let (orig_width, orig_height) = img.dimensions();
45
46    let max_w = max_width.unwrap_or(MAX_WIDTH);
47    let resized = if orig_width > max_w {
48        let ratio = max_w as f32 / orig_width as f32;
49        let new_height = (orig_height as f32 * ratio) as u32;
50        img.resize(max_w, new_height, image::imageops::FilterType::Lanczos3)
51    } else {
52        img.clone()
53    };
54
55    let (final_width, final_height) = resized.dimensions();
56
57    let original = encode_image(&resized, format)?;
58    let webp = encode_webp(&resized)?;
59
60    Ok(OptimizedImage {
61        original,
62        original_format: format,
63        webp,
64        width: final_width,
65        height: final_height,
66    })
67}
68
69pub fn generate_thumbnail(data: &[u8], size: Option<u32>) -> Result<Vec<u8>> {
70    let img = image::load_from_memory(data)?;
71    let thumb_size = size.unwrap_or(THUMBNAIL_SIZE);
72
73    let thumbnail = img.resize_to_fill(
74        thumb_size,
75        thumb_size,
76        image::imageops::FilterType::Lanczos3,
77    );
78
79    encode_webp(&thumbnail)
80}
81
82pub fn generate_srcset_variants(data: &[u8]) -> Result<Vec<ImageVariant>> {
83    let widths = [400, 800, 1200, 1600];
84    let img = image::load_from_memory(data)?;
85    let (orig_width, _) = img.dimensions();
86
87    let mut variants = Vec::new();
88
89    for &width in &widths {
90        if width > orig_width {
91            continue;
92        }
93
94        let ratio = width as f32 / orig_width as f32;
95        let new_height = (img.height() as f32 * ratio) as u32;
96        let resized = img.resize(width, new_height, image::imageops::FilterType::Lanczos3);
97
98        let webp_data = encode_webp(&resized)?;
99
100        variants.push(ImageVariant {
101            width,
102            data: webp_data,
103            suffix: format!("-{}w", width),
104        });
105    }
106
107    Ok(variants)
108}
109
110fn encode_image(img: &DynamicImage, format: ImageFormat) -> Result<Vec<u8>> {
111    let mut buffer = Cursor::new(Vec::new());
112
113    match format {
114        ImageFormat::Jpeg => {
115            let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buffer, 85);
116            img.write_with_encoder(encoder)?;
117        }
118        ImageFormat::Png => {
119            img.write_to(&mut buffer, ImageFormat::Png)?;
120        }
121        ImageFormat::Gif => {
122            img.write_to(&mut buffer, ImageFormat::Gif)?;
123        }
124        ImageFormat::WebP => {
125            return encode_webp(img);
126        }
127        _ => bail!("Unsupported format for encoding"),
128    }
129
130    Ok(buffer.into_inner())
131}
132
133fn encode_webp(img: &DynamicImage) -> Result<Vec<u8>> {
134    let rgba = img.to_rgba8();
135    let (width, height) = rgba.dimensions();
136
137    let mut buffer = Cursor::new(Vec::new());
138    let encoder = WebPEncoder::new_lossless(&mut buffer);
139    encoder.encode(&rgba, width, height, image::ExtendedColorType::Rgba8)?;
140
141    Ok(buffer.into_inner())
142}