Skip to content
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions cells/cell-image-proto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub struct ThumbhashInput {
pub pixels: Vec<u8>,
pub width: u32,
pub height: u32,
pub channels: u8,
}

/// Image processor interface.
Expand Down
18 changes: 10 additions & 8 deletions cells/cell-image/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,16 @@ impl ImageProcessor for ImageProcessorImpl {
}

async fn generate_thumbhash_data_url(&self, input: ThumbhashInput) -> ImageResult {
let img = match pixels_to_dynamic_image(&input.pixels, input.width, input.height, 4) {
Some(img) => img,
None => {
return ImageResult::Error {
message: "Invalid pixel data".to_string(),
};
}
};
let img =
match pixels_to_dynamic_image(&input.pixels, input.width, input.height, input.channels)
{
Some(img) => img,
None => {
return ImageResult::Error {
message: "Invalid pixel data".to_string(),
};
}
};

// Thumbhash works best with small images, resize if needed
let thumb_img = if input.width > 100 || input.height > 100 {
Expand Down
1 change: 1 addition & 0 deletions cells/cell-jxl-proto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct JXLEncodeInput {
pub width: u32,
pub height: u32,
pub quality: u8,
pub channels: u8,
}

/// Result of JXL processing operations
Expand Down
6 changes: 3 additions & 3 deletions cells/cell-jxl/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ impl JXLProcessor for JXLProcessorImpl {
}

async fn encode_jxl(&self, input: JXLEncodeInput) -> JXLResult {
if input.pixels.len() != (input.width * input.height * 4) as usize {
if input.pixels.len() != (input.width * input.height * input.channels as u32) as usize {
return JXLResult::Error {
message: format!(
"Expected {} bytes for {}x{} RGBA, got {}",
input.width * input.height * 4,
input.width * input.height * input.channels as u32,
input.width,
input.height,
input.pixels.len()
Expand All @@ -71,7 +71,7 @@ impl JXLProcessor for JXLProcessorImpl {
};

encoder.has_alpha = true;
let frame = EncoderFrame::new(&input.pixels).num_channels(4);
let frame = EncoderFrame::new(&input.pixels).num_channels(input.channels as u32);
let result = match encoder.encode_frame::<_, u8>(&frame, input.width, input.height) {
Ok(r) => r,
Err(e) => {
Expand Down
1 change: 1 addition & 0 deletions cells/cell-webp-proto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct WebPEncodeInput {
pub width: u32,
pub height: u32,
pub quality: u8,
pub channels: u8,
}

/// Result of WebP processing operations
Expand Down
10 changes: 7 additions & 3 deletions cells/cell-webp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,23 @@ impl WebPProcessor for WebPProcessorImpl {
}

async fn encode_webp(&self, input: WebPEncodeInput) -> WebPResult {
if input.pixels.len() != (input.width * input.height * 4) as usize {
if input.pixels.len() != (input.width * input.height * input.channels as u32) as usize {
return WebPResult::Error {
message: format!(
"Expected {} bytes for {}x{} RGBA, got {}",
input.width * input.height * 4,
input.width * input.height * input.channels as u32,
input.width,
input.height,
input.pixels.len()
),
};
}

let encoder = webp::Encoder::from_rgba(&input.pixels, input.width, input.height);
let encoder = if input.channels == 4 {
webp::Encoder::from_rgba(&input.pixels, input.width, input.height)
} else {
webp::Encoder::from_rgb(&input.pixels, input.width, input.height)
};
let webp = encoder.encode(input.quality as f32);

WebPResult::EncodeSuccess {
Expand Down
26 changes: 23 additions & 3 deletions crates/dodeca/src/cells.rs
Original file line number Diff line number Diff line change
Expand Up @@ -585,11 +585,17 @@ pub async fn resize_image(
}
}

pub async fn generate_thumbhash(pixels: &[u8], width: u32, height: u32) -> Option<String> {
pub async fn generate_thumbhash(
pixels: &[u8],
width: u32,
height: u32,
channels: u8,
) -> Option<String> {
let input = ThumbhashInput {
pixels: pixels.to_vec(),
width,
height,
channels,
};
match ddc_cell_image::ImageProcessorImpl
.generate_thumbhash_data_url(input)
Expand All @@ -604,12 +610,19 @@ pub async fn generate_thumbhash(pixels: &[u8], width: u32, height: u32) -> Optio
}
}

pub async fn encode_webp(pixels: &[u8], width: u32, height: u32, quality: u8) -> Option<Vec<u8>> {
pub async fn encode_webp(
pixels: &[u8],
width: u32,
height: u32,
quality: u8,
channels: u8,
) -> Option<Vec<u8>> {
let input = WebPEncodeInput {
pixels: pixels.to_vec(),
width,
height,
quality,
channels,
};
match ddc_cell_webp::WebPProcessorImpl.encode_webp(input).await {
WebPResult::EncodeSuccess { data } => Some(data),
Expand All @@ -621,12 +634,19 @@ pub async fn encode_webp(pixels: &[u8], width: u32, height: u32, quality: u8) ->
}
}

pub async fn encode_jxl(pixels: &[u8], width: u32, height: u32, quality: u8) -> Option<Vec<u8>> {
pub async fn encode_jxl(
pixels: &[u8],
width: u32,
height: u32,
quality: u8,
channels: u8,
) -> Option<Vec<u8>> {
let input = JXLEncodeInput {
pixels: pixels.to_vec(),
width,
height,
quality,
channels,
};
match ddc_cell_jxl::JXLProcessorImpl.encode_jxl(input).await {
JXLResult::EncodeSuccess { data } => Some(data),
Expand Down
34 changes: 27 additions & 7 deletions crates/dodeca/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,18 +128,24 @@ async fn resize_image(decoded: &DecodedImage, target_width: u32) -> Option<Decod

/// Generate a thumbhash and encode it as a data URL
async fn generate_thumbhash_data_url(decoded: &DecodedImage) -> Option<String> {
cells::generate_thumbhash(&decoded.pixels, decoded.width, decoded.height).await
cells::generate_thumbhash(
&decoded.pixels,
decoded.width,
decoded.height,
decoded.channels,
)
.await
}

/// Encode pixels to WebP format.
async fn encode_webp(pixels: &[u8], width: u32, height: u32) -> Option<Vec<u8>> {
cells::encode_webp(pixels, width, height, 82).await
async fn encode_webp(pixels: &[u8], width: u32, height: u32, channels: u8) -> Option<Vec<u8>> {
cells::encode_webp(pixels, width, height, 82, channels).await
}

/// Encode pixels to JPEG-XL format.
async fn encode_jxl(pixels: &[u8], width: u32, height: u32) -> Option<Vec<u8>> {
async fn encode_jxl(pixels: &[u8], width: u32, height: u32, channels: u8) -> Option<Vec<u8>> {
// Quality 80 maps to distance ~3 in the cell (high quality)
cells::encode_jxl(pixels, width, height, 80).await
cells::encode_jxl(pixels, width, height, 80, channels).await
}

/// Image metadata without the processed bytes
Expand Down Expand Up @@ -219,15 +225,29 @@ pub async fn process_image(data: &[u8], input_format: InputFormat) -> Option<Pro
let height = resized.height;

// Encode to both formats
if let Some(jxl_data) = encode_jxl(&resized.pixels, resized.width, resized.height).await {
if let Some(jxl_data) = encode_jxl(
&resized.pixels,
resized.width,
resized.height,
resized.channels,
)
.await
{
jxl_variants.push(ImageVariant {
data: jxl_data,
width,
height,
});
}

if let Some(webp_data) = encode_webp(&resized.pixels, resized.width, resized.height).await {
if let Some(webp_data) = encode_webp(
&resized.pixels,
resized.width,
resized.height,
resized.channels,
)
.await
{
webp_variants.push(ImageVariant {
data: webp_data,
width,
Expand Down
Loading