1use std::path::Path;
11
12#[cfg(any_standard_encode)]
13use crate::core::BitDepth;
14use crate::core::image::RgbImage;
15use crate::core::metadata::ImageMetadata;
16#[cfg(any_standard_encode)]
17use crate::error::EncodeError;
18use crate::error::{RawError, RawResult};
19
20use super::export::EncodeOptions;
21#[cfg(feature = "webp-encode")]
22use super::export::WebPMode;
23
24#[cfg_attr(not(any_standard_encode), allow(unused_variables, unreachable_code))]
32pub fn encode_rgb_image_to_vec(
33 image: &RgbImage,
34 metadata: &ImageMetadata,
35 encode_options: &EncodeOptions,
36) -> RawResult<Vec<u8>> {
37 match encode_options {
38 #[cfg(feature = "png-encode")]
39 EncodeOptions::PngZune(cfg) => encode_png(image, metadata, cfg),
40 #[cfg(feature = "jpeg-encode")]
41 EncodeOptions::JpegJpegEnc(cfg) => encode_jpeg(image, metadata, cfg),
42 #[cfg(feature = "webp-encode")]
43 EncodeOptions::WebpLibwebp(cfg) => encode_webp(image, metadata, cfg),
44 #[cfg(feature = "avif-encode")]
45 EncodeOptions::AvifRavif(cfg) => encode_avif(image, metadata, cfg),
46 #[cfg(feature = "jxl-encode")]
47 EncodeOptions::JxlZune(cfg) => encode_jxl(image, metadata, cfg),
48 #[cfg(feature = "jxl-encode-libjxl")]
49 EncodeOptions::JxlLibjxl(cfg) => encode_jxl_libjxl(image, metadata, cfg),
50 #[cfg(feature = "dng-encode")]
51 EncodeOptions::Dng(cfg) => {
52 let mut buf = std::io::Cursor::new(Vec::new());
53 super::dng_export::export_dng_to_writer(&mut buf, image, metadata, cfg)?;
54 Ok(buf.into_inner())
55 }
56 #[allow(unreachable_patterns)]
57 _ => Err(RawError::Unsupported(
58 "This encode format is not available with the current feature flags.".to_string(),
59 )),
60 }
61}
62
63pub fn encode_rgb_image_to_writer<W: std::io::Write>(
67 image: &RgbImage,
68 metadata: &ImageMetadata,
69 writer: &mut W,
70 encode_options: &EncodeOptions,
71) -> RawResult<()> {
72 let bytes = encode_rgb_image_to_vec(image, metadata, encode_options)?;
73 writer.write_all(&bytes)?;
74 Ok(())
75}
76
77pub fn encode_rgb_image(
81 image: &RgbImage,
82 metadata: &ImageMetadata,
83 path: &Path,
84 encode_options: &EncodeOptions,
85) -> RawResult<()> {
86 let bytes = encode_rgb_image_to_vec(image, metadata, encode_options)?;
87 std::fs::write(path, bytes)?;
88 Ok(())
89}
90
91#[cfg(any_standard_encode)]
95#[allow(dead_code)] fn pack_rgb8(image: &RgbImage) -> Vec<u8> {
97 image.data.iter().map(|&p| (p >> 8) as u8).collect()
98}
99
100#[cfg(any_standard_encode)]
105#[allow(dead_code)] fn check_8bit_backend(bit_depth: BitDepth, format: &'static str) -> Result<(), EncodeError> {
107 match bit_depth {
108 BitDepth::Eight | BitDepth::Sixteen => Ok(()),
109 _ => Err(EncodeError::UnsupportedBitDepth {
110 format,
111 requested: bit_depth,
112 }),
113 }
114}
115
116#[cfg(feature = "png-encode")]
119fn encode_png(
120 image: &RgbImage,
121 metadata: &ImageMetadata,
122 cfg: &super::export::ZunePngEncodeConfig,
123) -> RawResult<Vec<u8>> {
124 use zune_core::colorspace::ColorSpace as ZuneColorSpace;
125 use zune_core::options::EncoderOptions;
126 use zune_png::PngEncoder;
127
128 let (data_bytes, depth) = match cfg.common.bit_depth {
130 BitDepth::Eight => (pack_rgb8(image), zune_core::bit_depth::BitDepth::Eight),
131 BitDepth::Sixteen => {
132 let mut bytes = Vec::with_capacity(image.data.len() * 2);
133 for &pixel in &image.data {
134 bytes.extend_from_slice(&pixel.to_be_bytes());
135 }
136 (bytes, zune_core::bit_depth::BitDepth::Sixteen)
137 }
138 other => {
139 return Err(RawError::Encode(EncodeError::UnsupportedBitDepth {
140 format: "PNG",
141 requested: other,
142 }));
143 }
144 };
145
146 let options = EncoderOptions::default()
147 .set_width(image.width() as usize)
148 .set_height(image.height() as usize)
149 .set_colorspace(ZuneColorSpace::RGB)
150 .set_depth(depth);
151
152 let mut encoder = PngEncoder::new(&data_bytes, options);
153 let mut output = Vec::new();
154 encoder.encode(&mut output).map_err(|e| {
155 RawError::Encode(EncodeError::Encoding {
156 format: "PNG",
157 message: format!("PNG encoding error: {e:?}"),
158 })
159 })?;
160
161 let m = &cfg.common.metadata;
162 if m.embed_exif || m.embed_icc || m.embed_xmp {
163 use crate::metadata::exif::ExifBuilder;
164 use crate::metadata::icc::IccProfile;
165 use img_parts::png::{Png, PngChunk};
166 use img_parts::{Bytes, ImageEXIF, ImageICC};
167
168 match Png::from_bytes(Bytes::from(output.clone())) {
169 Ok(mut png) => {
170 if m.embed_icc {
171 let icc = IccProfile::srgb();
172 png.set_icc_profile(Some(Bytes::from(icc.as_bytes().to_vec())));
173 }
174 if m.embed_exif {
175 let exif_builder = ExifBuilder::new(metadata);
176 match exif_builder.build_bytes() {
177 Ok(bytes) => png.set_exif(Some(Bytes::from(bytes))),
178 Err(e) => tracing::warn!("Failed to embed EXIF in PNG: {e}"),
179 }
180 }
181 if m.embed_xmp
182 && let Some(xmp_data) = &metadata.xmp
183 {
184 let mut chunk_data = Vec::with_capacity(22 + xmp_data.len());
187 chunk_data.extend_from_slice(b"XML:com.adobe.xmp\0");
188 chunk_data.push(0); chunk_data.push(0); chunk_data.push(0); chunk_data.push(0); chunk_data.extend_from_slice(xmp_data);
193 let chunk = PngChunk::new(*b"iTXt", Bytes::from(chunk_data));
194 let idx = png.chunks().len().saturating_sub(1);
195 png.chunks_mut().insert(idx, chunk);
196 }
197 use std::io::Cursor;
198 let mut buf = Cursor::new(Vec::new());
199 match png.encoder().write_to(&mut buf) {
200 Ok(_) => output = buf.into_inner(),
201 Err(e) => tracing::warn!("Failed to write PNG with metadata: {e}"),
202 }
203 }
204 Err(e) => tracing::warn!("Failed to parse PNG for metadata embedding: {e}"),
205 }
206 }
207
208 Ok(output)
209}
210
211#[cfg(feature = "jpeg-encode")]
214fn encode_jpeg(
215 image: &RgbImage,
216 metadata: &ImageMetadata,
217 cfg: &super::export::JpegEncEncodeConfig,
218) -> RawResult<Vec<u8>> {
219 use crate::metadata::exif::ExifBuilder;
220 use crate::metadata::icc::IccProfile;
221 use jpeg_encoder::{ColorType, Encoder};
222
223 check_8bit_backend(cfg.common.bit_depth, "JPEG")?;
224 let data_8bit = pack_rgb8(image);
225
226 let quality = if cfg.quality == 0 { 90 } else { cfg.quality };
227 let mut jpeg_buf = Vec::new();
228 let encoder = Encoder::new(&mut jpeg_buf, quality);
229 encoder.encode(
230 &data_8bit,
231 image.width() as u16,
232 image.height() as u16,
233 ColorType::Rgb,
234 )?;
235
236 let m = &cfg.common.metadata;
237 if m.embed_exif {
238 let exif_builder = ExifBuilder::new(metadata);
239 match exif_builder.append_to_jpeg(jpeg_buf.clone()) {
240 Ok(data) => jpeg_buf = data,
241 Err(e) => tracing::warn!("Failed to embed EXIF in JPEG: {e}"),
242 }
243 }
244 if m.embed_icc {
245 match IccProfile::srgb().append_to_jpeg(jpeg_buf.clone()) {
246 Ok(data) => jpeg_buf = data,
247 Err(e) => tracing::warn!("Failed to embed ICC in JPEG: {e}"),
248 }
249 }
250 if m.embed_xmp
251 && let Some(xmp_data) = &metadata.xmp
252 {
253 use crate::metadata::xmp::append_xmp_to_jpeg;
254 match append_xmp_to_jpeg(xmp_data, jpeg_buf.clone()) {
255 Ok(data) => jpeg_buf = data,
256 Err(e) => tracing::warn!("Failed to embed XMP in JPEG: {e}"),
257 }
258 }
259
260 Ok(jpeg_buf)
261}
262
263#[cfg(feature = "webp-encode")]
266fn encode_webp(
267 image: &RgbImage,
268 metadata: &ImageMetadata,
269 cfg: &super::export::LibwebpEncodeConfig,
270) -> RawResult<Vec<u8>> {
271 use crate::codecs::webp::{build_webp_config, encode_webp_rgb, mux_webp};
272 use crate::metadata::exif::ExifBuilder;
273 use crate::metadata::icc::IccProfile;
274
275 check_8bit_backend(cfg.common.bit_depth, "WebP")?;
276
277 let lossless = cfg.mode == WebPMode::Lossless;
278 let config = build_webp_config(lossless, cfg.quality, cfg.method, cfg.near_lossless)
279 .map_err(|e| RawError::Encode(EncodeError::WebP(e)))?;
280
281 let data_8bit = pack_rgb8(image);
282 let encoded = encode_webp_rgb(&data_8bit, image.width(), image.height(), &config)
283 .map_err(|e| RawError::Encode(EncodeError::WebP(e)))?;
284
285 let m = &cfg.common.metadata;
286 let exif_bytes = if m.embed_exif {
287 match ExifBuilder::new(metadata).build_bytes() {
288 Ok(bytes) => Some(bytes),
289 Err(e) => {
290 tracing::warn!("Failed to build EXIF for WebP: {e}");
291 None
292 }
293 }
294 } else {
295 None
296 };
297 let icc_bytes = if m.embed_icc {
298 Some(IccProfile::srgb().as_bytes().to_vec())
299 } else {
300 None
301 };
302 let xmp_bytes = if m.embed_xmp {
303 metadata.xmp.as_deref()
304 } else {
305 None
306 };
307
308 mux_webp(
309 &encoded,
310 exif_bytes.as_deref(),
311 icc_bytes.as_deref(),
312 xmp_bytes,
313 )
314 .map_err(|e| RawError::Encode(EncodeError::WebP(e)))
315}
316
317#[cfg(feature = "avif-encode")]
320fn encode_avif(
321 image: &RgbImage,
322 metadata: &ImageMetadata,
323 cfg: &super::export::RavifEncodeConfig,
324) -> RawResult<Vec<u8>> {
325 use crate::metadata::exif::ExifBuilder;
326 use crate::metadata::icc::IccProfile;
327 use crate::metadata::xmp::append_xmp_to_avif;
328 use ravif::{Encoder, Img, RGBA8};
329
330 check_8bit_backend(cfg.common.bit_depth, "AVIF")?;
331
332 let rgba_data: Vec<RGBA8> = image
333 .data
334 .chunks_exact(3)
335 .map(|rgb| {
336 RGBA8::new(
337 (rgb[0] >> 8) as u8,
338 (rgb[1] >> 8) as u8,
339 (rgb[2] >> 8) as u8,
340 255,
341 )
342 })
343 .collect();
344
345 let img = Img::new(
346 rgba_data.as_slice(),
347 image.width() as usize,
348 image.height() as usize,
349 );
350
351 let encoder = Encoder::new()
352 .with_quality(cfg.quality as f32)
353 .with_speed(cfg.speed);
354
355 let result = encoder.encode_rgba(img).map_err(|e| {
358 RawError::Encode(EncodeError::Encoding {
359 format: "AVIF",
360 message: format!("{e:?}"),
361 })
362 })?;
363 let mut avif_bytes = result.avif_file;
364
365 let m = &cfg.common.metadata;
366 if m.embed_icc {
367 match IccProfile::srgb().append_to_avif(avif_bytes.clone()) {
368 Ok(data) => avif_bytes = data,
369 Err(e) => tracing::warn!("Failed to embed ICC in AVIF: {e}"),
370 }
371 }
372 if m.embed_exif {
373 match ExifBuilder::new(metadata).append_to_avif(avif_bytes.clone()) {
374 Ok(data) => avif_bytes = data,
375 Err(e) => tracing::warn!("Failed to embed EXIF in AVIF: {e}"),
376 }
377 }
378 if m.embed_xmp
379 && let Some(xmp_data) = &metadata.xmp
380 {
381 match append_xmp_to_avif(xmp_data, avif_bytes.clone()) {
382 Ok(data) => avif_bytes = data,
383 Err(e) => tracing::warn!("Failed to embed XMP in AVIF: {e}"),
384 }
385 }
386
387 Ok(avif_bytes)
388}
389
390#[cfg(feature = "jxl-encode")]
393fn encode_jxl(
394 image: &RgbImage,
395 metadata: &ImageMetadata,
396 cfg: &super::export::ZuneJxlEncodeConfig,
397) -> RawResult<Vec<u8>> {
398 use crate::metadata::exif::ExifBuilder;
399 use crate::metadata::icc::IccProfile;
400 use crate::metadata::xmp::append_xmp_to_jxl;
401 use zune_core::colorspace::ColorSpace as ZuneColorSpace;
402 use zune_core::options::EncoderOptions;
403 use zune_jpegxl::JxlSimpleEncoder;
404
405 check_8bit_backend(cfg.common.bit_depth, "JXL")?;
406 let data_8bit = pack_rgb8(image);
407
408 let quality = if cfg.quality == 0.0 {
409 100
410 } else {
411 cfg.quality as u8
412 };
413 let enc_options = EncoderOptions::default()
414 .set_width(image.width() as usize)
415 .set_height(image.height() as usize)
416 .set_colorspace(ZuneColorSpace::RGB)
417 .set_quality(quality);
418
419 let encoder = JxlSimpleEncoder::new(&data_8bit, enc_options);
420 let mut encoded: Vec<u8> = Vec::new();
421 encoder.encode(&mut encoded).map_err(|e| {
422 RawError::Encode(EncodeError::Encoding {
423 format: "JXL",
424 message: format!("{e:?}"),
425 })
426 })?;
427
428 let m = &cfg.common.metadata;
429 if m.embed_exif {
430 match ExifBuilder::new(metadata).append_to_jxl(encoded.clone()) {
431 Ok(data) => encoded = data,
432 Err(e) => tracing::warn!("Failed to embed EXIF in JXL: {e}"),
433 }
434 }
435 if m.embed_icc {
436 match IccProfile::srgb().append_to_jxl(encoded.clone()) {
437 Ok(data) => encoded = data,
438 Err(e) => tracing::warn!("Failed to embed ICC in JXL: {e}"),
439 }
440 }
441 if m.embed_xmp
442 && let Some(xmp_data) = &metadata.xmp
443 {
444 match append_xmp_to_jxl(xmp_data, encoded.clone()) {
445 Ok(data) => encoded = data,
446 Err(e) => tracing::warn!("Failed to embed XMP in JXL: {e}"),
447 }
448 }
449
450 Ok(encoded)
451}
452
453#[cfg(feature = "jxl-encode-libjxl")]
456fn encode_jxl_libjxl(
457 image: &RgbImage,
458 metadata: &ImageMetadata,
459 cfg: &super::export::LibjxlEncodeConfig,
460) -> RawResult<Vec<u8>> {
461 use super::export::{LibjxlColorTransform, LibjxlModular};
462 use crate::codecs::jxl_libjxl::{self, JxlEncodeParams};
463 use crate::metadata::exif::ExifBuilder;
464 use crate::metadata::icc::IccProfile;
465 use crate::metadata::xmp::append_xmp_to_jxl;
466
467 let (samples, bits_per_sample) = match cfg.common.bit_depth {
471 BitDepth::Eight => (pack_rgb8(image), 8u32),
472 BitDepth::Sixteen => {
473 let mut bytes = Vec::with_capacity(image.data.len() * 2);
474 for &sample in &image.data {
475 bytes.extend_from_slice(&sample.to_ne_bytes());
476 }
477 (bytes, 16u32)
478 }
479 other => {
480 return Err(RawError::Encode(EncodeError::UnsupportedBitDepth {
481 format: "JXL",
482 requested: other,
483 }));
484 }
485 };
486
487 let modular = match cfg.modular {
489 LibjxlModular::Auto => -1,
490 LibjxlModular::VarDct => 0,
491 LibjxlModular::Modular => 1,
492 };
493 let color_transform = match cfg.color_transform {
494 LibjxlColorTransform::Auto => -1,
495 LibjxlColorTransform::Xyb => 0,
496 LibjxlColorTransform::None => 1,
497 LibjxlColorTransform::YCbCr => 2,
498 };
499 let tri = |o: Option<bool>| match o {
500 None => -1,
501 Some(false) => 0,
502 Some(true) => 1,
503 };
504
505 let params = JxlEncodeParams {
506 distance: cfg.distance,
507 quality: cfg.quality,
508 lossless: cfg.lossless,
509 effort: i64::from(cfg.effort),
510 brotli_effort: i64::from(cfg.brotli_effort),
511 decoding_speed: i64::from(cfg.decoding_speed),
512 progressive: cfg.progressive,
513 modular,
514 color_transform,
515 epf: i64::from(cfg.epf),
516 gaborish: tri(cfg.gaborish),
517 noise: tri(cfg.noise),
518 dots: tri(cfg.dots),
519 patches: tri(cfg.patches),
520 photon_noise_iso: cfg.photon_noise_iso,
521 resampling: i64::from(cfg.resampling),
522 use_container: cfg.use_container,
523 codestream_level: i32::from(cfg.codestream_level),
524 extra_int_options: cfg.extra_int_options.clone(),
525 extra_float_options: cfg.extra_float_options.clone(),
526 };
527
528 let mut encoded = jxl_libjxl::encode(
529 &samples,
530 image.width(),
531 image.height(),
532 bits_per_sample,
533 ¶ms,
534 )
535 .map_err(|e| RawError::Encode(EncodeError::Jxl(e)))?;
536
537 let m = &cfg.common.metadata;
539 if m.embed_exif {
540 match ExifBuilder::new(metadata).append_to_jxl(encoded.clone()) {
541 Ok(data) => encoded = data,
542 Err(e) => tracing::warn!("Failed to embed EXIF in JXL: {e}"),
543 }
544 }
545 if m.embed_icc {
546 match IccProfile::srgb().append_to_jxl(encoded.clone()) {
547 Ok(data) => encoded = data,
548 Err(e) => tracing::warn!("Failed to embed ICC in JXL: {e}"),
549 }
550 }
551 if m.embed_xmp
552 && let Some(xmp_data) = &metadata.xmp
553 {
554 match append_xmp_to_jxl(xmp_data, encoded.clone()) {
555 Ok(data) => encoded = data,
556 Err(e) => tracing::warn!("Failed to embed XMP in JXL: {e}"),
557 }
558 }
559
560 Ok(encoded)
561}