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