truss/codecs/mod.rs
1//! Backend codec implementations.
2
3use crate::core::{MediaType, TransformError, TransformRequest, TransformResult};
4
5/// Raster image decoding and encoding support.
6pub mod raster;
7
8/// SVG sanitization and rasterization support.
9#[cfg(feature = "svg")]
10pub mod svg;
11
12/// Dispatches a transform request to the appropriate codec based on the input media type.
13///
14/// This is the entry point for every image transformation, and the only one: it routes an
15/// SVG input to the SVG codec and everything else to the raster codec, and rejects an
16/// unsupported conversion such as raster-to-SVG output with a clear error. The two codecs
17/// are not exported, because choosing between them is what this function does and naming
18/// the wrong one for an input is only a way to get an error.
19///
20/// GIF is an input-only format here. A single-frame GIF decodes and transforms like any
21/// other raster input; an animated one is refused rather than silently reduced to its
22/// first frame, because a caller that sends an animation and receives a still image back
23/// with exit code 0 has no way to notice the animation was discarded. `truss inspect`
24/// still reads animated GIFs and reports `isAnimated`, so a caller can branch on that
25/// before converting.
26///
27/// # Errors
28///
29/// Returns [`TransformError::UnsupportedOutputMediaType`] if a raster input requests
30/// SVG or GIF output, [`TransformError::UnsupportedInputMediaType`] for an animated GIF
31/// input, [`TransformError::CapabilityMissing`] if an SVG input is provided but the `svg`
32/// feature is not enabled, or any error propagated from the underlying codec.
33/// # Examples
34///
35/// ```
36/// use image::codecs::png::PngEncoder;
37/// use image::{ColorType, ImageEncoder, Rgba, RgbaImage};
38/// use truss::{sniff_artifact, transform, MediaType, RawArtifact, TransformOptions, TransformRequest};
39///
40/// let image = RgbaImage::from_pixel(2, 2, Rgba([10, 20, 30, 255]));
41/// let mut bytes = Vec::new();
42/// PngEncoder::new(&mut bytes)
43/// .write_image(&image, 2, 2, ColorType::Rgba8.into())
44/// .unwrap();
45///
46/// let input = sniff_artifact(RawArtifact::new(bytes, Some(MediaType::Png))).unwrap();
47/// let mut options = TransformOptions::default();
48/// options.format = Some(MediaType::Jpeg);
49/// let output = transform(TransformRequest::new(
50/// input,
51/// options,
52/// ))
53/// .unwrap();
54///
55/// assert_eq!(output.artifact.media_type, MediaType::Jpeg);
56/// assert_eq!(output.artifact.metadata.width, Some(2));
57/// assert_eq!(output.artifact.metadata.height, Some(2));
58/// ```
59///
60/// ```
61/// use image::codecs::png::PngEncoder;
62/// use image::{ColorType, ImageEncoder, Rgba, RgbaImage};
63/// use truss::{sniff_artifact, transform, MediaType, RawArtifact, TransformOptions, TransformRequest};
64///
65/// let image = RgbaImage::from_pixel(2, 2, Rgba([10, 20, 30, 255]));
66/// let mut bytes = Vec::new();
67/// PngEncoder::new(&mut bytes)
68/// .write_image(&image, 2, 2, ColorType::Rgba8.into())
69/// .unwrap();
70///
71/// let input = sniff_artifact(RawArtifact::new(bytes, Some(MediaType::Png))).unwrap();
72/// let mut options = TransformOptions::default();
73/// options.format = Some(MediaType::Avif);
74/// options.quality = Some(70);
75/// let output = transform(TransformRequest::new(
76/// input,
77/// options,
78/// ))
79/// .unwrap();
80/// let sniffed = sniff_artifact(RawArtifact::new(output.artifact.bytes.clone(), None)).unwrap();
81///
82/// assert_eq!(output.artifact.media_type, MediaType::Avif);
83/// assert_eq!(sniffed.media_type, MediaType::Avif);
84/// ```
85///
86/// ```
87/// use image::codecs::jpeg::JpegDecoder;
88/// use image::codecs::jpeg::JpegEncoder;
89/// use image::metadata::Orientation;
90/// use image::{ColorType, ImageDecoder, ImageEncoder, Rgb, RgbImage};
91/// use std::io::Cursor;
92/// use truss::{sniff_artifact, transform, MediaType, RawArtifact, TransformOptions, TransformRequest};
93///
94/// let image = RgbImage::from_pixel(2, 1, Rgb([10, 20, 30]));
95/// let exif = vec![
96/// 0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00,
97/// 0x01, 0x00, 0x12, 0x01, 0x03, 0x00, 0x01, 0x00,
98/// 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
99/// 0x00, 0x00,
100/// ];
101/// let mut bytes = Vec::new();
102/// let mut encoder = JpegEncoder::new_with_quality(&mut bytes, 80);
103/// encoder.set_exif_metadata(exif).unwrap();
104/// encoder
105/// .write_image(&image, 2, 1, ColorType::Rgb8.into())
106/// .unwrap();
107///
108/// let input = sniff_artifact(RawArtifact::new(bytes, Some(MediaType::Jpeg))).unwrap();
109/// let mut options = TransformOptions::default();
110/// options.format = Some(MediaType::Jpeg);
111/// options.strip_metadata = false;
112/// options.preserve_exif = true;
113/// let output = transform(TransformRequest::new(
114/// input,
115/// options,
116/// ))
117/// .unwrap();
118///
119/// let mut decoder = JpegDecoder::new(Cursor::new(&output.artifact.bytes)).unwrap();
120/// let exif = decoder.exif_metadata().unwrap().unwrap();
121///
122/// assert_eq!(output.artifact.metadata.width, Some(1));
123/// assert_eq!(output.artifact.metadata.height, Some(2));
124/// assert_eq!(Orientation::from_exif_chunk(&exif), Some(Orientation::NoTransforms));
125/// ```
126///
127/// ```
128/// use image::codecs::jpeg::JpegDecoder;
129/// use image::codecs::jpeg::JpegEncoder;
130/// use image::{ColorType, ImageDecoder, ImageEncoder, Rgb, RgbImage};
131/// use std::io::Cursor;
132/// use truss::{sniff_artifact, transform, MediaType, RawArtifact, TransformOptions, TransformRequest};
133///
134/// let image = RgbImage::from_pixel(2, 1, Rgb([10, 20, 30]));
135/// let mut bytes = Vec::new();
136/// let mut encoder = JpegEncoder::new_with_quality(&mut bytes, 80);
137/// encoder.set_icc_profile(b"demo-icc-profile".to_vec()).unwrap();
138/// encoder
139/// .write_image(&image, 2, 1, ColorType::Rgb8.into())
140/// .unwrap();
141///
142/// let input = sniff_artifact(RawArtifact::new(bytes, Some(MediaType::Jpeg))).unwrap();
143/// let mut options = TransformOptions::default();
144/// options.format = Some(MediaType::Jpeg);
145/// options.strip_metadata = false;
146/// let output = transform(TransformRequest::new(
147/// input,
148/// options,
149/// ))
150/// .unwrap();
151///
152/// let mut decoder = JpegDecoder::new(Cursor::new(&output.artifact.bytes)).unwrap();
153/// assert_eq!(decoder.icc_profile().unwrap(), Some(b"demo-icc-profile".to_vec()));
154/// ```
155///
156/// ```
157/// use truss::{sniff_artifact, RawArtifact, TransformRequest, TransformOptions, MediaType};
158/// use truss::transform;
159///
160/// let svg_bytes = b"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"10\" height=\"10\"><rect width=\"10\" height=\"10\" fill=\"red\"/></svg>";
161/// let input = sniff_artifact(RawArtifact::new(svg_bytes.to_vec(), None)).unwrap();
162/// let mut options = TransformOptions::default();
163/// options.format = Some(MediaType::Png);
164/// options.width = Some(10);
165/// options.height = Some(10);
166/// let result = transform(TransformRequest::new(input, options)).unwrap();
167/// assert_eq!(result.artifact.media_type, MediaType::Png);
168/// ```
169#[must_use = "this function returns the transform result without side effects"]
170pub fn transform(request: TransformRequest) -> Result<TransformResult, TransformError> {
171 if request.input.media_type == MediaType::Svg {
172 #[cfg(feature = "svg")]
173 {
174 return svg::transform_svg(request);
175 }
176 #[cfg(not(feature = "svg"))]
177 {
178 let _ = request;
179 return Err(TransformError::CapabilityMissing(
180 "SVG processing is not enabled in this build".to_string(),
181 ));
182 }
183 }
184
185 if request.options.format == Some(MediaType::Svg) {
186 return Err(TransformError::UnsupportedOutputMediaType(MediaType::Svg));
187 }
188
189 if request.options.format == Some(MediaType::Gif) {
190 return Err(TransformError::UnsupportedOutputMediaType(MediaType::Gif));
191 }
192
193 // The rule is about frames, not about one container. Gating it on GIF let an animated
194 // WebP, an APNG, and an animated AVIF through, where the decoder kept the first frame
195 // and the caller was told nothing.
196 if request.input.metadata.frame_count > 1 {
197 return Err(TransformError::UnsupportedInputMediaType(format!(
198 "animated {} is not supported ({} frames); truss transforms single-frame images only",
199 request.input.media_type.as_name(),
200 request.input.metadata.frame_count
201 )));
202 }
203
204 raster::transform_raster(request)
205}
206
207#[cfg(test)]
208mod tests {
209 use super::transform;
210 use crate::core::{
211 Artifact, ArtifactMetadata, MediaType, TransformError, TransformOptions, TransformRequest,
212 };
213
214 fn animated_artifact(media_type: MediaType, signature: &[u8], frame_count: u32) -> Artifact {
215 Artifact::new(
216 signature.to_vec(),
217 media_type,
218 ArtifactMetadata {
219 width: Some(4),
220 height: Some(4),
221 frame_count,
222 duration: None,
223 has_alpha: Some(false),
224 orientation: None,
225 },
226 )
227 }
228
229 /// The rule the GIF message states is about frames, not about GIF. A picture that has
230 /// more than one of them is refused whatever container it arrived in, or the container
231 /// decides whether the caller is told their animation was reduced to a still.
232 #[test]
233 fn a_multi_frame_input_is_refused_whatever_its_container() {
234 let cases: &[(MediaType, &[u8])] = &[
235 (MediaType::Gif, b"GIF89a"),
236 (MediaType::Png, b"\x89PNG\r\n\x1a\n"),
237 (MediaType::Webp, b"RIFF\0\0\0\0WEBP"),
238 (MediaType::Avif, b"\0\0\0\x18ftypavis"),
239 ];
240
241 for &(media_type, signature) in cases {
242 let error = transform(TransformRequest::new(
243 animated_artifact(media_type, signature, 4),
244 TransformOptions {
245 format: Some(MediaType::Png),
246 ..TransformOptions::default()
247 },
248 ))
249 .expect_err("a multi-frame input should be refused, not reduced to one frame");
250
251 assert!(
252 matches!(error, TransformError::UnsupportedInputMediaType(ref message)
253 if message.contains("4 frames")),
254 "{media_type:?} was not refused for having frames: {error}"
255 );
256 }
257 }
258
259 fn gif_artifact(frame_count: u32) -> Artifact {
260 // The dispatcher decides on the media type and metadata alone, before any decode,
261 // so the bytes only need the signature that put this artifact on the GIF path.
262 Artifact::new(
263 b"GIF89a".to_vec(),
264 MediaType::Gif,
265 ArtifactMetadata {
266 width: Some(4),
267 height: Some(4),
268 frame_count,
269 duration: None,
270 has_alpha: Some(false),
271 orientation: None,
272 },
273 )
274 }
275
276 #[test]
277 fn transform_rejects_an_animated_gif_input() {
278 let error = transform(TransformRequest::new(
279 gif_artifact(12),
280 TransformOptions {
281 format: Some(MediaType::Png),
282 ..TransformOptions::default()
283 },
284 ))
285 .expect_err("an animated gif should be refused, not reduced to one frame");
286
287 match error {
288 TransformError::UnsupportedInputMediaType(message) => {
289 assert!(
290 message.contains("animated gif") && message.contains("12 frames"),
291 "the error should name the format and the frame count, got: {message}"
292 );
293 }
294 other => panic!("expected UnsupportedInputMediaType, got: {other}"),
295 }
296 }
297
298 #[test]
299 fn transform_rejects_gif_output() {
300 let error = transform(TransformRequest::new(
301 Artifact::new(
302 b"\x89PNG\r\n\x1a\n".to_vec(),
303 MediaType::Png,
304 ArtifactMetadata::default(),
305 ),
306 TransformOptions {
307 format: Some(MediaType::Gif),
308 ..TransformOptions::default()
309 },
310 ))
311 .expect_err("gif output should be refused");
312
313 assert!(
314 matches!(
315 error,
316 TransformError::UnsupportedOutputMediaType(MediaType::Gif)
317 ),
318 "expected UnsupportedOutputMediaType(Gif), got: {error}"
319 );
320 }
321
322 #[test]
323 fn transform_accepts_a_single_frame_gif_past_the_animation_guard() {
324 // The guard must not fire for a still image. The bytes here are not a decodable
325 // GIF, so the request is expected to fail later, in the decoder, not at dispatch.
326 let error = transform(TransformRequest::new(
327 gif_artifact(1),
328 TransformOptions {
329 format: Some(MediaType::Png),
330 ..TransformOptions::default()
331 },
332 ))
333 .expect_err("truncated gif bytes cannot decode");
334
335 assert!(
336 matches!(error, TransformError::DecodeFailed(_)),
337 "a single-frame gif should reach the decoder, got: {error}"
338 );
339 }
340}