pebble_cms/services/
image.rs1use 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}