Skip to content

Commit

Permalink
add turbopack-image (vercel/turborepo#4621)
Browse files Browse the repository at this point in the history
### Description

allows to have custom plugins for module types

add turbopack-image crate which adds some image processing and blur
placholder generation

next.js PR: #48531
  • Loading branch information
sokra authored Apr 19, 2023
1 parent 3e0a456 commit bb6f434
Show file tree
Hide file tree
Showing 12 changed files with 330 additions and 5 deletions.
2 changes: 2 additions & 0 deletions crates/turbo-binding/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ __turbopack_dev_dynamic_embed_contents = [
__turbopack_dev_server = ["__turbopack", "turbopack-dev-server"]
__turbopack_ecmascript = ["__turbopack", "turbopack-ecmascript"]
__turbopack_env = ["__turbopack", "turbopack-env"]
__turbopack_image = ["__turbopack", "turbopack-image"]
__turbopack_json = ["__turbopack", "turbopack-json"]
__turbopack_mdx = ["__turbopack", "turbopack-mdx"]
__turbopack_node = ["__turbopack", "turbopack-node"]
Expand Down Expand Up @@ -183,6 +184,7 @@ turbopack-dev = { optional = true, workspace = true }
turbopack-dev-server = { optional = true, workspace = true }
turbopack-ecmascript = { optional = true, workspace = true }
turbopack-env = { optional = true, workspace = true }
turbopack-image = { optional = true, workspace = true }
turbopack-json = { optional = true, workspace = true }
turbopack-mdx = { optional = true, workspace = true }
turbopack-node = { optional = true, workspace = true }
Expand Down
4 changes: 3 additions & 1 deletion crates/turbo-binding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,16 @@ pub mod turbopack {
pub use turbopack_ecmascript as ecmascript;
#[cfg(feature = "__turbopack_env")]
pub use turbopack_env as env;
#[cfg(feature = "__turbopack_image")]
pub use turbopack_image as image;
#[cfg(feature = "__turbopack_json")]
pub use turbopack_json as json;
#[cfg(feature = "__turbopack_mdx")]
pub use turbopack_mdx as mdx;
#[cfg(feature = "__turbopack_node")]
pub use turbopack_node as node;
#[cfg(feature = "__turbopack_static")]
pub use turbopack_static;
pub use turbopack_static as r#static;
#[cfg(feature = "__turbopack_swc_utils")]
pub use turbopack_swc_utils as swc_utils;
#[cfg(feature = "__turbopack_test_utils")]
Expand Down
1 change: 1 addition & 0 deletions crates/turbopack-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod error;
pub mod ident;
pub mod introspect;
pub mod issue;
pub mod plugin;
pub mod reference;
pub mod reference_type;
pub mod resolve;
Expand Down
11 changes: 11 additions & 0 deletions crates/turbopack-core/src/plugin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use crate::{asset::AssetVc, context::AssetContextVc, resolve::ModulePartVc};

#[turbo_tasks::value_trait]
pub trait CustomModuleType {
fn create_module(
&self,
source: AssetVc,
context: AssetContextVc,
part: Option<ModulePartVc>,
) -> AssetVc;
}
5 changes: 5 additions & 0 deletions crates/turbopack-ecmascript/src/transform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);

#[turbo_tasks::value_impl]
impl EcmascriptInputTransformsVc {
#[turbo_tasks::function]
pub fn empty() -> Self {
EcmascriptInputTransformsVc::cell(Vec::new())
}

#[turbo_tasks::function]
pub async fn extend(self, other: EcmascriptInputTransformsVc) -> Result<Self> {
let mut transforms = self.await?.clone_value();
Expand Down
33 changes: 33 additions & 0 deletions crates/turbopack-image/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[package]
name = "turbopack-image"
version = "0.1.0"
description = "TBD"
license = "MPL-2.0"
edition = "2021"
autobenches = false

[lib]
bench = false

[features]
avif = ["image/avif-decoder", "image/avif-encoder"]

[dependencies]
anyhow = { workspace = true }
base64 = "0.21.0"
image = { workspace = true, default-features = false, features = [
"webp",
"png",
"jpeg",
"webp-encoder",
] }
indexmap = { workspace = true }
mime = { workspace = true }
serde = { workspace = true }
serde_with = { workspace = true }
turbo-tasks = { workspace = true }
turbo-tasks-fs = { workspace = true }
turbopack-core = { workspace = true }

[build-dependencies]
turbo-tasks-build = { workspace = true }
5 changes: 5 additions & 0 deletions crates/turbopack-image/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use turbo_tasks_build::generate_register;

fn main() {
generate_register();
}
8 changes: 8 additions & 0 deletions crates/turbopack-image/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pub mod process;

pub fn register() {
turbo_tasks::register();
turbo_tasks_fs::register();
turbopack_core::register();
include!(concat!(env!("OUT_DIR"), "/register.rs"));
}
256 changes: 256 additions & 0 deletions crates/turbopack-image/src/process.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
use std::{io::Cursor, str::FromStr};

use anyhow::{bail, Context, Result};
use base64::{display::Base64Display, engine::general_purpose::STANDARD};
use image::{
codecs::{
jpeg::JpegEncoder,
png::{CompressionType, PngEncoder},
webp::{WebPEncoder, WebPQuality},
},
imageops::FilterType,
GenericImageView, ImageEncoder, ImageFormat,
};
use mime::Mime;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use turbo_tasks::{debug::ValueDebugFormat, primitives::StringVc, trace::TraceRawVcs};
use turbo_tasks_fs::{FileContent, FileContentVc, FileSystemPathVc};
use turbopack_core::{
error::PrettyPrintError,
ident::AssetIdentVc,
issue::{Issue, IssueVc},
};

/// Small placeholder version of the image.
#[derive(PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, ValueDebugFormat)]
pub struct BlurPlaceholder {
pub data_url: String,
pub width: u32,
pub height: u32,
}

/// Gathered meta information about an image.
#[serde_as]
#[turbo_tasks::value]
#[derive(Default)]
pub struct ImageMetaData {
pub width: u32,
pub height: u32,
#[turbo_tasks(trace_ignore, debug_ignore)]
#[serde_as(as = "Option<DisplayFromStr>")]
pub mime_type: Option<Mime>,
pub blur_placeholder: Option<BlurPlaceholder>,
placeholder_for_future_extensions: (),
}

/// Options for generating a blur placeholder.
#[turbo_tasks::value(shared)]
pub struct BlurPlaceholderOptions {
pub quality: u8,
pub size: u32,
}

fn load_image(bytes: &[u8]) -> Result<(image::DynamicImage, Option<ImageFormat>)> {
let reader = image::io::Reader::new(Cursor::new(&bytes));
let reader = reader
.with_guessed_format()
.context("unable to determine image format from file content")?;
let format = reader.format();
let image = reader.decode().context("unable to decode image data")?;
Ok((image, format))
}

fn compute_blur_data(
image: image::DynamicImage,
format: ImageFormat,
options: &BlurPlaceholderOptions,
) -> Result<(String, u32, u32)> {
let small_image = image.resize(options.size, options.size, FilterType::Triangle);
let mut buf = Vec::new();
let blur_width = small_image.width();
let blur_height = small_image.height();
let url = match format {
ImageFormat::Png => {
PngEncoder::new_with_quality(
&mut buf,
CompressionType::Best,
image::codecs::png::FilterType::NoFilter,
)
.write_image(
small_image.as_bytes(),
blur_width,
blur_height,
small_image.color(),
)?;
format!(
"data:image/png;base64,{}",
Base64Display::new(&buf, &STANDARD)
)
}
ImageFormat::Jpeg => {
JpegEncoder::new_with_quality(&mut buf, options.quality).write_image(
small_image.as_bytes(),
blur_width,
blur_height,
small_image.color(),
)?;
format!(
"data:image/jpeg;base64,{}",
Base64Display::new(&buf, &STANDARD)
)
}
ImageFormat::WebP => {
WebPEncoder::new_with_quality(&mut buf, WebPQuality::lossy(options.quality))
.write_image(
small_image.as_bytes(),
blur_width,
blur_height,
small_image.color(),
)?;
format!(
"data:image/webp;base64,{}",
Base64Display::new(&buf, &STANDARD)
)
}
#[cfg(feature = "avif")]
ImageFormat::Avif => {
use image::codecs::avif::AvifEncoder;
AvifEncoder::new_with_speed_quality(&mut buf, 6, options.quality).write_image(
small_image.as_bytes(),
blur_width,
blur_height,
small_image.color(),
)?;
format!(
"data:image/avif;base64,{}",
Base64Display::new(&buf, &STANDARD)
)
}
_ => unreachable!(),
};

Ok((url, blur_width, blur_height))
}

fn image_format_to_mime_type(format: ImageFormat) -> Result<Option<Mime>> {
Ok(match format {
ImageFormat::Png => Some(mime::IMAGE_PNG),
ImageFormat::Jpeg => Some(mime::IMAGE_JPEG),
ImageFormat::WebP => Some(Mime::from_str("image/webp")?),
ImageFormat::Avif => Some(Mime::from_str("image/avif")?),
ImageFormat::Bmp => Some(mime::IMAGE_BMP),
ImageFormat::Dds => Some(Mime::from_str("image/vnd-ms.dds")?),
ImageFormat::Farbfeld => Some(mime::APPLICATION_OCTET_STREAM),
ImageFormat::Gif => Some(mime::IMAGE_GIF),
ImageFormat::Hdr => Some(Mime::from_str("image/vnd.radiance")?),
ImageFormat::Ico => Some(Mime::from_str("image/x-icon")?),
ImageFormat::OpenExr => Some(Mime::from_str("image/x-exr")?),
ImageFormat::Pnm => Some(Mime::from_str("image/x-portable-anymap")?),
ImageFormat::Qoi => Some(mime::APPLICATION_OCTET_STREAM),
ImageFormat::Tga => Some(Mime::from_str("image/x-tga")?),
ImageFormat::Tiff => Some(Mime::from_str("image/tiff")?),
_ => None,
})
}

/// Analyze an image and return meta information about it.
/// Optionally computes a blur placeholder.
#[turbo_tasks::function]
pub async fn get_meta_data(
ident: AssetIdentVc,
content: FileContentVc,
blur_placeholder: Option<BlurPlaceholderOptionsVc>,
) -> Result<ImageMetaDataVc> {
let FileContent::Content(content) = &*content.await? else {
bail!("Input image not found");
};
let bytes = content.content().to_bytes()?;
let (image, format) = match load_image(&bytes) {
Ok(r) => r,
Err(err) => {
ImageProcessingIssue {
path: ident.path(),
message: StringVc::cell(format!("{}", PrettyPrintError(&err))),
}
.cell()
.as_issue()
.emit();
return Ok(ImageMetaData::default().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)
) {
match compute_blur_data(image, format.unwrap(), &*blur_placeholder.await?)
.context("unable to compute blur placeholder")
{
Ok((url, blur_width, blur_height)) => Some(BlurPlaceholder {
data_url: url,
width: blur_width,
height: blur_height,
}),
Err(err) => {
ImageProcessingIssue {
path: ident.path(),
message: StringVc::cell(format!("{}", PrettyPrintError(&err))),
}
.cell()
.as_issue()
.emit();
None
}
}
} 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())
}

#[turbo_tasks::value]
struct ImageProcessingIssue {
path: FileSystemPathVc,
message: StringVc,
}

#[turbo_tasks::value_impl]
impl Issue for ImageProcessingIssue {
#[turbo_tasks::function]
fn context(&self) -> FileSystemPathVc {
self.path
}
#[turbo_tasks::function]
fn category(&self) -> StringVc {
StringVc::cell("image".to_string())
}
#[turbo_tasks::function]
fn title(&self) -> StringVc {
StringVc::cell("Processing image failed".to_string())
}
#[turbo_tasks::function]
fn description(&self) -> StringVc {
self.message
}
}
1 change: 1 addition & 0 deletions crates/turbopack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ turbopack-core = { workspace = true }
turbopack-css = { workspace = true }
turbopack-ecmascript = { workspace = true }
turbopack-env = { workspace = true }
turbopack-image = { workspace = true }
turbopack-json = { workspace = true }
turbopack-mdx = { workspace = true }
turbopack-node = { workspace = true }
Expand Down
3 changes: 2 additions & 1 deletion crates/turbopack/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use turbopack_core::{
context::{AssetContext, AssetContextVc},
ident::AssetIdentVc,
issue::{unsupported_module::UnsupportedModuleIssue, Issue, IssueVc},
plugin::CustomModuleType,
reference::all_referenced_assets,
reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType},
resolve::{
Expand Down Expand Up @@ -177,7 +178,7 @@ async fn apply_module_type(
transforms,
options,
} => MdxModuleAssetVc::new(source, context.into(), *transforms, *options).into(),
ModuleType::Custom(_) => todo!(),
ModuleType::Custom(custom) => custom.create_module(source, context.into(), part),
})
}

Expand Down
Loading

0 comments on commit bb6f434

Please sign in to comment.