Skip to content

Commit 917ade8

Browse files
authored
feat(turbopack): emit raw image bytes for unsupported codec (vercel/turborepo#5967)
### Description Turbopack fails & errors when the given image import is non decode / encodeable. This is a large papercut to run some pages as single image import blockes entire page rendering. Enabling those codec support (avif, webp) is unforunately not trivial to achieve across all of the platforms. This PR flips the approach, instead of bailing out if certain compile time feature is not enabled (webp/avif), emits raw bytes to the image directly. Browser may / may not support render, also this disables optimizations but at least can render remaining pages. It is stopgap still; the eventual goal is enable full codec support. Closes WEB-1590
1 parent bc49efa commit 917ade8

1 file changed

Lines changed: 150 additions & 52 deletions

File tree

  • crates/turbopack-image/src/process

crates/turbopack-image/src/process/mod.rs

Lines changed: 150 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use turbo_tasks_fs::{File, FileContent, FileSystemPath};
2222
use turbopack_core::{
2323
error::PrettyPrintError,
2424
ident::AssetIdent,
25-
issue::{Issue, IssueExt},
25+
issue::{Issue, IssueExt, IssueSeverity},
2626
};
2727

2828
use self::svg::calculate;
@@ -108,6 +108,8 @@ fn result_to_issue<T>(ident: Vc<AssetIdent>, result: Result<T>) -> Option<T> {
108108
ImageProcessingIssue {
109109
path: ident.path(),
110110
message: Vc::cell(format!("{}", PrettyPrintError(&err))),
111+
issue_severity: None,
112+
title: None,
111113
}
112114
.cell()
113115
.emit();
@@ -120,14 +122,22 @@ fn load_image(
120122
ident: Vc<AssetIdent>,
121123
bytes: &[u8],
122124
extension: Option<&str>,
123-
) -> Option<(image::DynamicImage, Option<ImageFormat>)> {
124-
result_to_issue(ident, load_image_internal(bytes, extension))
125+
) -> Option<(ImageBuffer, Option<ImageFormat>)> {
126+
result_to_issue(ident, load_image_internal(ident, bytes, extension))
127+
}
128+
129+
/// Type of raw image buffer read by reader from `load_image`.
130+
/// If the image could not be decoded, the raw bytes are returned.
131+
enum ImageBuffer {
132+
Raw(Vec<u8>),
133+
Decoded(image::DynamicImage),
125134
}
126135

127136
fn load_image_internal(
137+
ident: Vc<AssetIdent>,
128138
bytes: &[u8],
129139
extension: Option<&str>,
130-
) -> Result<(image::DynamicImage, Option<ImageFormat>)> {
140+
) -> Result<(ImageBuffer, Option<ImageFormat>)> {
131141
let reader = image::io::Reader::new(Cursor::new(&bytes));
132142
let mut reader = reader
133143
.with_guessed_format()
@@ -141,8 +151,51 @@ fn load_image_internal(
141151
}
142152
}
143153
}
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+
path: ident.path(),
167+
message: Vc::cell(
168+
"This version of Turbopack does not support AVIF images, will emit without \
169+
optimization or encoding"
170+
.to_string(),
171+
),
172+
title: Some(Vc::cell("AVIF image not supported".to_string())),
173+
issue_severity: Some(IssueSeverity::Warning.into()),
174+
}
175+
.cell()
176+
.emit();
177+
return Ok((ImageBuffer::Raw(bytes.to_vec()), format));
178+
}
179+
180+
#[cfg(not(feature = "webp"))]
181+
if matches!(format, Some(ImageFormat::WebP)) {
182+
ImageProcessingIssue {
183+
path: ident.path(),
184+
message: Vc::cell(
185+
"This version of Turbopack does not support WEBP images, will emit without \
186+
optimization or encoding"
187+
.to_string(),
188+
),
189+
title: Some(Vc::cell("WEBP image not supported".to_string())),
190+
issue_severity: Some(IssueSeverity::Warning.into()),
191+
}
192+
.cell()
193+
.emit();
194+
return Ok((ImageBuffer::Raw(bytes.to_vec()), format));
195+
}
196+
144197
let image = reader.decode().context("unable to decode image data")?;
145-
Ok((image, format))
198+
Ok((ImageBuffer::Decoded(image), format))
146199
}
147200

148201
fn compute_blur_data(
@@ -159,6 +212,8 @@ fn compute_blur_data(
159212
ImageProcessingIssue {
160213
path: ident.path(),
161214
message: Vc::cell(format!("{}", PrettyPrintError(&err))),
215+
issue_severity: None,
216+
title: None,
162217
}
163218
.cell()
164219
.emit();
@@ -321,36 +376,42 @@ pub async fn get_meta_data(
321376
let Some((image, format)) = load_image(ident, &bytes, extension) else {
322377
return Ok(ImageMetaData::fallback_value(None).cell());
323378
};
324-
let (width, height) = image.dimensions();
325-
let blur_placeholder = if let Some(blur_placeholder) = blur_placeholder {
326-
if matches!(
327-
format,
328-
// list should match next/client/image.tsx
329-
Some(ImageFormat::Png)
330-
| Some(ImageFormat::Jpeg)
331-
| Some(ImageFormat::WebP)
332-
| Some(ImageFormat::Avif)
333-
) {
334-
compute_blur_data(ident, image, format.unwrap(), &*blur_placeholder.await?)
335-
} else {
336-
None
337-
}
338-
} else {
339-
None
340-
};
341379

342-
Ok(ImageMetaData {
343-
width,
344-
height,
345-
mime_type: if let Some(format) = format {
346-
image_format_to_mime_type(format)?
347-
} else {
348-
None
349-
},
350-
blur_placeholder,
351-
placeholder_for_future_extensions: (),
380+
match image {
381+
ImageBuffer::Raw(..) => Ok(ImageMetaData::fallback_value(None).cell()),
382+
ImageBuffer::Decoded(image) => {
383+
let (width, height) = image.dimensions();
384+
let blur_placeholder = if let Some(blur_placeholder) = blur_placeholder {
385+
if matches!(
386+
format,
387+
// list should match next/client/image.tsx
388+
Some(ImageFormat::Png)
389+
| Some(ImageFormat::Jpeg)
390+
| Some(ImageFormat::WebP)
391+
| Some(ImageFormat::Avif)
392+
) {
393+
compute_blur_data(ident, image, format.unwrap(), &*blur_placeholder.await?)
394+
} else {
395+
None
396+
}
397+
} else {
398+
None
399+
};
400+
401+
Ok(ImageMetaData {
402+
width,
403+
height,
404+
mime_type: if let Some(format) = format {
405+
image_format_to_mime_type(format)?
406+
} else {
407+
None
408+
},
409+
blur_placeholder,
410+
placeholder_for_future_extensions: (),
411+
}
412+
.cell())
413+
}
352414
}
353-
.cell())
354415
}
355416

356417
#[turbo_tasks::function]
@@ -365,38 +426,73 @@ pub async fn optimize(
365426
return Ok(FileContent::NotFound.cell());
366427
};
367428
let bytes = content.content().to_bytes()?;
368-
let Some((image, mut format)) = load_image(ident, &bytes, ident.path().await?.extension_ref())
429+
430+
let Some((image, format)) = load_image(ident, &bytes, ident.path().await?.extension_ref())
369431
else {
370432
return Ok(FileContent::NotFound.cell());
371433
};
372-
let (width, height) = image.dimensions();
373-
let image = if width > max_width || height > max_height {
374-
image.resize(max_width, max_height, FilterType::Lanczos3)
375-
} else {
376-
image
377-
};
378-
#[cfg(not(feature = "avif"))]
379-
if matches!(format, Some(ImageFormat::Avif)) {
380-
format = Some(ImageFormat::Jpeg);
381-
}
382-
#[cfg(not(feature = "webp"))]
383-
if matches!(format, Some(ImageFormat::WebP)) {
384-
format = Some(ImageFormat::Jpeg);
385-
}
386-
let format = format.unwrap_or(ImageFormat::Jpeg);
387-
let (data, mime_type) = encode_image(image, format, quality)?;
434+
match image {
435+
ImageBuffer::Raw(buffer) => {
436+
#[cfg(not(feature = "avif"))]
437+
if matches!(format, Some(ImageFormat::Avif)) {
438+
return Ok(FileContent::Content(
439+
File::from(buffer).with_content_type(Mime::from_str("image/avif")?),
440+
)
441+
.cell());
442+
}
388443

389-
Ok(FileContent::Content(File::from(data).with_content_type(mime_type)).cell())
444+
#[cfg(not(feature = "webp"))]
445+
if matches!(format, Some(ImageFormat::WebP)) {
446+
return Ok(FileContent::Content(
447+
File::from(buffer).with_content_type(Mime::from_str("image/webp")?),
448+
)
449+
.cell());
450+
}
451+
452+
let mime_type = if let Some(format) = format {
453+
image_format_to_mime_type(format)?
454+
} else {
455+
None
456+
};
457+
458+
// Falls back to image/jpeg if the format is unknown, thouogh it is not
459+
// technically correct
460+
Ok(FileContent::Content(
461+
File::from(buffer).with_content_type(mime_type.unwrap_or(mime::IMAGE_JPEG)),
462+
)
463+
.cell())
464+
}
465+
ImageBuffer::Decoded(image) => {
466+
let (width, height) = image.dimensions();
467+
let image = if width > max_width || height > max_height {
468+
image.resize(max_width, max_height, FilterType::Lanczos3)
469+
} else {
470+
image
471+
};
472+
473+
let format = format.unwrap_or(ImageFormat::Jpeg);
474+
let (data, mime_type) = encode_image(image, format, quality)?;
475+
476+
Ok(FileContent::Content(File::from(data).with_content_type(mime_type)).cell())
477+
}
478+
}
390479
}
391480

392481
#[turbo_tasks::value]
393482
struct ImageProcessingIssue {
394483
path: Vc<FileSystemPath>,
395484
message: Vc<String>,
485+
title: Option<Vc<String>>,
486+
issue_severity: Option<Vc<IssueSeverity>>,
396487
}
397488

398489
#[turbo_tasks::value_impl]
399490
impl Issue for ImageProcessingIssue {
491+
#[turbo_tasks::function]
492+
fn severity(&self) -> Vc<IssueSeverity> {
493+
self.issue_severity.unwrap_or(IssueSeverity::Error.into())
494+
}
495+
400496
#[turbo_tasks::function]
401497
fn file_path(&self) -> Vc<FileSystemPath> {
402498
self.path
@@ -407,8 +503,10 @@ impl Issue for ImageProcessingIssue {
407503
}
408504
#[turbo_tasks::function]
409505
fn title(&self) -> Vc<String> {
410-
Vc::cell("Processing image failed".to_string())
506+
self.title
507+
.unwrap_or(Vc::cell("Processing image failed".to_string()))
411508
}
509+
412510
#[turbo_tasks::function]
413511
fn description(&self) -> Vc<String> {
414512
self.message

0 commit comments

Comments
 (0)