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