Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(turbopack): emit raw image bytes for unsupported codec #5967

Merged
merged 1 commit into from
Sep 18, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 150 additions & 52 deletions crates/turbopack-image/src/process/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use turbo_tasks_fs::{File, FileContent, FileSystemPath};
use turbopack_core::{
error::PrettyPrintError,
ident::AssetIdent,
issue::{Issue, IssueExt},
issue::{Issue, IssueExt, IssueSeverity},
};

use self::svg::calculate;
Expand Down Expand Up @@ -108,6 +108,8 @@ fn result_to_issue<T>(ident: Vc<AssetIdent>, result: Result<T>) -> Option<T> {
ImageProcessingIssue {
path: ident.path(),
message: Vc::cell(format!("{}", PrettyPrintError(&err))),
issue_severity: None,
title: None,
}
.cell()
.emit();
Expand All @@ -120,14 +122,22 @@ fn load_image(
ident: Vc<AssetIdent>,
bytes: &[u8],
extension: Option<&str>,
) -> Option<(image::DynamicImage, Option<ImageFormat>)> {
result_to_issue(ident, load_image_internal(bytes, extension))
) -> Option<(ImageBuffer, Option<ImageFormat>)> {
result_to_issue(ident, load_image_internal(ident, bytes, extension))
}

/// Type of raw image buffer read by reader from `load_image`.
/// If the image could not be decoded, the raw bytes are returned.
enum ImageBuffer {
Raw(Vec<u8>),
Decoded(image::DynamicImage),
}

fn load_image_internal(
ident: Vc<AssetIdent>,
bytes: &[u8],
extension: Option<&str>,
) -> Result<(image::DynamicImage, Option<ImageFormat>)> {
) -> Result<(ImageBuffer, Option<ImageFormat>)> {
let reader = image::io::Reader::new(Cursor::new(&bytes));
let mut reader = reader
.with_guessed_format()
Expand All @@ -141,8 +151,51 @@ fn load_image_internal(
}
}
}

// [NOTE]
// Workaround for missing codec supports in Turbopack,
// Instead of erroring out the whole build, emitting raw image bytes as-is
// (Not applying resize, not applying optimization or anything else)
// and expect a browser decodes it.
// This is a stop gap until we have proper encoding/decoding in majority of the
// platforms

#[cfg(not(feature = "avif"))]
if matches!(format, Some(ImageFormat::Avif)) {
ImageProcessingIssue {
path: ident.path(),
message: Vc::cell(
"This version of Turbopack does not support AVIF images, will emit without \
optimization or encoding"
.to_string(),
),
title: Some(Vc::cell("AVIF image not supported".to_string())),
issue_severity: Some(IssueSeverity::Warning.into()),
}
.cell()
.emit();
return Ok((ImageBuffer::Raw(bytes.to_vec()), format));
}

#[cfg(not(feature = "webp"))]
if matches!(format, Some(ImageFormat::WebP)) {
ImageProcessingIssue {
path: ident.path(),
message: Vc::cell(
"This version of Turbopack does not support WEBP images, will emit without \
optimization or encoding"
.to_string(),
),
title: Some(Vc::cell("WEBP image not supported".to_string())),
issue_severity: Some(IssueSeverity::Warning.into()),
}
.cell()
.emit();
return Ok((ImageBuffer::Raw(bytes.to_vec()), format));
}

let image = reader.decode().context("unable to decode image data")?;
Ok((image, format))
Ok((ImageBuffer::Decoded(image), format))
}

fn compute_blur_data(
Expand All @@ -159,6 +212,8 @@ fn compute_blur_data(
ImageProcessingIssue {
path: ident.path(),
message: Vc::cell(format!("{}", PrettyPrintError(&err))),
issue_severity: None,
title: None,
}
.cell()
.emit();
Expand Down Expand Up @@ -321,36 +376,42 @@ pub async fn get_meta_data(
let Some((image, format)) = load_image(ident, &bytes, extension) else {
return Ok(ImageMetaData::fallback_value(None).cell());
};
let (width, height) = image.dimensions();
let blur_placeholder = if let Some(blur_placeholder) = blur_placeholder {
if matches!(
format,
// list should match next/client/image.tsx
Some(ImageFormat::Png)
| Some(ImageFormat::Jpeg)
| Some(ImageFormat::WebP)
| Some(ImageFormat::Avif)
) {
compute_blur_data(ident, image, format.unwrap(), &*blur_placeholder.await?)
} else {
None
}
} else {
None
};

Ok(ImageMetaData {
width,
height,
mime_type: if let Some(format) = format {
image_format_to_mime_type(format)?
} else {
None
},
blur_placeholder,
placeholder_for_future_extensions: (),
match image {
ImageBuffer::Raw(..) => Ok(ImageMetaData::fallback_value(None).cell()),
ImageBuffer::Decoded(image) => {
let (width, height) = image.dimensions();
let blur_placeholder = if let Some(blur_placeholder) = blur_placeholder {
if matches!(
format,
// list should match next/client/image.tsx
Some(ImageFormat::Png)
| Some(ImageFormat::Jpeg)
| Some(ImageFormat::WebP)
| Some(ImageFormat::Avif)
) {
compute_blur_data(ident, image, format.unwrap(), &*blur_placeholder.await?)
} else {
None
}
} else {
None
};

Ok(ImageMetaData {
width,
height,
mime_type: if let Some(format) = format {
image_format_to_mime_type(format)?
} else {
None
},
blur_placeholder,
placeholder_for_future_extensions: (),
}
.cell())
}
}
.cell())
}

#[turbo_tasks::function]
Expand All @@ -365,38 +426,73 @@ pub async fn optimize(
return Ok(FileContent::NotFound.cell());
};
let bytes = content.content().to_bytes()?;
let Some((image, mut format)) = load_image(ident, &bytes, ident.path().await?.extension_ref())

let Some((image, format)) = load_image(ident, &bytes, ident.path().await?.extension_ref())
else {
return Ok(FileContent::NotFound.cell());
};
let (width, height) = image.dimensions();
let image = if width > max_width || height > max_height {
image.resize(max_width, max_height, FilterType::Lanczos3)
} else {
image
};
#[cfg(not(feature = "avif"))]
if matches!(format, Some(ImageFormat::Avif)) {
format = Some(ImageFormat::Jpeg);
}
#[cfg(not(feature = "webp"))]
if matches!(format, Some(ImageFormat::WebP)) {
format = Some(ImageFormat::Jpeg);
}
let format = format.unwrap_or(ImageFormat::Jpeg);
let (data, mime_type) = encode_image(image, format, quality)?;
match image {
ImageBuffer::Raw(buffer) => {
#[cfg(not(feature = "avif"))]
if matches!(format, Some(ImageFormat::Avif)) {
return Ok(FileContent::Content(
File::from(buffer).with_content_type(Mime::from_str("image/avif")?),
)
.cell());
}

Ok(FileContent::Content(File::from(data).with_content_type(mime_type)).cell())
#[cfg(not(feature = "webp"))]
if matches!(format, Some(ImageFormat::WebP)) {
return Ok(FileContent::Content(
File::from(buffer).with_content_type(Mime::from_str("image/webp")?),
)
.cell());
}

let mime_type = if let Some(format) = format {
image_format_to_mime_type(format)?
} else {
None
};

// Falls back to image/jpeg if the format is unknown, thouogh it is not
// technically correct
Ok(FileContent::Content(
File::from(buffer).with_content_type(mime_type.unwrap_or(mime::IMAGE_JPEG)),
)
.cell())
}
ImageBuffer::Decoded(image) => {
let (width, height) = image.dimensions();
let image = if width > max_width || height > max_height {
image.resize(max_width, max_height, FilterType::Lanczos3)
} else {
image
};

let format = format.unwrap_or(ImageFormat::Jpeg);
let (data, mime_type) = encode_image(image, format, quality)?;

Ok(FileContent::Content(File::from(data).with_content_type(mime_type)).cell())
}
}
}

#[turbo_tasks::value]
struct ImageProcessingIssue {
path: Vc<FileSystemPath>,
message: Vc<String>,
title: Option<Vc<String>>,
issue_severity: Option<Vc<IssueSeverity>>,
}

#[turbo_tasks::value_impl]
impl Issue for ImageProcessingIssue {
#[turbo_tasks::function]
fn severity(&self) -> Vc<IssueSeverity> {
self.issue_severity.unwrap_or(IssueSeverity::Error.into())
}

#[turbo_tasks::function]
fn file_path(&self) -> Vc<FileSystemPath> {
self.path
Expand All @@ -407,8 +503,10 @@ impl Issue for ImageProcessingIssue {
}
#[turbo_tasks::function]
fn title(&self) -> Vc<String> {
Vc::cell("Processing image failed".to_string())
self.title
.unwrap_or(Vc::cell("Processing image failed".to_string()))
}

#[turbo_tasks::function]
fn description(&self) -> Vc<String> {
self.message
Expand Down
Loading