Skip to content

Commit 36434c8

Browse files
authored
add turbopack-image (vercel/turborepo#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: #48531
1 parent 1aa554f commit 36434c8

File tree

12 files changed

+330
-5
lines changed

12 files changed

+330
-5
lines changed

crates/turbo-binding/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ __turbopack_dev_dynamic_embed_contents = [
109109
__turbopack_dev_server = ["__turbopack", "turbopack-dev-server"]
110110
__turbopack_ecmascript = ["__turbopack", "turbopack-ecmascript"]
111111
__turbopack_env = ["__turbopack", "turbopack-env"]
112+
__turbopack_image = ["__turbopack", "turbopack-image"]
112113
__turbopack_json = ["__turbopack", "turbopack-json"]
113114
__turbopack_mdx = ["__turbopack", "turbopack-mdx"]
114115
__turbopack_node = ["__turbopack", "turbopack-node"]
@@ -183,6 +184,7 @@ turbopack-dev = { optional = true, workspace = true }
183184
turbopack-dev-server = { optional = true, workspace = true }
184185
turbopack-ecmascript = { optional = true, workspace = true }
185186
turbopack-env = { optional = true, workspace = true }
187+
turbopack-image = { optional = true, workspace = true }
186188
turbopack-json = { optional = true, workspace = true }
187189
turbopack-mdx = { optional = true, workspace = true }
188190
turbopack-node = { optional = true, workspace = true }

crates/turbo-binding/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,16 @@ pub mod turbopack {
7272
pub use turbopack_ecmascript as ecmascript;
7373
#[cfg(feature = "__turbopack_env")]
7474
pub use turbopack_env as env;
75+
#[cfg(feature = "__turbopack_image")]
76+
pub use turbopack_image as image;
7577
#[cfg(feature = "__turbopack_json")]
7678
pub use turbopack_json as json;
7779
#[cfg(feature = "__turbopack_mdx")]
7880
pub use turbopack_mdx as mdx;
7981
#[cfg(feature = "__turbopack_node")]
8082
pub use turbopack_node as node;
8183
#[cfg(feature = "__turbopack_static")]
82-
pub use turbopack_static;
84+
pub use turbopack_static as r#static;
8385
#[cfg(feature = "__turbopack_swc_utils")]
8486
pub use turbopack_swc_utils as swc_utils;
8587
#[cfg(feature = "__turbopack_test_utils")]

crates/turbopack-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub mod error;
1616
pub mod ident;
1717
pub mod introspect;
1818
pub mod issue;
19+
pub mod plugin;
1920
pub mod reference;
2021
pub mod reference_type;
2122
pub mod resolve;

crates/turbopack-core/src/plugin.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use crate::{asset::AssetVc, context::AssetContextVc, resolve::ModulePartVc};
2+
3+
#[turbo_tasks::value_trait]
4+
pub trait CustomModuleType {
5+
fn create_module(
6+
&self,
7+
source: AssetVc,
8+
context: AssetContextVc,
9+
part: Option<ModulePartVc>,
10+
) -> AssetVc;
11+
}

crates/turbopack-ecmascript/src/transform/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ pub struct EcmascriptInputTransforms(Vec<EcmascriptInputTransform>);
117117

118118
#[turbo_tasks::value_impl]
119119
impl EcmascriptInputTransformsVc {
120+
#[turbo_tasks::function]
121+
pub fn empty() -> Self {
122+
EcmascriptInputTransformsVc::cell(Vec::new())
123+
}
124+
120125
#[turbo_tasks::function]
121126
pub async fn extend(self, other: EcmascriptInputTransformsVc) -> Result<Self> {
122127
let mut transforms = self.await?.clone_value();

crates/turbopack-image/Cargo.toml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[package]
2+
name = "turbopack-image"
3+
version = "0.1.0"
4+
description = "TBD"
5+
license = "MPL-2.0"
6+
edition = "2021"
7+
autobenches = false
8+
9+
[lib]
10+
bench = false
11+
12+
[features]
13+
avif = ["image/avif-decoder", "image/avif-encoder"]
14+
15+
[dependencies]
16+
anyhow = { workspace = true }
17+
base64 = "0.21.0"
18+
image = { workspace = true, default-features = false, features = [
19+
"webp",
20+
"png",
21+
"jpeg",
22+
"webp-encoder",
23+
] }
24+
indexmap = { workspace = true }
25+
mime = { workspace = true }
26+
serde = { workspace = true }
27+
serde_with = { workspace = true }
28+
turbo-tasks = { workspace = true }
29+
turbo-tasks-fs = { workspace = true }
30+
turbopack-core = { workspace = true }
31+
32+
[build-dependencies]
33+
turbo-tasks-build = { workspace = true }

crates/turbopack-image/build.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
use turbo_tasks_build::generate_register;
2+
3+
fn main() {
4+
generate_register();
5+
}

crates/turbopack-image/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
pub mod process;
2+
3+
pub fn register() {
4+
turbo_tasks::register();
5+
turbo_tasks_fs::register();
6+
turbopack_core::register();
7+
include!(concat!(env!("OUT_DIR"), "/register.rs"));
8+
}

crates/turbopack-image/src/process.rs

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
use std::{io::Cursor, str::FromStr};
2+
3+
use anyhow::{bail, Context, Result};
4+
use base64::{display::Base64Display, engine::general_purpose::STANDARD};
5+
use image::{
6+
codecs::{
7+
jpeg::JpegEncoder,
8+
png::{CompressionType, PngEncoder},
9+
webp::{WebPEncoder, WebPQuality},
10+
},
11+
imageops::FilterType,
12+
GenericImageView, ImageEncoder, ImageFormat,
13+
};
14+
use mime::Mime;
15+
use serde::{Deserialize, Serialize};
16+
use serde_with::{serde_as, DisplayFromStr};
17+
use turbo_tasks::{debug::ValueDebugFormat, primitives::StringVc, trace::TraceRawVcs};
18+
use turbo_tasks_fs::{FileContent, FileContentVc, FileSystemPathVc};
19+
use turbopack_core::{
20+
error::PrettyPrintError,
21+
ident::AssetIdentVc,
22+
issue::{Issue, IssueVc},
23+
};
24+
25+
/// Small placeholder version of the image.
26+
#[derive(PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, ValueDebugFormat)]
27+
pub struct BlurPlaceholder {
28+
pub data_url: String,
29+
pub width: u32,
30+
pub height: u32,
31+
}
32+
33+
/// Gathered meta information about an image.
34+
#[serde_as]
35+
#[turbo_tasks::value]
36+
#[derive(Default)]
37+
pub struct ImageMetaData {
38+
pub width: u32,
39+
pub height: u32,
40+
#[turbo_tasks(trace_ignore, debug_ignore)]
41+
#[serde_as(as = "Option<DisplayFromStr>")]
42+
pub mime_type: Option<Mime>,
43+
pub blur_placeholder: Option<BlurPlaceholder>,
44+
placeholder_for_future_extensions: (),
45+
}
46+
47+
/// Options for generating a blur placeholder.
48+
#[turbo_tasks::value(shared)]
49+
pub struct BlurPlaceholderOptions {
50+
pub quality: u8,
51+
pub size: u32,
52+
}
53+
54+
fn load_image(bytes: &[u8]) -> Result<(image::DynamicImage, Option<ImageFormat>)> {
55+
let reader = image::io::Reader::new(Cursor::new(&bytes));
56+
let reader = reader
57+
.with_guessed_format()
58+
.context("unable to determine image format from file content")?;
59+
let format = reader.format();
60+
let image = reader.decode().context("unable to decode image data")?;
61+
Ok((image, format))
62+
}
63+
64+
fn compute_blur_data(
65+
image: image::DynamicImage,
66+
format: ImageFormat,
67+
options: &BlurPlaceholderOptions,
68+
) -> Result<(String, u32, u32)> {
69+
let small_image = image.resize(options.size, options.size, FilterType::Triangle);
70+
let mut buf = Vec::new();
71+
let blur_width = small_image.width();
72+
let blur_height = small_image.height();
73+
let url = match format {
74+
ImageFormat::Png => {
75+
PngEncoder::new_with_quality(
76+
&mut buf,
77+
CompressionType::Best,
78+
image::codecs::png::FilterType::NoFilter,
79+
)
80+
.write_image(
81+
small_image.as_bytes(),
82+
blur_width,
83+
blur_height,
84+
small_image.color(),
85+
)?;
86+
format!(
87+
"data:image/png;base64,{}",
88+
Base64Display::new(&buf, &STANDARD)
89+
)
90+
}
91+
ImageFormat::Jpeg => {
92+
JpegEncoder::new_with_quality(&mut buf, options.quality).write_image(
93+
small_image.as_bytes(),
94+
blur_width,
95+
blur_height,
96+
small_image.color(),
97+
)?;
98+
format!(
99+
"data:image/jpeg;base64,{}",
100+
Base64Display::new(&buf, &STANDARD)
101+
)
102+
}
103+
ImageFormat::WebP => {
104+
WebPEncoder::new_with_quality(&mut buf, WebPQuality::lossy(options.quality))
105+
.write_image(
106+
small_image.as_bytes(),
107+
blur_width,
108+
blur_height,
109+
small_image.color(),
110+
)?;
111+
format!(
112+
"data:image/webp;base64,{}",
113+
Base64Display::new(&buf, &STANDARD)
114+
)
115+
}
116+
#[cfg(feature = "avif")]
117+
ImageFormat::Avif => {
118+
use image::codecs::avif::AvifEncoder;
119+
AvifEncoder::new_with_speed_quality(&mut buf, 6, options.quality).write_image(
120+
small_image.as_bytes(),
121+
blur_width,
122+
blur_height,
123+
small_image.color(),
124+
)?;
125+
format!(
126+
"data:image/avif;base64,{}",
127+
Base64Display::new(&buf, &STANDARD)
128+
)
129+
}
130+
_ => unreachable!(),
131+
};
132+
133+
Ok((url, blur_width, blur_height))
134+
}
135+
136+
fn image_format_to_mime_type(format: ImageFormat) -> Result<Option<Mime>> {
137+
Ok(match format {
138+
ImageFormat::Png => Some(mime::IMAGE_PNG),
139+
ImageFormat::Jpeg => Some(mime::IMAGE_JPEG),
140+
ImageFormat::WebP => Some(Mime::from_str("image/webp")?),
141+
ImageFormat::Avif => Some(Mime::from_str("image/avif")?),
142+
ImageFormat::Bmp => Some(mime::IMAGE_BMP),
143+
ImageFormat::Dds => Some(Mime::from_str("image/vnd-ms.dds")?),
144+
ImageFormat::Farbfeld => Some(mime::APPLICATION_OCTET_STREAM),
145+
ImageFormat::Gif => Some(mime::IMAGE_GIF),
146+
ImageFormat::Hdr => Some(Mime::from_str("image/vnd.radiance")?),
147+
ImageFormat::Ico => Some(Mime::from_str("image/x-icon")?),
148+
ImageFormat::OpenExr => Some(Mime::from_str("image/x-exr")?),
149+
ImageFormat::Pnm => Some(Mime::from_str("image/x-portable-anymap")?),
150+
ImageFormat::Qoi => Some(mime::APPLICATION_OCTET_STREAM),
151+
ImageFormat::Tga => Some(Mime::from_str("image/x-tga")?),
152+
ImageFormat::Tiff => Some(Mime::from_str("image/tiff")?),
153+
_ => None,
154+
})
155+
}
156+
157+
/// Analyze an image and return meta information about it.
158+
/// Optionally computes a blur placeholder.
159+
#[turbo_tasks::function]
160+
pub async fn get_meta_data(
161+
ident: AssetIdentVc,
162+
content: FileContentVc,
163+
blur_placeholder: Option<BlurPlaceholderOptionsVc>,
164+
) -> Result<ImageMetaDataVc> {
165+
let FileContent::Content(content) = &*content.await? else {
166+
bail!("Input image not found");
167+
};
168+
let bytes = content.content().to_bytes()?;
169+
let (image, format) = match load_image(&bytes) {
170+
Ok(r) => r,
171+
Err(err) => {
172+
ImageProcessingIssue {
173+
path: ident.path(),
174+
message: StringVc::cell(format!("{}", PrettyPrintError(&err))),
175+
}
176+
.cell()
177+
.as_issue()
178+
.emit();
179+
return Ok(ImageMetaData::default().cell());
180+
}
181+
};
182+
let (width, height) = image.dimensions();
183+
let blur_placeholder = if let Some(blur_placeholder) = blur_placeholder {
184+
if matches!(
185+
format,
186+
// list should match next/client/image.tsx
187+
Some(ImageFormat::Png)
188+
| Some(ImageFormat::Jpeg)
189+
| Some(ImageFormat::WebP)
190+
| Some(ImageFormat::Avif)
191+
) {
192+
match compute_blur_data(image, format.unwrap(), &*blur_placeholder.await?)
193+
.context("unable to compute blur placeholder")
194+
{
195+
Ok((url, blur_width, blur_height)) => Some(BlurPlaceholder {
196+
data_url: url,
197+
width: blur_width,
198+
height: blur_height,
199+
}),
200+
Err(err) => {
201+
ImageProcessingIssue {
202+
path: ident.path(),
203+
message: StringVc::cell(format!("{}", PrettyPrintError(&err))),
204+
}
205+
.cell()
206+
.as_issue()
207+
.emit();
208+
None
209+
}
210+
}
211+
} else {
212+
None
213+
}
214+
} else {
215+
None
216+
};
217+
218+
Ok(ImageMetaData {
219+
width,
220+
height,
221+
mime_type: if let Some(format) = format {
222+
image_format_to_mime_type(format)?
223+
} else {
224+
None
225+
},
226+
blur_placeholder,
227+
placeholder_for_future_extensions: (),
228+
}
229+
.cell())
230+
}
231+
232+
#[turbo_tasks::value]
233+
struct ImageProcessingIssue {
234+
path: FileSystemPathVc,
235+
message: StringVc,
236+
}
237+
238+
#[turbo_tasks::value_impl]
239+
impl Issue for ImageProcessingIssue {
240+
#[turbo_tasks::function]
241+
fn context(&self) -> FileSystemPathVc {
242+
self.path
243+
}
244+
#[turbo_tasks::function]
245+
fn category(&self) -> StringVc {
246+
StringVc::cell("image".to_string())
247+
}
248+
#[turbo_tasks::function]
249+
fn title(&self) -> StringVc {
250+
StringVc::cell("Processing image failed".to_string())
251+
}
252+
#[turbo_tasks::function]
253+
fn description(&self) -> StringVc {
254+
self.message
255+
}
256+
}

crates/turbopack/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ turbopack-core = { workspace = true }
3030
turbopack-css = { workspace = true }
3131
turbopack-ecmascript = { workspace = true }
3232
turbopack-env = { workspace = true }
33+
turbopack-image = { workspace = true }
3334
turbopack-json = { workspace = true }
3435
turbopack-mdx = { workspace = true }
3536
turbopack-node = { workspace = true }

0 commit comments

Comments
 (0)