1use skia_safe::{Color, jpeg_encoder, webp_encoder};
2
3use crate::SvgRenderError;
4
5pub(crate) const MAX_RENDER_BYTES: usize = 512 * 1024 * 1024;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct RenderSize {
15 pub width: u32,
16 pub height: u32,
17}
18
19impl RenderSize {
20 pub fn new(width: u32, height: u32) -> Result<Self, SvgRenderError> {
26 if width == 0 || height == 0 {
27 return Err(SvgRenderError::InvalidSize { width, height });
28 }
29
30 if width > i32::MAX as u32 || height > i32::MAX as u32 {
31 return Err(SvgRenderError::InvalidSize { width, height });
32 }
33
34 let byte_len = (width as usize)
35 .checked_mul(height as usize)
36 .and_then(|pixels| pixels.checked_mul(4))
37 .ok_or(SvgRenderError::InvalidSize { width, height })?;
38 if byte_len > MAX_RENDER_BYTES {
39 return Err(SvgRenderError::InvalidSize { width, height });
40 }
41
42 Ok(Self { width, height })
43 }
44
45 pub(crate) fn as_i32_pair(self) -> (i32, i32) {
47 (self.width as i32, self.height as i32)
48 }
49}
50
51#[derive(Debug, Clone)]
59pub struct RenderOptions {
60 pub size: RenderSize,
62 pub clear_color: Color,
64 pub sample_count: usize,
66}
67
68impl RenderOptions {
69 pub fn new(width: u32, height: u32) -> Result<Self, SvgRenderError> {
72 Ok(Self {
73 size: RenderSize::new(width, height)?,
74 clear_color: Color::TRANSPARENT,
75 sample_count: 4,
76 })
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct JpegOptions {
85 pub quality: u32,
87 pub downsample: JpegDownsample,
89 pub alpha_option: JpegAlphaOption,
91}
92
93impl Default for JpegOptions {
94 fn default() -> Self {
95 Self {
96 quality: 90,
97 downsample: JpegDownsample::BothDirections,
98 alpha_option: JpegAlphaOption::Ignore,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum JpegDownsample {
106 BothDirections,
108 Horizontal,
110 No,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum JpegAlphaOption {
117 Ignore,
119 BlendOnBlack,
121}
122
123impl From<JpegOptions> for jpeg_encoder::Options {
124 fn from(value: JpegOptions) -> Self {
125 let downsample = match value.downsample {
126 JpegDownsample::BothDirections => jpeg_encoder::Downsample::BothDirections,
127 JpegDownsample::Horizontal => jpeg_encoder::Downsample::Horizontal,
128 JpegDownsample::No => jpeg_encoder::Downsample::No,
129 };
130 let alpha_option = match value.alpha_option {
131 JpegAlphaOption::Ignore => jpeg_encoder::AlphaOption::Ignore,
132 JpegAlphaOption::BlendOnBlack => jpeg_encoder::AlphaOption::BlendOnBlack,
133 };
134
135 Self {
136 quality: value.quality.clamp(0, 100),
137 downsample,
138 alpha_option,
139 ..jpeg_encoder::Options::default()
140 }
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq)]
148pub struct WebpOptions {
149 pub compression: WebpCompression,
151 pub quality: f32,
153}
154
155impl Default for WebpOptions {
156 fn default() -> Self {
157 Self {
158 compression: WebpCompression::Lossy,
159 quality: 90.0,
160 }
161 }
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum WebpCompression {
167 Lossy,
169 Lossless,
171}
172
173impl From<WebpOptions> for webp_encoder::Options {
174 fn from(value: WebpOptions) -> Self {
175 let compression = match value.compression {
176 WebpCompression::Lossy => webp_encoder::Compression::Lossy,
177 WebpCompression::Lossless => webp_encoder::Compression::Lossless,
178 };
179
180 Self {
181 compression,
182 quality: value.quality.clamp(0.0, 100.0),
183 }
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn rejects_zero_sized_render_targets() {
193 assert!(matches!(
194 RenderSize::new(0, 32),
195 Err(SvgRenderError::InvalidSize { .. })
196 ));
197 assert!(matches!(
198 RenderSize::new(32, 0),
199 Err(SvgRenderError::InvalidSize { .. })
200 ));
201 }
202
203 #[test]
204 fn rejects_render_targets_that_would_allocate_too_many_bytes() {
205 assert!(matches!(
206 RenderSize::new(i32::MAX as u32, i32::MAX as u32),
207 Err(SvgRenderError::InvalidSize { .. })
208 ));
209 }
210
211 #[test]
212 fn default_render_options_are_valid() {
213 let options = RenderOptions::new(64, 48).unwrap();
214
215 assert_eq!(options.size.width, 64);
216 assert_eq!(options.size.height, 48);
217 assert_eq!(options.sample_count, 4);
218 }
219}