Skip to main content

turbopack_image/process/
mod.rs

1pub mod svg;
2
3use std::{io::Cursor, str::FromStr};
4
5use anyhow::{Context, Result, bail};
6use async_trait::async_trait;
7use base64::{display::Base64Display, engine::general_purpose::STANDARD};
8use bincode::{Decode, Encode};
9use image::{
10    DynamicImage, GenericImageView, ImageEncoder, ImageFormat,
11    codecs::{
12        bmp::BmpEncoder,
13        ico::IcoEncoder,
14        jpeg::JpegEncoder,
15        png::{CompressionType, PngEncoder},
16        webp::WebPEncoder,
17    },
18    imageops::FilterType,
19};
20use mime::Mime;
21use turbo_rcstr::rcstr;
22use turbo_tasks::{NonLocalValue, PrettyPrintError, ResolvedVc, Vc, debug::ValueDebugFormat};
23use turbo_tasks_fs::{File, FileContent, FileSystemPath};
24use turbopack_core::{
25    issue::{Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString},
26    source::Source,
27};
28
29use self::svg::calculate;
30
31/// Small placeholder version of the image.
32#[derive(PartialEq, Eq, ValueDebugFormat, NonLocalValue, Encode, Decode)]
33pub struct BlurPlaceholder {
34    pub data_url: String,
35    pub width: u32,
36    pub height: u32,
37}
38
39impl BlurPlaceholder {
40    pub fn fallback() -> Self {
41        BlurPlaceholder {
42            data_url: "data:image/gif;base64,R0lGODlhAQABAIAAAP///\
43                       wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
44                .to_string(),
45            width: 1,
46            height: 1,
47        }
48    }
49}
50
51/// Gathered meta information about an image.
52#[allow(clippy::manual_non_exhaustive)]
53#[turbo_tasks::value]
54#[derive(Default)]
55#[non_exhaustive]
56pub struct ImageMetaData {
57    pub width: u32,
58    pub height: u32,
59    #[turbo_tasks(unsafe_ignore, debug_ignore)]
60    #[bincode(with = "turbo_bincode::mime_option")]
61    pub mime_type: Option<Mime>,
62    pub blur_placeholder: Option<BlurPlaceholder>,
63}
64
65impl ImageMetaData {
66    pub fn fallback_value(mime_type: Option<Mime>) -> Self {
67        ImageMetaData {
68            width: 100,
69            height: 100,
70            mime_type,
71            blur_placeholder: Some(BlurPlaceholder::fallback()),
72        }
73    }
74}
75
76/// Options for generating a blur placeholder.
77#[turbo_tasks::value(shared)]
78pub struct BlurPlaceholderOptions {
79    pub quality: u8,
80    pub size: u32,
81}
82
83fn extension_to_image_format(extension: &str) -> Option<ImageFormat> {
84    Some(match extension {
85        "avif" => ImageFormat::Avif,
86        "jpg" | "jpeg" => ImageFormat::Jpeg,
87        "png" => ImageFormat::Png,
88        "gif" => ImageFormat::Gif,
89        "webp" => ImageFormat::WebP,
90        "tif" | "tiff" => ImageFormat::Tiff,
91        "tga" => ImageFormat::Tga,
92        "dds" => ImageFormat::Dds,
93        "bmp" => ImageFormat::Bmp,
94        "ico" => ImageFormat::Ico,
95        "hdr" => ImageFormat::Hdr,
96        "exr" => ImageFormat::OpenExr,
97        "pbm" | "pam" | "ppm" | "pgm" => ImageFormat::Pnm,
98        "ff" | "farbfeld" => ImageFormat::Farbfeld,
99        "qoi" => ImageFormat::Qoi,
100        _ => return None,
101    })
102}
103
104fn result_to_issue<T>(source: ResolvedVc<Box<dyn Source>>, result: Result<T>) -> Option<T> {
105    match result {
106        Ok(r) => Some(r),
107        Err(err) => {
108            ImageProcessingIssue {
109                message: StyledString::Text(format!("{}", PrettyPrintError(&err)).into())
110                    .resolved_cell(),
111                issue_severity: None,
112                title: None,
113                source: IssueSource::from_source_only(source),
114            }
115            .resolved_cell()
116            .emit();
117            None
118        }
119    }
120}
121
122fn load_image(
123    path: ResolvedVc<Box<dyn Source>>,
124    bytes: &[u8],
125    extension: Option<&str>,
126) -> Option<(ImageBuffer, Option<ImageFormat>)> {
127    result_to_issue(path, load_image_internal(path, bytes, extension))
128}
129
130/// Type of raw image buffer read by reader from `load_image`.
131/// If the image could not be decoded, the raw bytes are returned.
132enum ImageBuffer {
133    Raw(Vec<u8>),
134    Decoded(image::DynamicImage),
135}
136
137fn load_image_internal(
138    image: ResolvedVc<Box<dyn Source>>,
139    bytes: &[u8],
140    extension: Option<&str>,
141) -> Result<(ImageBuffer, Option<ImageFormat>)> {
142    let reader = image::ImageReader::new(Cursor::new(&bytes));
143    let mut reader = reader
144        .with_guessed_format()
145        .context("unable to determine image format from file content")?;
146    let mut format = reader.format();
147    if format.is_none()
148        && let Some(ext) = extension
149        && let Some(new_format) = extension_to_image_format(ext)
150    {
151        format = Some(new_format);
152        reader.set_format(new_format);
153    }
154
155    // [NOTE]
156    // Workaround for missing codec supports in Turbopack,
157    // Instead of erroring out the whole build, emitting raw image bytes as-is
158    // (Not applying resize, not applying optimization or anything else)
159    // and expect a browser decodes it.
160    // This is a stop gap until we have proper encoding/decoding in majority of the
161    // platforms
162
163    #[cfg(not(feature = "avif"))]
164    if matches!(format, Some(ImageFormat::Avif)) {
165        ImageProcessingIssue {
166            source: IssueSource::from_source_only(image),
167            message: StyledString::Text(rcstr!(
168                "This version of Turbopack does not support AVIF images, will emit without \
169                 optimization or encoding"
170            ))
171            .resolved_cell(),
172            title: Some(StyledString::Text(rcstr!("AVIF image not supported")).resolved_cell()),
173            issue_severity: Some(IssueSeverity::Warning),
174        }
175        .resolved_cell()
176        .emit();
177        return Ok((ImageBuffer::Raw(bytes.to_vec()), format));
178    }
179
180    let image = reader.decode().context("unable to decode image data")?;
181    Ok((ImageBuffer::Decoded(image), format))
182}
183
184fn compute_blur_data(
185    path: ResolvedVc<Box<dyn Source>>,
186    image: image::DynamicImage,
187    format: ImageFormat,
188    options: &BlurPlaceholderOptions,
189) -> Option<BlurPlaceholder> {
190    match compute_blur_data_internal(image, format, options)
191        .context("unable to compute blur placeholder")
192    {
193        Ok(r) => Some(r),
194        Err(err) => {
195            ImageProcessingIssue {
196                source: IssueSource::from_source_only(path),
197                message: StyledString::Text(format!("{}", PrettyPrintError(&err)).into())
198                    .resolved_cell(),
199                issue_severity: None,
200                title: None,
201            }
202            .resolved_cell()
203            .emit();
204            Some(BlurPlaceholder::fallback())
205        }
206    }
207}
208
209fn encode_image(image: DynamicImage, format: ImageFormat, quality: u8) -> Result<(Vec<u8>, Mime)> {
210    let mut buf = Vec::new();
211    let (width, height) = image.dimensions();
212
213    Ok(match format {
214        ImageFormat::Png => {
215            PngEncoder::new_with_quality(
216                &mut buf,
217                CompressionType::Best,
218                image::codecs::png::FilterType::NoFilter,
219            )
220            .write_image(image.as_bytes(), width, height, image.color().into())?;
221            (buf, mime::IMAGE_PNG)
222        }
223        ImageFormat::Jpeg => {
224            JpegEncoder::new_with_quality(&mut buf, quality).write_image(
225                image.as_bytes(),
226                width,
227                height,
228                image.color().into(),
229            )?;
230            (buf, mime::IMAGE_JPEG)
231        }
232        ImageFormat::Ico => {
233            IcoEncoder::new(&mut buf).write_image(
234                image.as_bytes(),
235                width,
236                height,
237                image.color().into(),
238            )?;
239            // mime does not support typed IMAGE_X_ICO yet
240            (buf, Mime::from_str("image/x-icon")?)
241        }
242        ImageFormat::Bmp => {
243            BmpEncoder::new(&mut buf).write_image(
244                image.as_bytes(),
245                width,
246                height,
247                image.color().into(),
248            )?;
249            (buf, mime::IMAGE_BMP)
250        }
251        ImageFormat::WebP => {
252            let encoder = WebPEncoder::new_lossless(&mut buf);
253            encoder.encode(image.as_bytes(), width, height, image.color().into())?;
254
255            (buf, Mime::from_str("image/webp")?)
256        }
257        #[cfg(feature = "avif")]
258        ImageFormat::Avif => {
259            use image::codecs::avif::AvifEncoder;
260            AvifEncoder::new_with_speed_quality(&mut buf, 6, quality).write_image(
261                image.as_bytes(),
262                width,
263                height,
264                image.color().into(),
265            )?;
266            (buf, Mime::from_str("image/avif")?)
267        }
268        _ => bail!(
269            "Encoding for image format {:?} has not been compiled into the current build",
270            format
271        ),
272    })
273}
274
275fn compute_blur_data_internal(
276    image: image::DynamicImage,
277    format: ImageFormat,
278    options: &BlurPlaceholderOptions,
279) -> Result<BlurPlaceholder> {
280    let small_image = image.resize(options.size, options.size, FilterType::Triangle);
281    let width = small_image.width();
282    let height = small_image.height();
283    let (data, mime) = encode_image(small_image, format, options.quality)?;
284    let data_url = format!(
285        "data:{mime};base64,{}",
286        Base64Display::new(&data, &STANDARD)
287    );
288
289    Ok(BlurPlaceholder {
290        data_url,
291        width,
292        height,
293    })
294}
295
296fn image_format_to_mime_type(format: ImageFormat) -> Result<Option<Mime>> {
297    Ok(match format {
298        ImageFormat::Png => Some(mime::IMAGE_PNG),
299        ImageFormat::Jpeg => Some(mime::IMAGE_JPEG),
300        ImageFormat::WebP => Some(Mime::from_str("image/webp")?),
301        ImageFormat::Avif => Some(Mime::from_str("image/avif")?),
302        ImageFormat::Bmp => Some(mime::IMAGE_BMP),
303        ImageFormat::Dds => Some(Mime::from_str("image/vnd-ms.dds")?),
304        ImageFormat::Farbfeld => Some(mime::APPLICATION_OCTET_STREAM),
305        ImageFormat::Gif => Some(mime::IMAGE_GIF),
306        ImageFormat::Hdr => Some(Mime::from_str("image/vnd.radiance")?),
307        ImageFormat::Ico => Some(Mime::from_str("image/x-icon")?),
308        ImageFormat::OpenExr => Some(Mime::from_str("image/x-exr")?),
309        ImageFormat::Pnm => Some(Mime::from_str("image/x-portable-anymap")?),
310        ImageFormat::Qoi => Some(mime::APPLICATION_OCTET_STREAM),
311        ImageFormat::Tga => Some(Mime::from_str("image/x-tga")?),
312        ImageFormat::Tiff => Some(Mime::from_str("image/tiff")?),
313        _ => None,
314    })
315}
316
317/// Analyze an image and return meta information about it.
318/// Optionally computes a blur placeholder.
319#[turbo_tasks::function]
320pub async fn get_meta_data(
321    image: ResolvedVc<Box<dyn Source>>,
322    content: Vc<FileContent>,
323    blur_placeholder: Option<Vc<BlurPlaceholderOptions>>,
324) -> Result<Vc<ImageMetaData>> {
325    let FileContent::Content(content) = &*content.await? else {
326        bail!("Input image not found");
327    };
328    let bytes = content.content().to_bytes();
329    let ident = image.ident().await?;
330    let extension = ident.path.extension();
331
332    if extension == Some("svg") {
333        let content = result_to_issue(
334            image,
335            std::str::from_utf8(&bytes).context("Input image is not valid utf-8"),
336        );
337        let Some(content) = content else {
338            return Ok(ImageMetaData::fallback_value(Some(mime::IMAGE_SVG)).cell());
339        };
340        let info = result_to_issue(
341            image,
342            calculate(content).context("Failed to parse svg source code for image dimensions"),
343        );
344        let Some((width, height)) = info else {
345            return Ok(ImageMetaData::fallback_value(Some(mime::IMAGE_SVG)).cell());
346        };
347        return Ok(ImageMetaData {
348            width,
349            height,
350            mime_type: Some(mime::IMAGE_SVG),
351            blur_placeholder: None,
352        }
353        .cell());
354    }
355    let Some((image_buffer, format)) = load_image(image, &bytes, extension) else {
356        return Ok(ImageMetaData::fallback_value(None).cell());
357    };
358
359    let mime_type = if let Some(format) = format {
360        image_format_to_mime_type(format)?
361    } else {
362        None
363    };
364
365    match image_buffer {
366        ImageBuffer::Raw(..) => {
367            // We can't decode this format, but can still read the dimensions from its headers.
368            // There's no way to compute a real blur placeholder without decoding.
369            let size = result_to_issue(
370                image,
371                imagesize::blob_size(&bytes)
372                    .map_err(anyhow::Error::from)
373                    .and_then(|size| Ok((u32::try_from(size.width)?, u32::try_from(size.height)?)))
374                    .context("unable to determine image dimensions"),
375            );
376            let Some((width, height)) = size else {
377                return Ok(ImageMetaData::fallback_value(mime_type).cell());
378            };
379            Ok(ImageMetaData {
380                width,
381                height,
382                mime_type,
383                blur_placeholder: Some(BlurPlaceholder::fallback()),
384            }
385            .cell())
386        }
387        ImageBuffer::Decoded(image_data) => {
388            let (width, height) = image_data.dimensions();
389            let blur_placeholder = if let Some(blur_placeholder) = blur_placeholder {
390                if matches!(
391                    format,
392                    // list should match next/client/image.tsx
393                    Some(ImageFormat::Png)
394                        | Some(ImageFormat::Jpeg)
395                        | Some(ImageFormat::WebP)
396                        | Some(ImageFormat::Avif)
397                ) {
398                    compute_blur_data(
399                        image,
400                        image_data,
401                        format.unwrap(),
402                        &*blur_placeholder.await?,
403                    )
404                } else {
405                    None
406                }
407            } else {
408                None
409            };
410
411            Ok(ImageMetaData {
412                width,
413                height,
414                mime_type,
415                blur_placeholder,
416            }
417            .cell())
418        }
419    }
420}
421
422#[turbo_tasks::function]
423pub async fn optimize(
424    source: ResolvedVc<Box<dyn Source>>,
425    content: Vc<FileContent>,
426    max_width: u32,
427    max_height: u32,
428    quality: u8,
429) -> Result<Vc<FileContent>> {
430    let FileContent::Content(content) = &*content.await? else {
431        return Ok(FileContent::NotFound.cell());
432    };
433    let bytes = content.content().to_bytes();
434    let ident = source.ident().await?;
435    let extension = ident.path.extension();
436
437    let Some((image, format)) = load_image(source, &bytes, extension) else {
438        return Ok(FileContent::NotFound.cell());
439    };
440    match image {
441        ImageBuffer::Raw(buffer) => {
442            #[cfg(not(feature = "avif"))]
443            if matches!(format, Some(ImageFormat::Avif)) {
444                return Ok(FileContent::Content(
445                    File::from(buffer).with_content_type(Mime::from_str("image/avif")?),
446                )
447                .cell());
448            }
449
450            let mime_type = if let Some(format) = format {
451                image_format_to_mime_type(format)?
452            } else {
453                None
454            };
455
456            // Falls back to image/jpeg if the format is unknown, thouogh it is not
457            // technically correct
458            Ok(FileContent::Content(
459                File::from(buffer).with_content_type(mime_type.unwrap_or(mime::IMAGE_JPEG)),
460            )
461            .cell())
462        }
463        ImageBuffer::Decoded(image) => {
464            let (width, height) = image.dimensions();
465            let image = if width > max_width || height > max_height {
466                image.resize(max_width, max_height, FilterType::Lanczos3)
467            } else {
468                image
469            };
470
471            let format = format.unwrap_or(ImageFormat::Jpeg);
472            let (data, mime_type) = encode_image(image, format, quality)?;
473
474            Ok(FileContent::Content(File::from(data).with_content_type(mime_type)).cell())
475        }
476    }
477}
478
479#[turbo_tasks::value]
480struct ImageProcessingIssue {
481    message: ResolvedVc<StyledString>,
482    title: Option<ResolvedVc<StyledString>>,
483    issue_severity: Option<IssueSeverity>,
484    source: IssueSource,
485}
486
487#[async_trait]
488#[turbo_tasks::value_impl]
489impl Issue for ImageProcessingIssue {
490    fn severity(&self) -> IssueSeverity {
491        self.issue_severity.unwrap_or(IssueSeverity::Error)
492    }
493
494    async fn file_path(&self) -> anyhow::Result<FileSystemPath> {
495        self.source.file_path().await
496    }
497
498    fn stage(&self) -> IssueStage {
499        IssueStage::Transform
500    }
501
502    async fn title(&self) -> anyhow::Result<StyledString> {
503        Ok(match self.title {
504            Some(t) => (*t.await?).clone(),
505            None => StyledString::Text(rcstr!("Processing image failed")),
506        })
507    }
508
509    async fn description(&self) -> anyhow::Result<Option<StyledString>> {
510        Ok(Some((*self.message.await?).clone()))
511    }
512
513    fn source(&self) -> Option<IssueSource> {
514        Some(self.source)
515    }
516}