From 1b6a04ca5504955c571d1c97504fb45ea0befee4 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Mon, 8 Jan 2024 01:21:28 +0400 Subject: Initial vendor packages Signed-off-by: Valentin Popov --- vendor/image/src/codecs/hdr/decoder.rs | 1033 ++++++++++++++++++++++++++++++++ vendor/image/src/codecs/hdr/encoder.rs | 433 +++++++++++++ vendor/image/src/codecs/hdr/mod.rs | 15 + 3 files changed, 1481 insertions(+) create mode 100644 vendor/image/src/codecs/hdr/decoder.rs create mode 100644 vendor/image/src/codecs/hdr/encoder.rs create mode 100644 vendor/image/src/codecs/hdr/mod.rs (limited to 'vendor/image/src/codecs/hdr') diff --git a/vendor/image/src/codecs/hdr/decoder.rs b/vendor/image/src/codecs/hdr/decoder.rs new file mode 100644 index 0000000..8329d57 --- /dev/null +++ b/vendor/image/src/codecs/hdr/decoder.rs @@ -0,0 +1,1033 @@ +use crate::Primitive; +use num_traits::identities::Zero; +#[cfg(test)] +use std::borrow::Cow; +use std::convert::TryFrom; +use std::io::{self, BufRead, Cursor, Read, Seek}; +use std::iter::Iterator; +use std::marker::PhantomData; +use std::num::{ParseFloatError, ParseIntError}; +use std::path::Path; +use std::{error, fmt, mem}; + +use crate::color::{ColorType, Rgb}; +use crate::error::{ + DecodingError, ImageError, ImageFormatHint, ImageResult, ParameterError, ParameterErrorKind, + UnsupportedError, UnsupportedErrorKind, +}; +use crate::image::{self, ImageDecoder, ImageDecoderRect, ImageFormat, Progress}; + +/// Errors that can occur during decoding and parsing of a HDR image +#[derive(Debug, Clone, PartialEq, Eq)] +enum DecoderError { + /// HDR's "#?RADIANCE" signature wrong or missing + RadianceHdrSignatureInvalid, + /// EOF before end of header + TruncatedHeader, + /// EOF instead of image dimensions + TruncatedDimensions, + + /// A value couldn't be parsed + UnparsableF32(LineType, ParseFloatError), + /// A value couldn't be parsed + UnparsableU32(LineType, ParseIntError), + /// Not enough numbers in line + LineTooShort(LineType), + + /// COLORCORR contains too many numbers in strict mode + ExtraneousColorcorrNumbers, + + /// Dimensions line had too few elements + DimensionsLineTooShort(usize, usize), + /// Dimensions line had too many elements + DimensionsLineTooLong(usize), + + /// The length of a scanline (1) wasn't a match for the specified length (2) + WrongScanlineLength(usize, usize), + /// First pixel of a scanline is a run length marker + FirstPixelRlMarker, +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DecoderError::RadianceHdrSignatureInvalid => { + f.write_str("Radiance HDR signature not found") + } + DecoderError::TruncatedHeader => f.write_str("EOF in header"), + DecoderError::TruncatedDimensions => f.write_str("EOF in dimensions line"), + DecoderError::UnparsableF32(line, pe) => { + f.write_fmt(format_args!("Cannot parse {} value as f32: {}", line, pe)) + } + DecoderError::UnparsableU32(line, pe) => { + f.write_fmt(format_args!("Cannot parse {} value as u32: {}", line, pe)) + } + DecoderError::LineTooShort(line) => { + f.write_fmt(format_args!("Not enough numbers in {}", line)) + } + DecoderError::ExtraneousColorcorrNumbers => f.write_str("Extra numbers in COLORCORR"), + DecoderError::DimensionsLineTooShort(elements, expected) => f.write_fmt(format_args!( + "Dimensions line too short: have {} elements, expected {}", + elements, expected + )), + DecoderError::DimensionsLineTooLong(expected) => f.write_fmt(format_args!( + "Dimensions line too long, expected {} elements", + expected + )), + DecoderError::WrongScanlineLength(len, expected) => f.write_fmt(format_args!( + "Wrong length of decoded scanline: got {}, expected {}", + len, expected + )), + DecoderError::FirstPixelRlMarker => { + f.write_str("First pixel of a scanline shouldn't be run length marker") + } + } + } +} + +impl From for ImageError { + fn from(e: DecoderError) -> ImageError { + ImageError::Decoding(DecodingError::new(ImageFormat::Hdr.into(), e)) + } +} + +impl error::Error for DecoderError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + DecoderError::UnparsableF32(_, err) => Some(err), + DecoderError::UnparsableU32(_, err) => Some(err), + _ => None, + } + } +} + +/// Lines which contain parsable data that can fail +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +enum LineType { + Exposure, + Pixaspect, + Colorcorr, + DimensionsHeight, + DimensionsWidth, +} + +impl fmt::Display for LineType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + LineType::Exposure => "EXPOSURE", + LineType::Pixaspect => "PIXASPECT", + LineType::Colorcorr => "COLORCORR", + LineType::DimensionsHeight => "height dimension", + LineType::DimensionsWidth => "width dimension", + }) + } +} + +/// Adapter to conform to `ImageDecoder` trait +#[derive(Debug)] +pub struct HdrAdapter { + inner: Option>, + // data: Option>, + meta: HdrMetadata, +} + +impl HdrAdapter { + /// Creates adapter + pub fn new(r: R) -> ImageResult> { + let decoder = HdrDecoder::new(r)?; + let meta = decoder.metadata(); + Ok(HdrAdapter { + inner: Some(decoder), + meta, + }) + } + + /// Allows reading old Radiance HDR images + pub fn new_nonstrict(r: R) -> ImageResult> { + let decoder = HdrDecoder::with_strictness(r, false)?; + let meta = decoder.metadata(); + Ok(HdrAdapter { + inner: Some(decoder), + meta, + }) + } + + /// Read the actual data of the image, and store it in Self::data. + fn read_image_data(&mut self, buf: &mut [u8]) -> ImageResult<()> { + assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); + match self.inner.take() { + Some(decoder) => { + let img: Vec> = decoder.read_image_ldr()?; + for (i, Rgb(data)) in img.into_iter().enumerate() { + buf[(i * 3)..][..3].copy_from_slice(&data); + } + + Ok(()) + } + None => Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::NoMoreData, + ))), + } + } +} + +/// Wrapper struct around a `Cursor>` +pub struct HdrReader(Cursor>, PhantomData); +impl Read for HdrReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.0.read(buf) + } + fn read_to_end(&mut self, buf: &mut Vec) -> io::Result { + if self.0.position() == 0 && buf.is_empty() { + mem::swap(buf, self.0.get_mut()); + Ok(buf.len()) + } else { + self.0.read_to_end(buf) + } + } +} + +impl<'a, R: 'a + BufRead> ImageDecoder<'a> for HdrAdapter { + type Reader = HdrReader; + + fn dimensions(&self) -> (u32, u32) { + (self.meta.width, self.meta.height) + } + + fn color_type(&self) -> ColorType { + ColorType::Rgb8 + } + + fn into_reader(self) -> ImageResult { + Ok(HdrReader( + Cursor::new(image::decoder_to_vec(self)?), + PhantomData, + )) + } + + fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { + self.read_image_data(buf) + } +} + +impl<'a, R: 'a + BufRead + Seek> ImageDecoderRect<'a> for HdrAdapter { + fn read_rect_with_progress( + &mut self, + x: u32, + y: u32, + width: u32, + height: u32, + buf: &mut [u8], + progress_callback: F, + ) -> ImageResult<()> { + image::load_rect( + x, + y, + width, + height, + buf, + progress_callback, + self, + |_, _| unreachable!(), + |s, buf| s.read_image_data(buf), + ) + } +} + +/// Radiance HDR file signature +pub const SIGNATURE: &[u8] = b"#?RADIANCE"; +const SIGNATURE_LENGTH: usize = 10; + +/// An Radiance HDR decoder +#[derive(Debug)] +pub struct HdrDecoder { + r: R, + width: u32, + height: u32, + meta: HdrMetadata, +} + +/// Refer to [wikipedia](https://en.wikipedia.org/wiki/RGBE_image_format) +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Rgbe8Pixel { + /// Color components + pub c: [u8; 3], + /// Exponent + pub e: u8, +} + +/// Creates `Rgbe8Pixel` from components +pub fn rgbe8(r: u8, g: u8, b: u8, e: u8) -> Rgbe8Pixel { + Rgbe8Pixel { c: [r, g, b], e } +} + +impl Rgbe8Pixel { + /// Converts `Rgbe8Pixel` into `Rgb` linearly + #[inline] + pub fn to_hdr(self) -> Rgb { + if self.e == 0 { + Rgb([0.0, 0.0, 0.0]) + } else { + // let exp = f32::ldexp(1., self.e as isize - (128 + 8)); // unstable + let exp = f32::exp2(>::from(self.e) - (128.0 + 8.0)); + Rgb([ + exp * >::from(self.c[0]), + exp * >::from(self.c[1]), + exp * >::from(self.c[2]), + ]) + } + } + + /// Converts `Rgbe8Pixel` into `Rgb` with scale=1 and gamma=2.2 + /// + /// color_ldr = (color_hdr*scale)gamma + /// + /// # Panic + /// + /// Panics when `T::max_value()` cannot be represented as f32. + #[inline] + pub fn to_ldr(self) -> Rgb { + self.to_ldr_scale_gamma(1.0, 2.2) + } + + /// Converts `Rgbe8Pixel` into `Rgb` using provided scale and gamma + /// + /// color_ldr = (color_hdr*scale)gamma + /// + /// # Panic + /// + /// Panics when `T::max_value()` cannot be represented as f32. + /// Panics when scale or gamma is NaN + #[inline] + pub fn to_ldr_scale_gamma(self, scale: f32, gamma: f32) -> Rgb { + let Rgb(data) = self.to_hdr(); + let (r, g, b) = (data[0], data[1], data[2]); + #[inline] + fn sg(v: f32, scale: f32, gamma: f32) -> T { + let t_max = T::max_value(); + // Disassembly shows that t_max_f32 is compiled into constant + let t_max_f32: f32 = num_traits::NumCast::from(t_max) + .expect("to_ldr_scale_gamma: maximum value of type is not representable as f32"); + let fv = f32::powf(v * scale, gamma) * t_max_f32 + 0.5; + if fv < 0.0 { + T::zero() + } else if fv > t_max_f32 { + t_max + } else { + num_traits::NumCast::from(fv) + .expect("to_ldr_scale_gamma: cannot convert f32 to target type. NaN?") + } + } + Rgb([ + sg(r, scale, gamma), + sg(g, scale, gamma), + sg(b, scale, gamma), + ]) + } +} + +impl HdrDecoder { + /// Reads Radiance HDR image header from stream `r` + /// if the header is valid, creates HdrDecoder + /// strict mode is enabled + pub fn new(reader: R) -> ImageResult> { + HdrDecoder::with_strictness(reader, true) + } + + /// Reads Radiance HDR image header from stream `reader`, + /// if the header is valid, creates `HdrDecoder`. + /// + /// strict enables strict mode + /// + /// Warning! Reading wrong file in non-strict mode + /// could consume file size worth of memory in the process. + pub fn with_strictness(mut reader: R, strict: bool) -> ImageResult> { + let mut attributes = HdrMetadata::new(); + + { + // scope to make borrowck happy + let r = &mut reader; + if strict { + let mut signature = [0; SIGNATURE_LENGTH]; + r.read_exact(&mut signature)?; + if signature != SIGNATURE { + return Err(DecoderError::RadianceHdrSignatureInvalid.into()); + } // no else + // skip signature line ending + read_line_u8(r)?; + } else { + // Old Radiance HDR files (*.pic) don't use signature + // Let them be parsed in non-strict mode + } + // read header data until empty line + loop { + match read_line_u8(r)? { + None => { + // EOF before end of header + return Err(DecoderError::TruncatedHeader.into()); + } + Some(line) => { + if line.is_empty() { + // end of header + break; + } else if line[0] == b'#' { + // line[0] will not panic, line.len() == 0 is false here + // skip comments + continue; + } // no else + // process attribute line + let line = String::from_utf8_lossy(&line[..]); + attributes.update_header_info(&line, strict)?; + } // <= Some(line) + } // match read_line_u8() + } // loop + } // scope to end borrow of reader + // parse dimensions + let (width, height) = match read_line_u8(&mut reader)? { + None => { + // EOF instead of image dimensions + return Err(DecoderError::TruncatedDimensions.into()); + } + Some(dimensions) => { + let dimensions = String::from_utf8_lossy(&dimensions[..]); + parse_dimensions_line(&dimensions, strict)? + } + }; + + // color type is always rgb8 + if crate::utils::check_dimension_overflow(width, height, ColorType::Rgb8.bytes_per_pixel()) + { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Hdr.into(), + UnsupportedErrorKind::GenericFeature(format!( + "Image dimensions ({}x{}) are too large", + width, height + )), + ), + )); + } + + Ok(HdrDecoder { + r: reader, + + width, + height, + meta: HdrMetadata { + width, + height, + ..attributes + }, + }) + } // end with_strictness + + /// Returns file metadata. Refer to `HdrMetadata` for details. + pub fn metadata(&self) -> HdrMetadata { + self.meta.clone() + } + + /// Consumes decoder and returns a vector of RGBE8 pixels + pub fn read_image_native(mut self) -> ImageResult> { + // Don't read anything if image is empty + if self.width == 0 || self.height == 0 { + return Ok(vec![]); + } + // expression self.width > 0 && self.height > 0 is true from now to the end of this method + let pixel_count = self.width as usize * self.height as usize; + let mut ret = vec![Default::default(); pixel_count]; + for chunk in ret.chunks_mut(self.width as usize) { + read_scanline(&mut self.r, chunk)?; + } + Ok(ret) + } + + /// Consumes decoder and returns a vector of transformed pixels + pub fn read_image_transform T>( + mut self, + f: F, + output_slice: &mut [T], + ) -> ImageResult<()> { + assert_eq!( + output_slice.len(), + self.width as usize * self.height as usize + ); + + // Don't read anything if image is empty + if self.width == 0 || self.height == 0 { + return Ok(()); + } + + let chunks_iter = output_slice.chunks_mut(self.width as usize); + + let mut buf = vec![Default::default(); self.width as usize]; + for chunk in chunks_iter { + // read_scanline overwrites the entire buffer or returns an Err, + // so not resetting the buffer here is ok. + read_scanline(&mut self.r, &mut buf[..])?; + for (dst, &pix) in chunk.iter_mut().zip(buf.iter()) { + *dst = f(pix); + } + } + Ok(()) + } + + /// Consumes decoder and returns a vector of `Rgb` pixels. + /// scale = 1, gamma = 2.2 + pub fn read_image_ldr(self) -> ImageResult>> { + let mut ret = vec![Rgb([0, 0, 0]); self.width as usize * self.height as usize]; + self.read_image_transform(|pix| pix.to_ldr(), &mut ret[..])?; + Ok(ret) + } + + /// Consumes decoder and returns a vector of `Rgb` pixels. + /// + pub fn read_image_hdr(self) -> ImageResult>> { + let mut ret = vec![Rgb([0.0, 0.0, 0.0]); self.width as usize * self.height as usize]; + self.read_image_transform(|pix| pix.to_hdr(), &mut ret[..])?; + Ok(ret) + } +} + +impl IntoIterator for HdrDecoder { + type Item = ImageResult; + type IntoIter = HdrImageDecoderIterator; + + fn into_iter(self) -> Self::IntoIter { + HdrImageDecoderIterator { + r: self.r, + scanline_cnt: self.height as usize, + buf: vec![Default::default(); self.width as usize], + col: 0, + scanline: 0, + trouble: true, // make first call to `next()` read scanline + error_encountered: false, + } + } +} + +/// Scanline buffered pixel by pixel iterator +pub struct HdrImageDecoderIterator { + r: R, + scanline_cnt: usize, + buf: Vec, // scanline buffer + col: usize, // current position in scanline + scanline: usize, // current scanline + trouble: bool, // optimization, true indicates that we need to check something + error_encountered: bool, +} + +impl HdrImageDecoderIterator { + // Advances counter to the next pixel + #[inline] + fn advance(&mut self) { + self.col += 1; + if self.col == self.buf.len() { + self.col = 0; + self.scanline += 1; + self.trouble = true; + } + } +} + +impl Iterator for HdrImageDecoderIterator { + type Item = ImageResult; + + fn next(&mut self) -> Option { + if !self.trouble { + let ret = self.buf[self.col]; + self.advance(); + Some(Ok(ret)) + } else { + // some condition is pending + if self.buf.is_empty() || self.scanline == self.scanline_cnt { + // No more pixels + return None; + } // no else + if self.error_encountered { + self.advance(); + // Error was encountered. Keep producing errors. + // ImageError can't implement Clone, so just dump some error + return Some(Err(ImageError::Parameter(ParameterError::from_kind( + ParameterErrorKind::FailedAlready, + )))); + } // no else + if self.col == 0 { + // fill scanline buffer + match read_scanline(&mut self.r, &mut self.buf[..]) { + Ok(_) => { + // no action required + } + Err(err) => { + self.advance(); + self.error_encountered = true; + self.trouble = true; + return Some(Err(err)); + } + } + } // no else + self.trouble = false; + let ret = self.buf[0]; + self.advance(); + Some(Ok(ret)) + } + } + + fn size_hint(&self) -> (usize, Option) { + let total_cnt = self.buf.len() * self.scanline_cnt; + let cur_cnt = self.buf.len() * self.scanline + self.col; + let remaining = total_cnt - cur_cnt; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for HdrImageDecoderIterator {} + +// Precondition: buf.len() > 0 +fn read_scanline(r: &mut R, buf: &mut [Rgbe8Pixel]) -> ImageResult<()> { + assert!(!buf.is_empty()); + let width = buf.len(); + // first 4 bytes in scanline allow to determine compression method + let fb = read_rgbe(r)?; + if fb.c[0] == 2 && fb.c[1] == 2 && fb.c[2] < 128 { + // denormalized pixel value (2,2,<128,_) indicates new per component RLE method + // decode_component guarantees that offset is within 0 .. width + // therefore we can skip bounds checking here, but we will not + decode_component(r, width, |offset, value| buf[offset].c[0] = value)?; + decode_component(r, width, |offset, value| buf[offset].c[1] = value)?; + decode_component(r, width, |offset, value| buf[offset].c[2] = value)?; + decode_component(r, width, |offset, value| buf[offset].e = value)?; + } else { + // old RLE method (it was considered old around 1991, should it be here?) + decode_old_rle(r, fb, buf)?; + } + Ok(()) +} + +#[inline(always)] +fn read_byte(r: &mut R) -> io::Result { + let mut buf = [0u8]; + r.read_exact(&mut buf[..])?; + Ok(buf[0]) +} + +// Guarantees that first parameter of set_component will be within pos .. pos+width +#[inline] +fn decode_component( + r: &mut R, + width: usize, + mut set_component: S, +) -> ImageResult<()> { + let mut buf = [0; 128]; + let mut pos = 0; + while pos < width { + // increment position by a number of decompressed values + pos += { + let rl = read_byte(r)?; + if rl <= 128 { + // sanity check + if pos + rl as usize > width { + return Err(DecoderError::WrongScanlineLength(pos + rl as usize, width).into()); + } + // read values + r.read_exact(&mut buf[0..rl as usize])?; + for (offset, &value) in buf[0..rl as usize].iter().enumerate() { + set_component(pos + offset, value); + } + rl as usize + } else { + // run + let rl = rl - 128; + // sanity check + if pos + rl as usize > width { + return Err(DecoderError::WrongScanlineLength(pos + rl as usize, width).into()); + } + // fill with same value + let value = read_byte(r)?; + for offset in 0..rl as usize { + set_component(pos + offset, value); + } + rl as usize + } + }; + } + if pos != width { + return Err(DecoderError::WrongScanlineLength(pos, width).into()); + } + Ok(()) +} + +// Decodes scanline, places it into buf +// Precondition: buf.len() > 0 +// fb - first 4 bytes of scanline +fn decode_old_rle(r: &mut R, fb: Rgbe8Pixel, buf: &mut [Rgbe8Pixel]) -> ImageResult<()> { + assert!(!buf.is_empty()); + let width = buf.len(); + // convenience function. + // returns run length if pixel is a run length marker + #[inline] + fn rl_marker(pix: Rgbe8Pixel) -> Option { + if pix.c == [1, 1, 1] { + Some(pix.e as usize) + } else { + None + } + } + // first pixel in scanline should not be run length marker + // it is error if it is + if rl_marker(fb).is_some() { + return Err(DecoderError::FirstPixelRlMarker.into()); + } + buf[0] = fb; // set first pixel of scanline + + let mut x_off = 1; // current offset from beginning of a scanline + let mut rl_mult = 1; // current run length multiplier + let mut prev_pixel = fb; + while x_off < width { + let pix = read_rgbe(r)?; + // it's harder to forget to increase x_off if I write this this way. + x_off += { + if let Some(rl) = rl_marker(pix) { + // rl_mult takes care of consecutive RL markers + let rl = rl * rl_mult; + rl_mult *= 256; + if x_off + rl <= width { + // do run + for b in &mut buf[x_off..x_off + rl] { + *b = prev_pixel; + } + } else { + return Err(DecoderError::WrongScanlineLength(x_off + rl, width).into()); + }; + rl // value to increase x_off by + } else { + rl_mult = 1; // chain of consecutive RL markers is broken + prev_pixel = pix; + buf[x_off] = pix; + 1 // value to increase x_off by + } + }; + } + if x_off != width { + return Err(DecoderError::WrongScanlineLength(x_off, width).into()); + } + Ok(()) +} + +fn read_rgbe(r: &mut R) -> io::Result { + let mut buf = [0u8; 4]; + r.read_exact(&mut buf[..])?; + Ok(Rgbe8Pixel { + c: [buf[0], buf[1], buf[2]], + e: buf[3], + }) +} + +/// Metadata for Radiance HDR image +#[derive(Debug, Clone)] +pub struct HdrMetadata { + /// Width of decoded image. It could be either scanline length, + /// or scanline count, depending on image orientation. + pub width: u32, + /// Height of decoded image. It depends on orientation too. + pub height: u32, + /// Orientation matrix. For standard orientation it is ((1,0),(0,1)) - left to right, top to bottom. + /// First pair tells how resulting pixel coordinates change along a scanline. + /// Second pair tells how they change from one scanline to the next. + pub orientation: ((i8, i8), (i8, i8)), + /// Divide color values by exposure to get to get physical radiance in + /// watts/steradian/m2 + /// + /// Image may not contain physical data, even if this field is set. + pub exposure: Option, + /// Divide color values by corresponding tuple member (r, g, b) to get to get physical radiance + /// in watts/steradian/m2 + /// + /// Image may not contain physical data, even if this field is set. + pub color_correction: Option<(f32, f32, f32)>, + /// Pixel height divided by pixel width + pub pixel_aspect_ratio: Option, + /// All lines contained in image header are put here. Ordering of lines is preserved. + /// Lines in the form "key=value" are represented as ("key", "value"). + /// All other lines are ("", "line") + pub custom_attributes: Vec<(String, String)>, +} + +impl HdrMetadata { + fn new() -> HdrMetadata { + HdrMetadata { + width: 0, + height: 0, + orientation: ((1, 0), (0, 1)), + exposure: None, + color_correction: None, + pixel_aspect_ratio: None, + custom_attributes: vec![], + } + } + + // Updates header info, in strict mode returns error for malformed lines (no '=' separator) + // unknown attributes are skipped + fn update_header_info(&mut self, line: &str, strict: bool) -> ImageResult<()> { + // split line at first '=' + // old Radiance HDR files (*.pic) feature tabs in key, so vvv trim + let maybe_key_value = split_at_first(line, "=").map(|(key, value)| (key.trim(), value)); + // save all header lines in custom_attributes + match maybe_key_value { + Some((key, val)) => self + .custom_attributes + .push((key.to_owned(), val.to_owned())), + None => self.custom_attributes.push(("".into(), line.to_owned())), + } + // parse known attributes + match maybe_key_value { + Some(("FORMAT", val)) => { + if val.trim() != "32-bit_rle_rgbe" { + // XYZE isn't supported yet + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Hdr.into(), + UnsupportedErrorKind::Format(ImageFormatHint::Name(limit_string_len( + val, 20, + ))), + ), + )); + } + } + Some(("EXPOSURE", val)) => { + match val.trim().parse::() { + Ok(v) => { + self.exposure = Some(self.exposure.unwrap_or(1.0) * v); // all encountered exposure values should be multiplied + } + Err(parse_error) => { + if strict { + return Err(DecoderError::UnparsableF32( + LineType::Exposure, + parse_error, + ) + .into()); + } // no else, skip this line in non-strict mode + } + }; + } + Some(("PIXASPECT", val)) => { + match val.trim().parse::() { + Ok(v) => { + self.pixel_aspect_ratio = Some(self.pixel_aspect_ratio.unwrap_or(1.0) * v); + // all encountered exposure values should be multiplied + } + Err(parse_error) => { + if strict { + return Err(DecoderError::UnparsableF32( + LineType::Pixaspect, + parse_error, + ) + .into()); + } // no else, skip this line in non-strict mode + } + }; + } + Some(("COLORCORR", val)) => { + let mut rgbcorr = [1.0, 1.0, 1.0]; + match parse_space_separated_f32(val, &mut rgbcorr, LineType::Colorcorr) { + Ok(extra_numbers) => { + if strict && extra_numbers { + return Err(DecoderError::ExtraneousColorcorrNumbers.into()); + } // no else, just ignore extra numbers + let (rc, gc, bc) = self.color_correction.unwrap_or((1.0, 1.0, 1.0)); + self.color_correction = + Some((rc * rgbcorr[0], gc * rgbcorr[1], bc * rgbcorr[2])); + } + Err(err) => { + if strict { + return Err(err); + } // no else, skip malformed line in non-strict mode + } + } + } + None => { + // old Radiance HDR files (*.pic) contain commands in a header + // just skip them + } + _ => { + // skip unknown attribute + } + } // match attributes + Ok(()) + } +} + +fn parse_space_separated_f32(line: &str, vals: &mut [f32], line_tp: LineType) -> ImageResult { + let mut nums = line.split_whitespace(); + for val in vals.iter_mut() { + if let Some(num) = nums.next() { + match num.parse::() { + Ok(v) => *val = v, + Err(err) => return Err(DecoderError::UnparsableF32(line_tp, err).into()), + } + } else { + // not enough numbers in line + return Err(DecoderError::LineTooShort(line_tp).into()); + } + } + Ok(nums.next().is_some()) +} + +// Parses dimension line "-Y height +X width" +// returns (width, height) or error +fn parse_dimensions_line(line: &str, strict: bool) -> ImageResult<(u32, u32)> { + const DIMENSIONS_COUNT: usize = 4; + + let mut dim_parts = line.split_whitespace(); + let c1_tag = dim_parts + .next() + .ok_or(DecoderError::DimensionsLineTooShort(0, DIMENSIONS_COUNT))?; + let c1_str = dim_parts + .next() + .ok_or(DecoderError::DimensionsLineTooShort(1, DIMENSIONS_COUNT))?; + let c2_tag = dim_parts + .next() + .ok_or(DecoderError::DimensionsLineTooShort(2, DIMENSIONS_COUNT))?; + let c2_str = dim_parts + .next() + .ok_or(DecoderError::DimensionsLineTooShort(3, DIMENSIONS_COUNT))?; + if strict && dim_parts.next().is_some() { + // extra data in dimensions line + return Err(DecoderError::DimensionsLineTooLong(DIMENSIONS_COUNT).into()); + } // no else + // dimensions line is in the form "-Y 10 +X 20" + // There are 8 possible orientations: +Y +X, +X -Y and so on + match (c1_tag, c2_tag) { + ("-Y", "+X") => { + // Common orientation (left-right, top-down) + // c1_str is height, c2_str is width + let height = c1_str + .parse::() + .map_err(|pe| DecoderError::UnparsableU32(LineType::DimensionsHeight, pe))?; + let width = c2_str + .parse::() + .map_err(|pe| DecoderError::UnparsableU32(LineType::DimensionsWidth, pe))?; + Ok((width, height)) + } + _ => Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Hdr.into(), + UnsupportedErrorKind::GenericFeature(format!( + "Orientation {} {}", + limit_string_len(c1_tag, 4), + limit_string_len(c2_tag, 4) + )), + ), + )), + } // final expression. Returns value +} + +// Returns string with no more than len+3 characters +fn limit_string_len(s: &str, len: usize) -> String { + let s_char_len = s.chars().count(); + if s_char_len > len { + s.chars().take(len).chain("...".chars()).collect() + } else { + s.into() + } +} + +// Splits string into (before separator, after separator) tuple +// or None if separator isn't found +fn split_at_first<'a>(s: &'a str, separator: &str) -> Option<(&'a str, &'a str)> { + match s.find(separator) { + None | Some(0) => None, + Some(p) if p >= s.len() - separator.len() => None, + Some(p) => Some((&s[..p], &s[(p + separator.len())..])), + } +} + +#[test] +fn split_at_first_test() { + assert_eq!(split_at_first(&Cow::Owned("".into()), "="), None); + assert_eq!(split_at_first(&Cow::Owned("=".into()), "="), None); + assert_eq!(split_at_first(&Cow::Owned("= ".into()), "="), None); + assert_eq!( + split_at_first(&Cow::Owned(" = ".into()), "="), + Some((" ", " ")) + ); + assert_eq!( + split_at_first(&Cow::Owned("EXPOSURE= ".into()), "="), + Some(("EXPOSURE", " ")) + ); + assert_eq!( + split_at_first(&Cow::Owned("EXPOSURE= =".into()), "="), + Some(("EXPOSURE", " =")) + ); + assert_eq!( + split_at_first(&Cow::Owned("EXPOSURE== =".into()), "=="), + Some(("EXPOSURE", " =")) + ); + assert_eq!(split_at_first(&Cow::Owned("EXPOSURE".into()), ""), None); +} + +// Reads input until b"\n" or EOF +// Returns vector of read bytes NOT including end of line characters +// or return None to indicate end of file +fn read_line_u8(r: &mut R) -> ::std::io::Result>> { + let mut ret = Vec::with_capacity(16); + match r.read_until(b'\n', &mut ret) { + Ok(0) => Ok(None), + Ok(_) => { + if let Some(&b'\n') = ret[..].last() { + let _ = ret.pop(); + } + Ok(Some(ret)) + } + Err(err) => Err(err), + } +} + +#[test] +fn read_line_u8_test() { + let buf: Vec<_> = (&b"One\nTwo\nThree\nFour\n\n\n"[..]).into(); + let input = &mut ::std::io::Cursor::new(buf); + assert_eq!(&read_line_u8(input).unwrap().unwrap()[..], &b"One"[..]); + assert_eq!(&read_line_u8(input).unwrap().unwrap()[..], &b"Two"[..]); + assert_eq!(&read_line_u8(input).unwrap().unwrap()[..], &b"Three"[..]); + assert_eq!(&read_line_u8(input).unwrap().unwrap()[..], &b"Four"[..]); + assert_eq!(&read_line_u8(input).unwrap().unwrap()[..], &b""[..]); + assert_eq!(&read_line_u8(input).unwrap().unwrap()[..], &b""[..]); + assert_eq!(read_line_u8(input).unwrap(), None); +} + +/// Helper function for reading raw 3-channel f32 images +pub fn read_raw_file>(path: P) -> ::std::io::Result>> { + use byteorder::{LittleEndian as LE, ReadBytesExt}; + use std::fs::File; + use std::io::BufReader; + + let mut r = BufReader::new(File::open(path)?); + let w = r.read_u32::()? as usize; + let h = r.read_u32::()? as usize; + let c = r.read_u32::()? as usize; + assert_eq!(c, 3); + let cnt = w * h; + let mut ret = Vec::with_capacity(cnt); + for _ in 0..cnt { + let cr = r.read_f32::()?; + let cg = r.read_f32::()?; + let cb = r.read_f32::()?; + ret.push(Rgb([cr, cg, cb])); + } + Ok(ret) +} + +#[cfg(test)] +mod test { + use super::*; + use std::io::Cursor; + + #[test] + fn dimension_overflow() { + let data = b"#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n -Y 4294967295 +X 4294967295"; + + assert!(HdrAdapter::new(Cursor::new(data)).is_err()); + assert!(HdrAdapter::new_nonstrict(Cursor::new(data)).is_err()); + } +} diff --git a/vendor/image/src/codecs/hdr/encoder.rs b/vendor/image/src/codecs/hdr/encoder.rs new file mode 100644 index 0000000..c3a176d --- /dev/null +++ b/vendor/image/src/codecs/hdr/encoder.rs @@ -0,0 +1,433 @@ +use crate::codecs::hdr::{rgbe8, Rgbe8Pixel, SIGNATURE}; +use crate::color::Rgb; +use crate::error::ImageResult; +use std::cmp::Ordering; +use std::io::{Result, Write}; + +/// Radiance HDR encoder +pub struct HdrEncoder { + w: W, +} + +impl HdrEncoder { + /// Creates encoder + pub fn new(w: W) -> HdrEncoder { + HdrEncoder { w } + } + + /// Encodes the image ```data``` + /// that has dimensions ```width``` and ```height``` + pub fn encode(mut self, data: &[Rgb], width: usize, height: usize) -> ImageResult<()> { + assert!(data.len() >= width * height); + let w = &mut self.w; + w.write_all(SIGNATURE)?; + w.write_all(b"\n")?; + w.write_all(b"# Rust HDR encoder\n")?; + w.write_all(b"FORMAT=32-bit_rle_rgbe\n\n")?; + w.write_all(format!("-Y {} +X {}\n", height, width).as_bytes())?; + + if !(8..=32_768).contains(&width) { + for &pix in data { + write_rgbe8(w, to_rgbe8(pix))?; + } + } else { + // new RLE marker contains scanline width + let marker = rgbe8(2, 2, (width / 256) as u8, (width % 256) as u8); + // buffers for encoded pixels + let mut bufr = vec![0; width]; + let mut bufg = vec![0; width]; + let mut bufb = vec![0; width]; + let mut bufe = vec![0; width]; + let mut rle_buf = vec![0; width]; + for scanline in data.chunks(width) { + for ((((r, g), b), e), &pix) in bufr + .iter_mut() + .zip(bufg.iter_mut()) + .zip(bufb.iter_mut()) + .zip(bufe.iter_mut()) + .zip(scanline.iter()) + { + let cp = to_rgbe8(pix); + *r = cp.c[0]; + *g = cp.c[1]; + *b = cp.c[2]; + *e = cp.e; + } + write_rgbe8(w, marker)?; // New RLE encoding marker + rle_buf.clear(); + rle_compress(&bufr[..], &mut rle_buf); + w.write_all(&rle_buf[..])?; + rle_buf.clear(); + rle_compress(&bufg[..], &mut rle_buf); + w.write_all(&rle_buf[..])?; + rle_buf.clear(); + rle_compress(&bufb[..], &mut rle_buf); + w.write_all(&rle_buf[..])?; + rle_buf.clear(); + rle_compress(&bufe[..], &mut rle_buf); + w.write_all(&rle_buf[..])?; + } + } + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq)] +enum RunOrNot { + Run(u8, usize), + Norun(usize, usize), +} +use self::RunOrNot::{Norun, Run}; + +const RUN_MAX_LEN: usize = 127; +const NORUN_MAX_LEN: usize = 128; + +struct RunIterator<'a> { + data: &'a [u8], + curidx: usize, +} + +impl<'a> RunIterator<'a> { + fn new(data: &'a [u8]) -> RunIterator<'a> { + RunIterator { data, curidx: 0 } + } +} + +impl<'a> Iterator for RunIterator<'a> { + type Item = RunOrNot; + + fn next(&mut self) -> Option { + if self.curidx == self.data.len() { + None + } else { + let cv = self.data[self.curidx]; + let crun = self.data[self.curidx..] + .iter() + .take_while(|&&v| v == cv) + .take(RUN_MAX_LEN) + .count(); + let ret = if crun > 2 { + Run(cv, crun) + } else { + Norun(self.curidx, crun) + }; + self.curidx += crun; + Some(ret) + } + } +} + +struct NorunCombineIterator<'a> { + runiter: RunIterator<'a>, + prev: Option, +} + +impl<'a> NorunCombineIterator<'a> { + fn new(data: &'a [u8]) -> NorunCombineIterator<'a> { + NorunCombineIterator { + runiter: RunIterator::new(data), + prev: None, + } + } +} + +// Combines sequential noruns produced by RunIterator +impl<'a> Iterator for NorunCombineIterator<'a> { + type Item = RunOrNot; + fn next(&mut self) -> Option { + loop { + match self.prev.take() { + Some(Run(c, len)) => { + // Just return stored run + return Some(Run(c, len)); + } + Some(Norun(idx, len)) => { + // Let's see if we need to continue norun + match self.runiter.next() { + Some(Norun(_, len1)) => { + // norun continues + let clen = len + len1; // combined length + match clen.cmp(&NORUN_MAX_LEN) { + Ordering::Equal => return Some(Norun(idx, clen)), + Ordering::Greater => { + // combined norun exceeds maximum length. store extra part of norun + self.prev = + Some(Norun(idx + NORUN_MAX_LEN, clen - NORUN_MAX_LEN)); + // then return maximal norun + return Some(Norun(idx, NORUN_MAX_LEN)); + } + Ordering::Less => { + // len + len1 < NORUN_MAX_LEN + self.prev = Some(Norun(idx, len + len1)); + // combine and continue loop + } + } + } + Some(Run(c, len1)) => { + // Run encountered. Store it + self.prev = Some(Run(c, len1)); + return Some(Norun(idx, len)); // and return combined norun + } + None => { + // End of sequence + return Some(Norun(idx, len)); // return combined norun + } + } + } // End match self.prev.take() == Some(NoRun()) + None => { + // No norun to combine + match self.runiter.next() { + Some(Norun(idx, len)) => { + self.prev = Some(Norun(idx, len)); + // store for combine and continue the loop + } + Some(Run(c, len)) => { + // Some run. Just return it + return Some(Run(c, len)); + } + None => { + // That's all, folks + return None; + } + } + } // End match self.prev.take() == None + } // End match + } // End loop + } +} + +// Appends RLE compressed ```data``` to ```rle``` +fn rle_compress(data: &[u8], rle: &mut Vec) { + rle.clear(); + if data.is_empty() { + rle.push(0); // Technically correct. It means read next 0 bytes. + return; + } + // Task: split data into chunks of repeating (max 127) and non-repeating bytes (max 128) + // Prepend non-repeating chunk with its length + // Replace repeating byte with (run length + 128) and the byte + for rnr in NorunCombineIterator::new(data) { + match rnr { + Run(c, len) => { + assert!(len <= 127); + rle.push(128u8 + len as u8); + rle.push(c); + } + Norun(idx, len) => { + assert!(len <= 128); + rle.push(len as u8); + rle.extend_from_slice(&data[idx..idx + len]); + } + } + } +} + +fn write_rgbe8(w: &mut W, v: Rgbe8Pixel) -> Result<()> { + w.write_all(&[v.c[0], v.c[1], v.c[2], v.e]) +} + +/// Converts ```Rgb``` into ```Rgbe8Pixel``` +pub fn to_rgbe8(pix: Rgb) -> Rgbe8Pixel { + let pix = pix.0; + let mx = f32::max(pix[0], f32::max(pix[1], pix[2])); + if mx <= 0.0 { + Rgbe8Pixel { c: [0, 0, 0], e: 0 } + } else { + // let (frac, exp) = mx.frexp(); // unstable yet + let exp = mx.log2().floor() as i32 + 1; + let mul = f32::powi(2.0, exp); + let mut conv = [0u8; 3]; + for (cv, &sv) in conv.iter_mut().zip(pix.iter()) { + *cv = f32::trunc(sv / mul * 256.0) as u8; + } + Rgbe8Pixel { + c: conv, + e: (exp + 128) as u8, + } + } +} + +#[test] +fn to_rgbe8_test() { + use crate::codecs::hdr::rgbe8; + let test_cases = vec![rgbe8(0, 0, 0, 0), rgbe8(1, 1, 128, 128)]; + for &pix in &test_cases { + assert_eq!(pix, to_rgbe8(pix.to_hdr())); + } + for mc in 128..255 { + // TODO: use inclusive range when stable + let pix = rgbe8(mc, mc, mc, 100); + assert_eq!(pix, to_rgbe8(pix.to_hdr())); + let pix = rgbe8(mc, 0, mc, 130); + assert_eq!(pix, to_rgbe8(pix.to_hdr())); + let pix = rgbe8(0, 0, mc, 140); + assert_eq!(pix, to_rgbe8(pix.to_hdr())); + let pix = rgbe8(1, 0, mc, 150); + assert_eq!(pix, to_rgbe8(pix.to_hdr())); + let pix = rgbe8(1, mc, 10, 128); + assert_eq!(pix, to_rgbe8(pix.to_hdr())); + for c in 0..255 { + // Radiance HDR seems to be pre IEEE 754. + // exponent can be -128 (represented as 0u8), so some colors cannot be represented in normalized f32 + // Let's exclude exponent value of -128 (0u8) from testing + let pix = rgbe8(1, mc, c, if c == 0 { 1 } else { c }); + assert_eq!(pix, to_rgbe8(pix.to_hdr())); + } + } + fn relative_dist(a: Rgb, b: Rgb) -> f32 { + // maximal difference divided by maximal value + let max_diff = + a.0.iter() + .zip(b.0.iter()) + .fold(0.0, |diff, (&a, &b)| f32::max(diff, (a - b).abs())); + let max_val = + a.0.iter() + .chain(b.0.iter()) + .fold(0.0, |maxv, &a| f32::max(maxv, a)); + if max_val == 0.0 { + 0.0 + } else { + max_diff / max_val + } + } + let test_values = vec![ + 0.000_001, 0.000_02, 0.000_3, 0.004, 0.05, 0.6, 7.0, 80.0, 900.0, 1_000.0, 20_000.0, + 300_000.0, + ]; + for &r in &test_values { + for &g in &test_values { + for &b in &test_values { + let c1 = Rgb([r, g, b]); + let c2 = to_rgbe8(c1).to_hdr(); + let rel_dist = relative_dist(c1, c2); + // Maximal value is normalized to the range 128..256, thus we have 1/128 precision + assert!( + rel_dist <= 1.0 / 128.0, + "Relative distance ({}) exceeds 1/128 for {:?} and {:?}", + rel_dist, + c1, + c2 + ); + } + } + } +} + +#[test] +fn runiterator_test() { + let data = []; + let mut run_iter = RunIterator::new(&data[..]); + assert_eq!(run_iter.next(), None); + let data = [5]; + let mut run_iter = RunIterator::new(&data[..]); + assert_eq!(run_iter.next(), Some(Norun(0, 1))); + assert_eq!(run_iter.next(), None); + let data = [1, 1]; + let mut run_iter = RunIterator::new(&data[..]); + assert_eq!(run_iter.next(), Some(Norun(0, 2))); + assert_eq!(run_iter.next(), None); + let data = [0, 0, 0]; + let mut run_iter = RunIterator::new(&data[..]); + assert_eq!(run_iter.next(), Some(Run(0u8, 3))); + assert_eq!(run_iter.next(), None); + let data = [0, 0, 1, 1]; + let mut run_iter = RunIterator::new(&data[..]); + assert_eq!(run_iter.next(), Some(Norun(0, 2))); + assert_eq!(run_iter.next(), Some(Norun(2, 2))); + assert_eq!(run_iter.next(), None); + let data = [0, 0, 0, 1, 1]; + let mut run_iter = RunIterator::new(&data[..]); + assert_eq!(run_iter.next(), Some(Run(0u8, 3))); + assert_eq!(run_iter.next(), Some(Norun(3, 2))); + assert_eq!(run_iter.next(), None); + let data = [1, 2, 2, 2]; + let mut run_iter = RunIterator::new(&data[..]); + assert_eq!(run_iter.next(), Some(Norun(0, 1))); + assert_eq!(run_iter.next(), Some(Run(2u8, 3))); + assert_eq!(run_iter.next(), None); + let data = [1, 1, 2, 2, 2]; + let mut run_iter = RunIterator::new(&data[..]); + assert_eq!(run_iter.next(), Some(Norun(0, 2))); + assert_eq!(run_iter.next(), Some(Run(2u8, 3))); + assert_eq!(run_iter.next(), None); + let data = [2; 128]; + let mut run_iter = RunIterator::new(&data[..]); + assert_eq!(run_iter.next(), Some(Run(2u8, 127))); + assert_eq!(run_iter.next(), Some(Norun(127, 1))); + assert_eq!(run_iter.next(), None); + let data = [2; 129]; + let mut run_iter = RunIterator::new(&data[..]); + assert_eq!(run_iter.next(), Some(Run(2u8, 127))); + assert_eq!(run_iter.next(), Some(Norun(127, 2))); + assert_eq!(run_iter.next(), None); + let data = [2; 130]; + let mut run_iter = RunIterator::new(&data[..]); + assert_eq!(run_iter.next(), Some(Run(2u8, 127))); + assert_eq!(run_iter.next(), Some(Run(2u8, 3))); + assert_eq!(run_iter.next(), None); +} + +#[test] +fn noruncombine_test() { + fn a(mut v: Vec, mut other: Vec) -> Vec { + v.append(&mut other); + v + } + + let v = vec![]; + let mut rsi = NorunCombineIterator::new(&v[..]); + assert_eq!(rsi.next(), None); + + let v = vec![1]; + let mut rsi = NorunCombineIterator::new(&v[..]); + assert_eq!(rsi.next(), Some(Norun(0, 1))); + assert_eq!(rsi.next(), None); + + let v = vec![2, 2]; + let mut rsi = NorunCombineIterator::new(&v[..]); + assert_eq!(rsi.next(), Some(Norun(0, 2))); + assert_eq!(rsi.next(), None); + + let v = vec![3, 3, 3]; + let mut rsi = NorunCombineIterator::new(&v[..]); + assert_eq!(rsi.next(), Some(Run(3, 3))); + assert_eq!(rsi.next(), None); + + let v = vec![4, 4, 3, 3, 3]; + let mut rsi = NorunCombineIterator::new(&v[..]); + assert_eq!(rsi.next(), Some(Norun(0, 2))); + assert_eq!(rsi.next(), Some(Run(3, 3))); + assert_eq!(rsi.next(), None); + + let v = vec![40; 400]; + let mut rsi = NorunCombineIterator::new(&v[..]); + assert_eq!(rsi.next(), Some(Run(40, 127))); + assert_eq!(rsi.next(), Some(Run(40, 127))); + assert_eq!(rsi.next(), Some(Run(40, 127))); + assert_eq!(rsi.next(), Some(Run(40, 19))); + assert_eq!(rsi.next(), None); + + let v = a(a(vec![5; 3], vec![6; 129]), vec![7, 3, 7, 10, 255]); + let mut rsi = NorunCombineIterator::new(&v[..]); + assert_eq!(rsi.next(), Some(Run(5, 3))); + assert_eq!(rsi.next(), Some(Run(6, 127))); + assert_eq!(rsi.next(), Some(Norun(130, 7))); + assert_eq!(rsi.next(), None); + + let v = a(a(vec![5; 2], vec![6; 129]), vec![7, 3, 7, 7, 255]); + let mut rsi = NorunCombineIterator::new(&v[..]); + assert_eq!(rsi.next(), Some(Norun(0, 2))); + assert_eq!(rsi.next(), Some(Run(6, 127))); + assert_eq!(rsi.next(), Some(Norun(129, 7))); + assert_eq!(rsi.next(), None); + + let v: Vec<_> = ::std::iter::repeat(()) + .flat_map(|_| (0..2)) + .take(257) + .collect(); + let mut rsi = NorunCombineIterator::new(&v[..]); + assert_eq!(rsi.next(), Some(Norun(0, 128))); + assert_eq!(rsi.next(), Some(Norun(128, 128))); + assert_eq!(rsi.next(), Some(Norun(256, 1))); + assert_eq!(rsi.next(), None); +} diff --git a/vendor/image/src/codecs/hdr/mod.rs b/vendor/image/src/codecs/hdr/mod.rs new file mode 100644 index 0000000..b3325bc --- /dev/null +++ b/vendor/image/src/codecs/hdr/mod.rs @@ -0,0 +1,15 @@ +//! Decoding of Radiance HDR Images +//! +//! A decoder for Radiance HDR images +//! +//! # Related Links +//! +//! * +//! * +//! + +mod decoder; +mod encoder; + +pub use self::decoder::*; +pub use self::encoder::*; -- cgit v1.2.3