use std::path::Path;
#[cfg(any_standard_encode)]
use crate::core::BitDepth;
use crate::core::image::RgbImage;
use crate::core::metadata::ImageMetadata;
#[cfg(any_standard_encode)]
use crate::error::EncodeError;
use crate::error::{RawError, RawResult};
use super::export::EncodeOptions;
#[cfg(feature = "webp-encode")]
use super::export::WebPMode;
#[cfg_attr(not(any_standard_encode), allow(unused_variables, unreachable_code))]
pub fn encode_rgb_image_to_vec(
image: &RgbImage,
metadata: &ImageMetadata,
encode_options: &EncodeOptions,
) -> RawResult<Vec<u8>> {
match encode_options {
#[cfg(feature = "png-encode")]
EncodeOptions::PngZune(cfg) => encode_png(image, metadata, cfg),
#[cfg(feature = "jpeg-encode")]
EncodeOptions::JpegJpegEnc(cfg) => encode_jpeg(image, metadata, cfg),
#[cfg(feature = "webp-encode")]
EncodeOptions::WebpLibwebp(cfg) => encode_webp(image, metadata, cfg),
#[cfg(feature = "avif-encode")]
EncodeOptions::AvifRavif(cfg) => encode_avif(image, metadata, cfg),
#[cfg(feature = "jxl-encode")]
EncodeOptions::JxlZune(cfg) => encode_jxl(image, metadata, cfg),
#[cfg(feature = "dng-encode")]
EncodeOptions::Dng(cfg) => {
let mut buf = std::io::Cursor::new(Vec::new());
super::dng_export::export_dng_to_writer(&mut buf, image, metadata, cfg)?;
Ok(buf.into_inner())
}
#[allow(unreachable_patterns)]
_ => Err(RawError::Unsupported(
"This encode format is not available with the current feature flags.".to_string(),
)),
}
}
pub fn encode_rgb_image_to_writer<W: std::io::Write>(
image: &RgbImage,
metadata: &ImageMetadata,
writer: &mut W,
encode_options: &EncodeOptions,
) -> RawResult<()> {
let bytes = encode_rgb_image_to_vec(image, metadata, encode_options)?;
writer.write_all(&bytes)?;
Ok(())
}
pub fn encode_rgb_image(
image: &RgbImage,
metadata: &ImageMetadata,
path: &Path,
encode_options: &EncodeOptions,
) -> RawResult<()> {
let bytes = encode_rgb_image_to_vec(image, metadata, encode_options)?;
std::fs::write(path, bytes)?;
Ok(())
}
#[cfg(any_standard_encode)]
#[allow(dead_code)] fn pack_rgb8(image: &RgbImage) -> Vec<u8> {
image.data.iter().map(|&p| (p >> 8) as u8).collect()
}
#[cfg(any_standard_encode)]
#[allow(dead_code)] fn check_8bit_backend(bit_depth: BitDepth, format: &'static str) -> Result<(), EncodeError> {
match bit_depth {
BitDepth::Eight | BitDepth::Sixteen => Ok(()),
_ => Err(EncodeError::UnsupportedBitDepth {
format,
requested: bit_depth,
}),
}
}
#[cfg(feature = "png-encode")]
fn encode_png(
image: &RgbImage,
metadata: &ImageMetadata,
cfg: &super::export::ZunePngEncodeConfig,
) -> RawResult<Vec<u8>> {
use zune_core::colorspace::ColorSpace as ZuneColorSpace;
use zune_core::options::EncoderOptions;
use zune_png::PngEncoder;
let (data_bytes, depth) = match cfg.common.bit_depth {
BitDepth::Eight => (pack_rgb8(image), zune_core::bit_depth::BitDepth::Eight),
BitDepth::Sixteen => {
let mut bytes = Vec::with_capacity(image.data.len() * 2);
for &pixel in &image.data {
bytes.extend_from_slice(&pixel.to_be_bytes());
}
(bytes, zune_core::bit_depth::BitDepth::Sixteen)
}
other => {
return Err(RawError::Encode(EncodeError::UnsupportedBitDepth {
format: "PNG",
requested: other,
}));
}
};
let options = EncoderOptions::default()
.set_width(image.width() as usize)
.set_height(image.height() as usize)
.set_colorspace(ZuneColorSpace::RGB)
.set_depth(depth);
let mut encoder = PngEncoder::new(&data_bytes, options);
let mut output = Vec::new();
encoder.encode(&mut output).map_err(|e| {
RawError::Encode(EncodeError::Encoding {
format: "PNG",
message: format!("PNG encoding error: {e:?}"),
})
})?;
let m = &cfg.common.metadata;
if m.embed_exif || m.embed_icc || m.embed_xmp {
use crate::metadata::exif::ExifBuilder;
use crate::metadata::icc::IccProfile;
use img_parts::png::{Png, PngChunk};
use img_parts::{Bytes, ImageEXIF, ImageICC};
match Png::from_bytes(Bytes::from(output.clone())) {
Ok(mut png) => {
if m.embed_icc {
let icc = IccProfile::srgb();
png.set_icc_profile(Some(Bytes::from(icc.as_bytes().to_vec())));
}
if m.embed_exif {
let exif_builder = ExifBuilder::new(metadata);
match exif_builder.build_bytes() {
Ok(bytes) => png.set_exif(Some(Bytes::from(bytes))),
Err(e) => tracing::warn!("Failed to embed EXIF in PNG: {e}"),
}
}
if m.embed_xmp
&& let Some(xmp_data) = &metadata.xmp
{
let mut chunk_data = Vec::with_capacity(22 + xmp_data.len());
chunk_data.extend_from_slice(b"XML:com.adobe.xmp\0");
chunk_data.push(0); chunk_data.push(0); chunk_data.push(0); chunk_data.push(0); chunk_data.extend_from_slice(xmp_data);
let chunk = PngChunk::new(*b"iTXt", Bytes::from(chunk_data));
let idx = png.chunks().len().saturating_sub(1);
png.chunks_mut().insert(idx, chunk);
}
use std::io::Cursor;
let mut buf = Cursor::new(Vec::new());
match png.encoder().write_to(&mut buf) {
Ok(_) => output = buf.into_inner(),
Err(e) => tracing::warn!("Failed to write PNG with metadata: {e}"),
}
}
Err(e) => tracing::warn!("Failed to parse PNG for metadata embedding: {e}"),
}
}
Ok(output)
}
#[cfg(feature = "jpeg-encode")]
fn encode_jpeg(
image: &RgbImage,
metadata: &ImageMetadata,
cfg: &super::export::JpegEncEncodeConfig,
) -> RawResult<Vec<u8>> {
use crate::metadata::exif::ExifBuilder;
use crate::metadata::icc::IccProfile;
use jpeg_encoder::{ColorType, Encoder};
check_8bit_backend(cfg.common.bit_depth, "JPEG")?;
let data_8bit = pack_rgb8(image);
let quality = if cfg.quality == 0 { 90 } else { cfg.quality };
let mut jpeg_buf = Vec::new();
let encoder = Encoder::new(&mut jpeg_buf, quality);
encoder.encode(
&data_8bit,
image.width() as u16,
image.height() as u16,
ColorType::Rgb,
)?;
let m = &cfg.common.metadata;
if m.embed_exif {
let exif_builder = ExifBuilder::new(metadata);
match exif_builder.append_to_jpeg(jpeg_buf.clone()) {
Ok(data) => jpeg_buf = data,
Err(e) => tracing::warn!("Failed to embed EXIF in JPEG: {e}"),
}
}
if m.embed_icc {
match IccProfile::srgb().append_to_jpeg(jpeg_buf.clone()) {
Ok(data) => jpeg_buf = data,
Err(e) => tracing::warn!("Failed to embed ICC in JPEG: {e}"),
}
}
if m.embed_xmp
&& let Some(xmp_data) = &metadata.xmp
{
use crate::metadata::xmp::append_xmp_to_jpeg;
match append_xmp_to_jpeg(xmp_data, jpeg_buf.clone()) {
Ok(data) => jpeg_buf = data,
Err(e) => tracing::warn!("Failed to embed XMP in JPEG: {e}"),
}
}
Ok(jpeg_buf)
}
#[cfg(feature = "webp-encode")]
fn encode_webp(
image: &RgbImage,
metadata: &ImageMetadata,
cfg: &super::export::LibwebpEncodeConfig,
) -> RawResult<Vec<u8>> {
use crate::codecs::webp::{build_webp_config, encode_webp_rgb, mux_webp};
use crate::metadata::exif::ExifBuilder;
use crate::metadata::icc::IccProfile;
check_8bit_backend(cfg.common.bit_depth, "WebP")?;
let lossless = cfg.mode == WebPMode::Lossless;
let config = build_webp_config(lossless, cfg.quality, cfg.method, cfg.near_lossless)
.map_err(|e| RawError::Encode(EncodeError::WebP(e)))?;
let data_8bit = pack_rgb8(image);
let encoded = encode_webp_rgb(&data_8bit, image.width(), image.height(), &config)
.map_err(|e| RawError::Encode(EncodeError::WebP(e)))?;
let m = &cfg.common.metadata;
let exif_bytes = if m.embed_exif {
match ExifBuilder::new(metadata).build_bytes() {
Ok(bytes) => Some(bytes),
Err(e) => {
tracing::warn!("Failed to build EXIF for WebP: {e}");
None
}
}
} else {
None
};
let icc_bytes = if m.embed_icc {
Some(IccProfile::srgb().as_bytes().to_vec())
} else {
None
};
let xmp_bytes = if m.embed_xmp {
metadata.xmp.as_deref()
} else {
None
};
mux_webp(
&encoded,
exif_bytes.as_deref(),
icc_bytes.as_deref(),
xmp_bytes,
)
.map_err(|e| RawError::Encode(EncodeError::WebP(e)))
}
#[cfg(feature = "avif-encode")]
fn encode_avif(
image: &RgbImage,
metadata: &ImageMetadata,
cfg: &super::export::RavifEncodeConfig,
) -> RawResult<Vec<u8>> {
use crate::metadata::exif::ExifBuilder;
use crate::metadata::icc::IccProfile;
use crate::metadata::xmp::append_xmp_to_avif;
use ravif::{Encoder, Img, RGBA8};
check_8bit_backend(cfg.common.bit_depth, "AVIF")?;
let rgba_data: Vec<RGBA8> = image
.data
.chunks_exact(3)
.map(|rgb| {
RGBA8::new(
(rgb[0] >> 8) as u8,
(rgb[1] >> 8) as u8,
(rgb[2] >> 8) as u8,
255,
)
})
.collect();
let img = Img::new(
rgba_data.as_slice(),
image.width() as usize,
image.height() as usize,
);
let encoder = Encoder::new()
.with_quality(cfg.quality as f32)
.with_speed(cfg.speed);
let result = encoder.encode_rgba(img).map_err(|e| {
RawError::Encode(EncodeError::Encoding {
format: "AVIF",
message: format!("{e:?}"),
})
})?;
let mut avif_bytes = result.avif_file;
let m = &cfg.common.metadata;
if m.embed_icc {
match IccProfile::srgb().append_to_avif(avif_bytes.clone()) {
Ok(data) => avif_bytes = data,
Err(e) => tracing::warn!("Failed to embed ICC in AVIF: {e}"),
}
}
if m.embed_exif {
match ExifBuilder::new(metadata).append_to_avif(avif_bytes.clone()) {
Ok(data) => avif_bytes = data,
Err(e) => tracing::warn!("Failed to embed EXIF in AVIF: {e}"),
}
}
if m.embed_xmp
&& let Some(xmp_data) = &metadata.xmp
{
match append_xmp_to_avif(xmp_data, avif_bytes.clone()) {
Ok(data) => avif_bytes = data,
Err(e) => tracing::warn!("Failed to embed XMP in AVIF: {e}"),
}
}
Ok(avif_bytes)
}
#[cfg(feature = "jxl-encode")]
fn encode_jxl(
image: &RgbImage,
metadata: &ImageMetadata,
cfg: &super::export::ZuneJxlEncodeConfig,
) -> RawResult<Vec<u8>> {
use crate::metadata::exif::ExifBuilder;
use crate::metadata::icc::IccProfile;
use crate::metadata::xmp::append_xmp_to_jxl;
use zune_core::colorspace::ColorSpace as ZuneColorSpace;
use zune_core::options::EncoderOptions;
use zune_jpegxl::JxlSimpleEncoder;
check_8bit_backend(cfg.common.bit_depth, "JXL")?;
let data_8bit = pack_rgb8(image);
let quality = if cfg.quality == 0.0 {
100
} else {
cfg.quality as u8
};
let enc_options = EncoderOptions::default()
.set_width(image.width() as usize)
.set_height(image.height() as usize)
.set_colorspace(ZuneColorSpace::RGB)
.set_quality(quality);
let encoder = JxlSimpleEncoder::new(&data_8bit, enc_options);
let mut encoded: Vec<u8> = Vec::new();
encoder.encode(&mut encoded).map_err(|e| {
RawError::Encode(EncodeError::Encoding {
format: "JXL",
message: format!("{e:?}"),
})
})?;
let m = &cfg.common.metadata;
if m.embed_exif {
match ExifBuilder::new(metadata).append_to_jxl(encoded.clone()) {
Ok(data) => encoded = data,
Err(e) => tracing::warn!("Failed to embed EXIF in JXL: {e}"),
}
}
if m.embed_icc {
match IccProfile::srgb().append_to_jxl(encoded.clone()) {
Ok(data) => encoded = data,
Err(e) => tracing::warn!("Failed to embed ICC in JXL: {e}"),
}
}
if m.embed_xmp
&& let Some(xmp_data) = &metadata.xmp
{
match append_xmp_to_jxl(xmp_data, encoded.clone()) {
Ok(data) => encoded = data,
Err(e) => tracing::warn!("Failed to embed XMP in JXL: {e}"),
}
}
Ok(encoded)
}