//! TRACES: FR-EXP-1 | FR-EXP-8 //! The encoders. //! //! All four write RGB, not RGBA. The pipeline produces an opaque frame — no //! operation makes a pixel transparent, and the shader writes 1.0 into alpha //! unconditionally — so a fourth channel would be a third more bytes carrying //! the same value in every pixel, and a PNG that some tools then treat as //! having meaningful transparency. //! //! # Metadata //! //! Nothing is written. `strip_location` defaults to on (FR-EXP-8) and this //! satisfies it in the strongest possible way: there is no EXIF block, so //! there is no GPS tag, no serial number, and no lens history in the file //! that leaves the machine. //! //! The other half of FR-EXP-8 — *retaining* camera and copyright metadata //! when the user asks for it — is not implemented, and cannot be faked by //! omission. It needs the source's EXIF carried through `dr-decode` and //! re-serialised here, which is a piece of work in its own right and belongs //! with the batch-export path that would make it worth having. use dr_types::{ExportFormat, ExportSettings}; use crate::ExportError; /// Encode a resized, sharpened RGBA buffer to the requested format. pub fn encode( rgba: &[u8], width: u32, height: u32, settings: &ExportSettings, ) -> Result, ExportError> { match settings.format { ExportFormat::Jpeg => jpeg(rgba, width, height, settings.quality), ExportFormat::Png => png(rgba, width, height), ExportFormat::Tiff8 => tiff8(rgba, width, height), ExportFormat::Tiff16 => tiff16(rgba, width, height), other => Err(ExportError::FormatUnsupported(other)), } } /// Drop alpha, which the pipeline never varies. fn rgb(rgba: &[u8]) -> Vec { let mut out = Vec::with_capacity(rgba.len() / 4 * 3); for px in rgba.chunks_exact(4) { out.extend_from_slice(&px[..3]); } out } fn jpeg(rgba: &[u8], width: u32, height: u32, quality: u8) -> Result, ExportError> { let mut bytes = Vec::new(); let encoder = jpeg_encoder::Encoder::new(&mut bytes, quality); encoder .encode( &rgb(rgba), width as u16, height as u16, jpeg_encoder::ColorType::Rgb, ) .map_err(|e| ExportError::Encode(e.to_string()))?; Ok(bytes) } fn png(rgba: &[u8], width: u32, height: u32) -> Result, ExportError> { let mut bytes = Vec::new(); { let mut encoder = png::Encoder::new(&mut bytes, width, height); encoder.set_color(png::ColorType::Rgb); encoder.set_depth(png::BitDepth::Eight); let mut writer = encoder .write_header() .map_err(|e| ExportError::Encode(e.to_string()))?; writer .write_image_data(&rgb(rgba)) .map_err(|e| ExportError::Encode(e.to_string()))?; writer .finish() .map_err(|e| ExportError::Encode(e.to_string()))?; } Ok(bytes) } fn tiff8(rgba: &[u8], width: u32, height: u32) -> Result, ExportError> { use tiff::encoder::{colortype, TiffEncoder}; let mut bytes = std::io::Cursor::new(Vec::new()); let mut encoder = TiffEncoder::new(&mut bytes).map_err(|e| ExportError::Encode(e.to_string()))?; encoder .write_image::(width, height, &rgb(rgba)) .map_err(|e| ExportError::Encode(e.to_string()))?; Ok(bytes.into_inner()) } /// 16-bit TIFF, for work continuing in another editor. /// /// **Honest about what it carries.** The adjust pass renders to an 8-bit /// target (`AdjustPass::FORMAT` is `Rgba8Unorm`, and the generated shader /// declares `texture_storage_2d`), so the samples widened /// here hold eight bits of information in a sixteen-bit container. The file /// is a correct 16-bit TIFF and will round-trip through any editor without /// further loss — but it does not resurrect precision the pipeline already /// quantised away. /// /// Making it mean what it says is a pipeline change rather than an encoder /// one: the composer has to be told what format to write, and export has to /// ask for the wide one (FR-EXP-9). Until then this is a container promotion, /// which is still the right thing to hand an editor that works in 16-bit. fn tiff16(rgba: &[u8], width: u32, height: u32) -> Result, ExportError> { use tiff::encoder::{colortype, TiffEncoder}; // `x * 257` rather than `x << 8`: it maps 255 to 65535 exactly, where the // shift maps it to 65280 and makes white slightly grey. let wide: Vec = rgb(rgba).iter().map(|&v| u16::from(v) * 257).collect(); let mut bytes = std::io::Cursor::new(Vec::new()); let mut encoder = TiffEncoder::new(&mut bytes).map_err(|e| ExportError::Encode(e.to_string()))?; encoder .write_image::(width, height, &wide) .map_err(|e| ExportError::Encode(e.to_string()))?; Ok(bytes.into_inner()) } #[cfg(test)] mod tests { use super::*; #[test] fn alpha_is_dropped_before_encoding() { let rgba = vec![1, 2, 3, 255, 4, 5, 6, 255]; assert_eq!(rgb(&rgba), vec![1, 2, 3, 4, 5, 6]); } #[test] fn white_widens_to_full_scale_not_almost() { // The bug a left-shift introduces: 255 << 8 is 65280, so pure white // comes out a quarter of a percent grey in every 16-bit export. assert_eq!(u16::from(255u8) * 257, u16::MAX); assert_eq!(u16::from(0u8) * 257, 0); // And the midpoint stays the midpoint. assert_eq!(u16::from(128u8) * 257, 32896); } #[test] fn a_png_round_trips_its_pixels_exactly() { // PNG is lossless, so this is a real end-to-end check that the buffer // reaching the encoder is the one we think it is — channel order // included, which a size assertion would not catch. let rgba: Vec = vec![ 255, 0, 0, 255, // red 0, 255, 0, 255, // green 0, 0, 255, 255, // blue 10, 20, 30, 255, ]; let bytes = png(&rgba, 2, 2).unwrap(); let decoder = png::Decoder::new(std::io::Cursor::new(&bytes)); let mut reader = decoder.read_info().unwrap(); let mut out = vec![0; reader.output_buffer_size().unwrap()]; let info = reader.next_frame(&mut out).unwrap(); assert_eq!((info.width, info.height), (2, 2)); assert_eq!(info.color_type, png::ColorType::Rgb); assert_eq!(&out[..12], &[255, 0, 0, 0, 255, 0, 0, 0, 255, 10, 20, 30]); } }