From bb6f434347962f82c09934cc4494a37ff01e8f3f Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Wed, 19 Apr 2023 18:21:02 +0200 Subject: [PATCH] add turbopack-image (vercel/turbo#4621) ### 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: https://github.com/vercel/next.js/pull/48531 --- crates/turbo-binding/Cargo.toml | 2 + crates/turbo-binding/src/lib.rs | 4 +- crates/turbopack-core/src/lib.rs | 1 + crates/turbopack-core/src/plugin.rs | 11 + .../turbopack-ecmascript/src/transform/mod.rs | 5 + crates/turbopack-image/Cargo.toml | 33 +++ crates/turbopack-image/build.rs | 5 + crates/turbopack-image/src/lib.rs | 8 + crates/turbopack-image/src/process.rs | 256 ++++++++++++++++++ crates/turbopack/Cargo.toml | 1 + crates/turbopack/src/lib.rs | 3 +- .../src/module_options/module_rule.rs | 6 +- 12 files changed, 330 insertions(+), 5 deletions(-) create mode 100644 crates/turbopack-core/src/plugin.rs create mode 100644 crates/turbopack-image/Cargo.toml create mode 100644 crates/turbopack-image/build.rs create mode 100644 crates/turbopack-image/src/lib.rs create mode 100644 crates/turbopack-image/src/process.rs diff --git a/crates/turbo-binding/Cargo.toml b/crates/turbo-binding/Cargo.toml index 17d6d9a5a6ee2..48f7130764706 100644 --- a/crates/turbo-binding/Cargo.toml +++ b/crates/turbo-binding/Cargo.toml @@ -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"] @@ -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 } diff --git a/crates/turbo-binding/src/lib.rs b/crates/turbo-binding/src/lib.rs index 24b955bdc113d..37d5243047b96 100644 --- a/crates/turbo-binding/src/lib.rs +++ b/crates/turbo-binding/src/lib.rs @@ -72,6 +72,8 @@ 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")] @@ -79,7 +81,7 @@ pub mod turbopack { #[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")] diff --git a/crates/turbopack-core/src/lib.rs b/crates/turbopack-core/src/lib.rs index c77afde2c722f..71440142708ff 100644 --- a/crates/turbopack-core/src/lib.rs +++ b/crates/turbopack-core/src/lib.rs @@ -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; diff --git a/crates/turbopack-core/src/plugin.rs b/crates/turbopack-core/src/plugin.rs new file mode 100644 index 0000000000000..352fd58bd6fbb --- /dev/null +++ b/crates/turbopack-core/src/plugin.rs @@ -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, + ) -> AssetVc; +} diff --git a/crates/turbopack-ecmascript/src/transform/mod.rs b/crates/turbopack-ecmascript/src/transform/mod.rs index 0bf5eb85e0c5e..aeaf58a8a221a 100644 --- a/crates/turbopack-ecmascript/src/transform/mod.rs +++ b/crates/turbopack-ecmascript/src/transform/mod.rs @@ -117,6 +117,11 @@ pub struct EcmascriptInputTransforms(Vec); #[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 { let mut transforms = self.await?.clone_value(); diff --git a/crates/turbopack-image/Cargo.toml b/crates/turbopack-image/Cargo.toml new file mode 100644 index 0000000000000..8b8f53f5cbda6 --- /dev/null +++ b/crates/turbopack-image/Cargo.toml @@ -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 } diff --git a/crates/turbopack-image/build.rs b/crates/turbopack-image/build.rs new file mode 100644 index 0000000000000..1673efed59cce --- /dev/null +++ b/crates/turbopack-image/build.rs @@ -0,0 +1,5 @@ +use turbo_tasks_build::generate_register; + +fn main() { + generate_register(); +} diff --git a/crates/turbopack-image/src/lib.rs b/crates/turbopack-image/src/lib.rs new file mode 100644 index 0000000000000..d4bf74ce99301 --- /dev/null +++ b/crates/turbopack-image/src/lib.rs @@ -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")); +} diff --git a/crates/turbopack-image/src/process.rs b/crates/turbopack-image/src/process.rs new file mode 100644 index 0000000000000..e32fded4d3034 --- /dev/null +++ b/crates/turbopack-image/src/process.rs @@ -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")] + pub mime_type: Option, + pub blur_placeholder: Option, + 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)> { + 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> { + 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, +) -> Result { + 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 + } +} diff --git a/crates/turbopack/Cargo.toml b/crates/turbopack/Cargo.toml index 8bdfcb81ffe0d..6fefe53eea027 100644 --- a/crates/turbopack/Cargo.toml +++ b/crates/turbopack/Cargo.toml @@ -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 } diff --git a/crates/turbopack/src/lib.rs b/crates/turbopack/src/lib.rs index 8de5872bc20d6..4e3f495549330 100644 --- a/crates/turbopack/src/lib.rs +++ b/crates/turbopack/src/lib.rs @@ -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::{ @@ -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), }) } diff --git a/crates/turbopack/src/module_options/module_rule.rs b/crates/turbopack/src/module_options/module_rule.rs index f47c75f2e24c4..672ab0edd0657 100644 --- a/crates/turbopack/src/module_options/module_rule.rs +++ b/crates/turbopack/src/module_options/module_rule.rs @@ -3,7 +3,8 @@ use serde::{Deserialize, Serialize}; use turbo_tasks::trace::TraceRawVcs; use turbo_tasks_fs::FileSystemPath; use turbopack_core::{ - asset::AssetVc, reference_type::ReferenceType, source_transform::SourceTransformsVc, + asset::AssetVc, plugin::CustomModuleTypeVc, reference_type::ReferenceType, + source_transform::SourceTransformsVc, }; use turbopack_css::CssInputTransformsVc; use turbopack_ecmascript::{EcmascriptInputTransformsVc, EcmascriptOptions}; @@ -65,6 +66,5 @@ pub enum ModuleType { Css(CssInputTransformsVc), CssModule(CssInputTransformsVc), Static, - // TODO allow custom function when we support function pointers - Custom(u8), + Custom(CustomModuleTypeVc), }