diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3c217d3e..1de10e15 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - rust: ["1.34.2", stable, beta, nightly] + rust: ["1.36.0", stable, beta, nightly] features: ["", "rayon"] command: [test, benchmark] @@ -29,11 +29,11 @@ jobs: - name: test run: > cargo test --tests --benches --no-default-features --features "$FEATURES" - if: ${{ matrix.command == 'test' && matrix.rust != '1.34.2' }} + if: ${{ matrix.command == 'test' && matrix.rust != '1.36.0' }} env: FEATURES: ${{ matrix.features }} - name: benchmark run: cargo bench --bench decoding_benchmark --no-default-features --features "$FEATURES" -- --warm-up-time 1 --measurement-time 1 --sample-size 25 - if: ${{ matrix.command == 'benchmark' && matrix.rust != '1.34.2' }} + if: ${{ matrix.command == 'benchmark' && matrix.rust != '1.36.0' }} env: FEATURES: ${{ matrix.features }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3379031a..1db53022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +- Added Lossless JPEG support +- Minimum supported rust version changed to 1.36 + ## v0.1.22 (2021-01-27) - Fix panic on jpeg without frames. diff --git a/README.md b/README.md index 33524ae3..c3f59783 100644 --- a/README.md +++ b/README.md @@ -32,4 +32,4 @@ fn main() { ``` ## Requirements -This crate compiles only with rust >= 1.34. +This crate compiles only with rust >= 1.36. diff --git a/benches/decoding_benchmark.rs b/benches/decoding_benchmark.rs index 6b62651d..fb8282de 100644 --- a/benches/decoding_benchmark.rs +++ b/benches/decoding_benchmark.rs @@ -33,5 +33,9 @@ fn main() { c.bench_function("extract metadata from an image", |b| b.iter(|| { read_metadata(include_bytes!("tower.jpg")) })); + + c.bench_function("decode a 3072x2048 RGB Lossless JPEG", |b| b.iter(|| { + read_image(include_bytes!("../tests/reftest/images/lossless/jpeg_lossless_sel1-rgb.jpg")) + })); c.final_summary(); } \ No newline at end of file diff --git a/examples/decode.rs b/examples/decode.rs index 67934a21..9453c132 100644 --- a/examples/decode.rs +++ b/examples/decode.rs @@ -23,17 +23,27 @@ fn main() { let output_file = File::create(output_path).unwrap(); let mut encoder = png::Encoder::new(output_file, info.width as u32, info.height as u32); - encoder.set_depth(png::BitDepth::Eight); match info.pixel_format { - jpeg::PixelFormat::L8 => encoder.set_color(png::ColorType::Grayscale), - jpeg::PixelFormat::RGB24 => encoder.set_color(png::ColorType::RGB), + jpeg::PixelFormat::L16 => { + encoder.set_depth(png::BitDepth::Sixteen); + encoder.set_color(png::ColorType::Grayscale); + }, + jpeg::PixelFormat::RGB24 => { + encoder.set_depth(png::BitDepth::Eight); + encoder.set_color(png::ColorType::RGB); + }, jpeg::PixelFormat::CMYK32 => { data = cmyk_to_rgb(&mut data); + encoder.set_depth(png::BitDepth::Eight); encoder.set_color(png::ColorType::RGB) }, - }; - + jpeg::PixelFormat::L8 => { + encoder.set_depth(png::BitDepth::Eight); + encoder.set_color(png::ColorType::Grayscale); + }, + } + encoder.write_header() .expect("writing png header failed") .write_image_data(&data) diff --git a/rust-toolchain b/rust-toolchain index 00e952d0..f55005a1 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.34.2 +1.36 diff --git a/src/decoder.rs b/src/decoder.rs index 664a0bdf..91d28885 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -15,6 +15,9 @@ use worker::{RowData, PlatformWorker, Worker}; pub const MAX_COMPONENTS: usize = 4; +mod lossless; +use self::lossless::compute_image_lossless; + static UNZIGZAG: [u8; 64] = [ 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, @@ -31,6 +34,8 @@ static UNZIGZAG: [u8; 64] = [ pub enum PixelFormat { /// Luminance (grayscale), 8 bits L8, + /// Luminance (grayscale), 16 bits + L16, /// RGB, 8 bits per channel RGB24, /// CMYK, 8 bits per channel @@ -42,6 +47,7 @@ impl PixelFormat { pub fn pixel_bytes(&self) -> usize { match self { PixelFormat::L8 => 1, + PixelFormat::L16 => 2, PixelFormat::RGB24 => 3, PixelFormat::CMYK32 => 4, } @@ -57,6 +63,8 @@ pub struct ImageInfo { pub height: u16, /// The pixel format of the image. pub pixel_format: PixelFormat, + /// The coding process of the image. + pub coding_process: CodingProcess, } /// JPEG decoder @@ -111,7 +119,11 @@ impl Decoder { match self.frame { Some(ref frame) => { let pixel_format = match frame.components.len() { - 1 => PixelFormat::L8, + 1 => match frame.precision { + 8 => PixelFormat::L8, + 16 => PixelFormat::L16, + _ => panic!() + }, 3 => PixelFormat::RGB24, 4 => PixelFormat::CMYK32, _ => panic!(), @@ -121,6 +133,7 @@ impl Decoder { width: frame.output_size.width, height: frame.output_size.height, pixel_format: pixel_format, + coding_process: frame.coding_process, }) }, None => None, @@ -209,7 +222,8 @@ impl Decoder { let mut pending_marker = None; let mut worker = None; let mut scans_processed = 0; - let mut planes = vec![Vec::new(); self.frame.as_ref().map_or(0, |frame| frame.components.len())]; + let mut planes = vec![Vec::::new(); self.frame.as_ref().map_or(0, |frame| frame.components.len())]; + let mut planes_u16 = vec![Vec::::new(); self.frame.as_ref().map_or(0, |frame| frame.components.len())]; loop { let marker = match pending_marker.take() { @@ -234,13 +248,13 @@ impl Decoder { if frame.is_differential { return Err(Error::Unsupported(UnsupportedFeature::Hierarchical)); } - if frame.coding_process == CodingProcess::Lossless { - return Err(Error::Unsupported(UnsupportedFeature::Lossless)); - } if frame.entropy_coding == EntropyCoding::Arithmetic { return Err(Error::Unsupported(UnsupportedFeature::ArithmeticEntropyCoding)); } - if frame.precision != 8 { + if frame.precision != 8 && frame.coding_process != CodingProcess::Lossless { + return Err(Error::Unsupported(UnsupportedFeature::SamplePrecision(frame.precision))); + } + if frame.precision != 8 && frame.precision != 16 { return Err(Error::Unsupported(UnsupportedFeature::SamplePrecision(frame.precision))); } if component_count != 1 && component_count != 3 && component_count != 4 { @@ -257,6 +271,7 @@ impl Decoder { } planes = vec![Vec::new(); component_count]; + planes_u16 = vec![Vec::new(); component_count]; }, // Scan header @@ -278,46 +293,57 @@ impl Decoder { }).collect(); } - // This was previously buggy, so let's explain the log here a bit. When a - // progressive frame is encoded then the coefficients (DC, AC) of each - // component (=color plane) can be split amongst scans. In particular it can - // happen or at least occurs in the wild that a scan contains coefficient 0 of - // all components. If now one but not all components had all other coefficients - // delivered in previous scans then such a scan contains all components but - // completes only some of them! (This is technically NOT permitted for all - // other coefficients as the standard dictates that scans with coefficients - // other than the 0th must only contain ONE component so we would either - // complete it or not. We may want to detect and error in case more component - // are part of a scan than allowed.) What a weird edge case. - // - // But this means we track precisely which components get completed here. - let mut finished = [false; MAX_COMPONENTS]; - - if scan.successive_approximation_low == 0 { - for (&i, component_finished) in scan.component_indices.iter().zip(&mut finished) { - if self.coefficients_finished[i] == !0 { - continue; - } - for j in scan.spectral_selection.clone() { - self.coefficients_finished[i] |= 1 << j; - } - if self.coefficients_finished[i] == !0 { - *component_finished = true; - } + + if frame.coding_process == CodingProcess::Lossless { + let (marker, data) = self.decode_scan_lossless(&frame, &scan)?; + + for (i, plane) in data.into_iter().enumerate().filter(|&(_, ref plane)| !plane.is_empty()) { + planes_u16[i] = plane; } + pending_marker = marker; } + else { + // This was previously buggy, so let's explain the log here a bit. When a + // progressive frame is encoded then the coefficients (DC, AC) of each + // component (=color plane) can be split amongst scans. In particular it can + // happen or at least occurs in the wild that a scan contains coefficient 0 of + // all components. If now one but not all components had all other coefficients + // delivered in previous scans then such a scan contains all components but + // completes only some of them! (This is technically NOT permitted for all + // other coefficients as the standard dictates that scans with coefficients + // other than the 0th must only contain ONE component so we would either + // complete it or not. We may want to detect and error in case more component + // are part of a scan than allowed.) What a weird edge case. + // + // But this means we track precisely which components get completed here. + let mut finished = [false; MAX_COMPONENTS]; + + if scan.successive_approximation_low == 0 { + for (&i, component_finished) in scan.component_indices.iter().zip(&mut finished) { + if self.coefficients_finished[i] == !0 { + continue; + } + for j in scan.spectral_selection.clone() { + self.coefficients_finished[i] |= 1 << j; + } + if self.coefficients_finished[i] == !0 { + *component_finished = true; + } + } + } - let (marker, data) = self.decode_scan(&frame, &scan, worker.as_mut().unwrap(), &finished)?; + let (marker, data) = self.decode_scan(&frame, &scan, worker.as_mut().unwrap(), &finished)?; - if let Some(data) = data { - for (i, plane) in data.into_iter().enumerate().filter(|&(_, ref plane)| !plane.is_empty()) { - if self.coefficients_finished[i] == !0 { - planes[i] = plane; + if let Some(data) = data { + for (i, plane) in data.into_iter().enumerate().filter(|&(_, ref plane)| !plane.is_empty()) { + if self.coefficients_finished[i] == !0 { + planes[i] = plane; + } } } + pending_marker = marker; } - pending_marker = marker; scans_processed += 1; }, @@ -465,7 +491,12 @@ impl Decoder { } } - compute_image(&frame.components, planes, frame.output_size, self.is_jfif, self.color_transform) + if frame.coding_process == CodingProcess::Lossless { + compute_image_lossless(&frame, planes_u16) + } + else{ + compute_image(&frame.components, planes, frame.output_size, self.is_jfif, self.color_transform) + } } fn read_marker(&mut self) -> Result { @@ -964,6 +995,7 @@ fn compute_image(components: &[Component], } } + #[cfg(feature="rayon")] fn compute_image_parallel(components: &[Component], data: Vec>, diff --git a/src/decoder/lossless.rs b/src/decoder/lossless.rs new file mode 100644 index 00000000..06f90236 --- /dev/null +++ b/src/decoder/lossless.rs @@ -0,0 +1,256 @@ +use decoder::{Decoder, MAX_COMPONENTS}; +use error::{Error, Result}; +use huffman::HuffmanDecoder; +use marker::Marker; +use parser::Predictor; +use parser::{Component, FrameInfo, ScanInfo}; +use std::io::Read; + +impl Decoder { + /// decode_scan_lossless + pub fn decode_scan_lossless( + &mut self, + frame: &FrameInfo, + scan: &ScanInfo, + ) -> Result<(Option, Vec>)> { + let ncomp = scan.component_indices.len(); + let npixel = frame.image_size.height as usize * frame.image_size.width as usize; + assert!(ncomp <= MAX_COMPONENTS); + let mut results = vec![vec![0u16; npixel]; ncomp]; + + let components: Vec = scan + .component_indices + .iter() + .map(|&i| frame.components[i].clone()) + .collect(); + + // Verify that all required huffman tables has been set. + if scan + .dc_table_indices + .iter() + .any(|&i| self.dc_huffman_tables[i].is_none()) + { + return Err(Error::Format( + "scan makes use of unset dc huffman table".to_owned(), + )); + } + + let mut huffman = HuffmanDecoder::new(); + let reader = &mut self.reader; + let mut mcus_left_until_restart = self.restart_interval; + let mut expected_rst_num = 0; + let mut ra = [0u16; MAX_COMPONENTS]; + let mut rb = [0u16; MAX_COMPONENTS]; + let mut rc = [0u16; MAX_COMPONENTS]; + + let width = frame.image_size.width as usize; + let height = frame.image_size.height as usize; + + let mut differences = vec![Vec::with_capacity(npixel); ncomp]; + for _mcu_y in 0..height { + for _mcu_x in 0..width { + if self.restart_interval > 0 { + if mcus_left_until_restart == 0 { + match huffman.take_marker(reader)? { + Some(Marker::RST(n)) => { + if n != expected_rst_num { + return Err(Error::Format(format!( + "found RST{} where RST{} was expected", + n, expected_rst_num + ))); + } + + huffman.reset(); + + expected_rst_num = (expected_rst_num + 1) % 8; + mcus_left_until_restart = self.restart_interval; + } + Some(marker) => { + return Err(Error::Format(format!( + "found marker {:?} inside scan where RST{} was expected", + marker, expected_rst_num + ))) + } + None => { + return Err(Error::Format(format!( + "no marker found where RST{} was expected", + expected_rst_num + ))) + } + } + } + + mcus_left_until_restart -= 1; + } + + for (i, _component) in components.iter().enumerate() { + let dc_table = self.dc_huffman_tables[scan.dc_table_indices[i]] + .as_ref() + .unwrap(); + let value = huffman.decode(reader, dc_table)?; + let diff = match value { + 0 => 0, + 1..=15 => huffman.receive_extend(reader, value)? as i32, + 16 => 32768, + _ => { + // Section F.1.2.1.1 + // Table F.1 + return Err(Error::Format( + "invalid DC difference magnitude category".to_owned(), + )); + } + }; + differences[i].push(diff); + } + } + } + + if scan.predictor_selection == Predictor::Ra { + for (i, _component) in components.iter().enumerate() { + // calculate the top left pixel + let diff = differences[i][0]; + let prediction = 1 << (frame.precision - scan.point_transform - 1) as i32; + let result = ((prediction + diff) & 0xFFFF) as u16; // modulo 2^16 + let result = result << scan.point_transform; + results[i][0] = result; + + // calculate leftmost column, using top pixel as predictor + let mut previous = result; + for mcu_y in 1..height { + let diff = differences[i][mcu_y * width]; + let prediction = previous as i32; + let result = ((prediction + diff) & 0xFFFF) as u16; // modulo 2^16 + let result = result << scan.point_transform; + results[i][mcu_y * width] = result; + previous = result; + } + + // calculate rows, using left pixel as predictor + for mcu_y in 0..height { + for mcu_x in 1..width { + let diff = differences[i][mcu_y * width + mcu_x]; + let prediction = results[i][mcu_y * width + mcu_x - 1] as i32; + let result = ((prediction + diff) & 0xFFFF) as u16; // modulo 2^16 + let result = result << scan.point_transform; + results[i][mcu_y * width + mcu_x] = result; + } + } + } + } else { + for mcu_y in 0..height { + for mcu_x in 0..width { + for (i, _component) in components.iter().enumerate() { + let diff = differences[i][mcu_y * width + mcu_x]; + + // The following lines could be further optimized, e.g. moving the checks + // and updates of the previous values into the prediction function or + // iterating such that diagonals with mcu_x + mcu_y = const are computed at + // the same time to exploit independent predictions in this case + if mcu_x > 0 { + ra[i] = results[i][mcu_y * frame.image_size.width as usize + mcu_x - 1]; + } + if mcu_y > 0 { + rb[i] = + results[i][(mcu_y - 1) * frame.image_size.width as usize + mcu_x]; + if mcu_x > 0 { + rc[i] = results[i] + [(mcu_y - 1) * frame.image_size.width as usize + (mcu_x - 1)]; + } + } + let prediction = predict( + ra[i] as i32, + rb[i] as i32, + rc[i] as i32, + scan.predictor_selection, + scan.point_transform, + frame.precision, + mcu_x, + mcu_y, + self.restart_interval > 0 + && mcus_left_until_restart == self.restart_interval - 1, + ); + let result = ((prediction + diff) & 0xFFFF) as u16; // modulo 2^16 + results[i][mcu_y * width + mcu_x] = result << scan.point_transform; + } + } + } + } + + let mut marker = huffman.take_marker(&mut self.reader)?; + while let Some(Marker::RST(_)) = marker { + marker = self.read_marker().ok(); + } + Ok((marker, results)) + } +} + +/// H.1.2.1 +fn predict( + ra: i32, + rb: i32, + rc: i32, + predictor: Predictor, + point_transform: u8, + input_precision: u8, + ix: usize, + iy: usize, + restart: bool, +) -> i32 { + let result = if (ix == 0 && iy == 0) || restart { + // start of first line or restart + 1 << (input_precision - point_transform - 1) + } else if iy == 0 { + // rest of first line + ra + } else if ix == 0 { + // start of other line + rb + } else { + // use predictor Table H.1 + match predictor { + Predictor::NoPrediction => 0, + Predictor::Ra => ra, + Predictor::Rb => rb, + Predictor::Rc => rc, + Predictor::RaRbRc1 => ra + rb - rc, + Predictor::RaRbRc2 => ra + ((rb - rc) >> 1), + Predictor::RaRbRc3 => rb + ((ra - rc) >> 1), + Predictor::RaRb => (ra + rb) / 2, + } + }; + result +} + +pub fn compute_image_lossless(frame: &FrameInfo, mut data: Vec>) -> Result> { + if data.is_empty() || data.iter().any(Vec::is_empty) { + return Err(Error::Format("not all components have data".to_owned())); + } + let output_size = frame.output_size; + let components = &frame.components; + let ncomp = components.len(); + + if ncomp == 1 { + let decoded = convert_to_u8(frame, data.remove(0)); + Ok(decoded) + } else { + let mut decoded: Vec = + vec![0u16; ncomp * output_size.width as usize * output_size.height as usize]; + for (x, chunk) in decoded.chunks_mut(ncomp).enumerate() { + for (i, (component_data, _)) in data.iter().zip(components.iter()).enumerate() { + chunk[i] = component_data[x]; + } + } + let decoded = convert_to_u8(frame, decoded); + Ok(decoded) + } +} + +fn convert_to_u8(frame: &FrameInfo, data: Vec) -> Vec { + if frame.precision == 8 { + data.iter().map(|x| *x as u8).collect() + } else { + // we output native endian, which is the standard for image-rs + let out: Vec<[u8; 2]> = data.iter().map(|x| x.to_ne_bytes()).collect(); + out.iter().flatten().map(|x| *x).collect() + } +} diff --git a/src/error.rs b/src/error.rs index fac73200..685efe00 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,11 +11,9 @@ pub type Result = ::std::result::Result; pub enum UnsupportedFeature { /// Hierarchical JPEG. Hierarchical, - /// Lossless JPEG. - Lossless, /// JPEG using arithmetic entropy coding instead of Huffman coding. ArithmeticEntropyCoding, - /// Sample precision in bits. 8 bit sample precision is what is currently supported. + /// Sample precision in bits. 8 bit sample precision is what is currently supported in non-lossless coding process. SamplePrecision(u8), /// Number of components in an image. 1, 3 and 4 components are currently supported. ComponentCount(u8), diff --git a/src/lib.rs b/src/lib.rs index a5e2b55f..ae74edea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ extern crate rayon; pub use decoder::{Decoder, ImageInfo, PixelFormat}; pub use error::{Error, UnsupportedFeature}; +pub use parser::CodingProcess; mod decoder; mod error; diff --git a/src/parser.rs b/src/parser.rs index 1c097af2..659b77fb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -18,13 +18,31 @@ pub enum EntropyCoding { Arithmetic, } +/// Represents the coding process of an image. #[derive(Clone, Copy, Debug, PartialEq)] pub enum CodingProcess { + /// Sequential Discrete Cosine Transform DctSequential, + /// Progressive Discrete Cosine Transform DctProgressive, + /// Lossless Lossless, } +// Table H.1 +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Predictor { + NoPrediction, + Ra, + Rb, + Rc, + RaRbRc1, // Ra + Rb - Rc + RaRbRc2, // Ra + ((Rb - Rc) >> 1) + RaRbRc3, // Rb + ((Ra - Rb) >> 1) + RaRb, // (Ra + Rb)/2 +} + + #[derive(Clone)] pub struct FrameInfo { pub is_baseline: bool, @@ -46,8 +64,10 @@ pub struct ScanInfo { pub ac_table_indices: Vec, pub spectral_selection: Range, + pub predictor_selection: Predictor, // for lossless pub successive_approximation_high: u8, pub successive_approximation_low: u8, + pub point_transform: u8, // for lossless } #[derive(Clone, Debug)] @@ -167,7 +187,7 @@ pub fn parse_sof(reader: &mut R, marker: Marker) -> Result { } }, _ => { - if coding_process != CodingProcess::Lossless { + if coding_process != CodingProcess::Lossless || precision > 16 { return Err(Error::Format(format!("invalid precision {} in frame header", precision))) } }, @@ -372,6 +392,21 @@ pub fn parse_sos(reader: &mut R, frame: &FrameInfo) -> Result let successive_approximation_high = byte >> 4; let successive_approximation_low = byte & 0x0f; + let predictor_selection = match spectral_selection_start { + 0 => Predictor::NoPrediction, + 1 => Predictor::Ra, + 2 => Predictor::Rb, + 3 => Predictor::Rc, + 4 => Predictor::RaRbRc1, + 5 => Predictor::RaRbRc2, + 6 => Predictor::RaRbRc3, + 7 => Predictor::RaRb, + _ => { + return Err(Error::Format(format!("invalid predictor selection value: {}", spectral_selection_start))); + } + }; + let point_transform = successive_approximation_low; + if frame.coding_process == CodingProcess::DctProgressive { if spectral_selection_end > 63 || spectral_selection_start > spectral_selection_end || (spectral_selection_start == 0 && spectral_selection_end != 0) { @@ -392,6 +427,14 @@ pub fn parse_sos(reader: &mut R, frame: &FrameInfo) -> Result return Err(Error::Format("successive approximation scan with more than one bit of improvement".to_owned())); } } + else if frame.coding_process == CodingProcess::Lossless { + if spectral_selection_end != 0 { + return Err(Error::Format("spectral selection end shall be zero in lossless scan".to_owned())); + } + if successive_approximation_high != 0 { + return Err(Error::Format("successive approximation high shall be zero in lossless scan".to_owned())); + } + } else { if spectral_selection_start != 0 || spectral_selection_end != 63 { return Err(Error::Format("spectral selection is not allowed in non-progressive scan".to_owned())); @@ -409,8 +452,10 @@ pub fn parse_sos(reader: &mut R, frame: &FrameInfo) -> Result start: spectral_selection_start, end: spectral_selection_end + 1, }, + predictor_selection, successive_approximation_high: successive_approximation_high, successive_approximation_low: successive_approximation_low, + point_transform, }) } diff --git a/tests/reftest/images/lossless/LICENSE b/tests/reftest/images/lossless/LICENSE new file mode 100644 index 00000000..97c87fdd --- /dev/null +++ b/tests/reftest/images/lossless/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015 RII-UTHSCSA +Copyright (c) 2021 Victor Saase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/tests/reftest/images/lossless/README.md b/tests/reftest/images/lossless/README.md new file mode 100644 index 00000000..e688135c --- /dev/null +++ b/tests/reftest/images/lossless/README.md @@ -0,0 +1,4 @@ +# lossless +The files starting with jpeg_lossless were taken from https://github.com/rii-mango/JPEGLosslessDecoderJS +The image data bytes were extracted from the DICOM files. +The reference PNG files were created with the IJG library as distributed in the GDCM toolkit https://github.com/malaterre/GDCM/tree/master/Utilities/gdcmjpeg. \ No newline at end of file diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel1-8bit.jpg b/tests/reftest/images/lossless/jpeg_lossless_sel1-8bit.jpg new file mode 100644 index 00000000..a2761af2 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel1-8bit.jpg differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel1-8bit.png b/tests/reftest/images/lossless/jpeg_lossless_sel1-8bit.png new file mode 100644 index 00000000..1390cb72 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel1-8bit.png differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel1-rgb.jpg b/tests/reftest/images/lossless/jpeg_lossless_sel1-rgb.jpg new file mode 100644 index 00000000..482733b3 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel1-rgb.jpg differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel1-rgb.png b/tests/reftest/images/lossless/jpeg_lossless_sel1-rgb.png new file mode 100644 index 00000000..24f8bc06 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel1-rgb.png differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel1.jpg b/tests/reftest/images/lossless/jpeg_lossless_sel1.jpg new file mode 100644 index 00000000..4f2c8a82 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel1.jpg differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel1.png b/tests/reftest/images/lossless/jpeg_lossless_sel1.png new file mode 100644 index 00000000..8c9f9501 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel1.png differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel2.jpg b/tests/reftest/images/lossless/jpeg_lossless_sel2.jpg new file mode 100644 index 00000000..1bb32888 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel2.jpg differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel2.png b/tests/reftest/images/lossless/jpeg_lossless_sel2.png new file mode 100644 index 00000000..8c9f9501 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel2.png differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel3.jpg b/tests/reftest/images/lossless/jpeg_lossless_sel3.jpg new file mode 100644 index 00000000..e0eb8d53 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel3.jpg differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel3.png b/tests/reftest/images/lossless/jpeg_lossless_sel3.png new file mode 100644 index 00000000..8c9f9501 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel3.png differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel4.jpg b/tests/reftest/images/lossless/jpeg_lossless_sel4.jpg new file mode 100644 index 00000000..5adb8caf Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel4.jpg differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel4.png b/tests/reftest/images/lossless/jpeg_lossless_sel4.png new file mode 100644 index 00000000..8c9f9501 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel4.png differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel5.jpg b/tests/reftest/images/lossless/jpeg_lossless_sel5.jpg new file mode 100644 index 00000000..0c98c307 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel5.jpg differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel5.png b/tests/reftest/images/lossless/jpeg_lossless_sel5.png new file mode 100644 index 00000000..8c9f9501 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel5.png differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel6.jpg b/tests/reftest/images/lossless/jpeg_lossless_sel6.jpg new file mode 100644 index 00000000..b513afd4 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel6.jpg differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel6.png b/tests/reftest/images/lossless/jpeg_lossless_sel6.png new file mode 100644 index 00000000..8c9f9501 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel6.png differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel7.jpg b/tests/reftest/images/lossless/jpeg_lossless_sel7.jpg new file mode 100644 index 00000000..c029159a Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel7.jpg differ diff --git a/tests/reftest/images/lossless/jpeg_lossless_sel7.png b/tests/reftest/images/lossless/jpeg_lossless_sel7.png new file mode 100644 index 00000000..8c9f9501 Binary files /dev/null and b/tests/reftest/images/lossless/jpeg_lossless_sel7.png differ diff --git a/tests/reftest/images/lossless/lossless16bit.jpg b/tests/reftest/images/lossless/lossless16bit.jpg new file mode 100644 index 00000000..dc63f3e5 Binary files /dev/null and b/tests/reftest/images/lossless/lossless16bit.jpg differ diff --git a/tests/reftest/images/lossless/lossless16bit.png b/tests/reftest/images/lossless/lossless16bit.png new file mode 100644 index 00000000..769fc546 Binary files /dev/null and b/tests/reftest/images/lossless/lossless16bit.png differ diff --git a/tests/reftest/mod.rs b/tests/reftest/mod.rs index aa5d6e2e..8182e35f 100644 --- a/tests/reftest/mod.rs +++ b/tests/reftest/mod.rs @@ -49,36 +49,55 @@ fn reftest_decoder(mut decoder: jpeg::Decoder, path: &Path, } let ref_file = File::open(ref_path).unwrap(); - let (ref_info, mut ref_reader) = png::Decoder::new(ref_file).read_info().expect("png failed to read info"); + let mut decoder = png::Decoder::new(ref_file); + + if pixel_format == jpeg::PixelFormat::L16 { + // disable the default 8bit output of png v0.16.8 (fixed in master branch of png) + decoder.set_transformations(png::Transformations::EXPAND); + } + + let (ref_info, mut ref_reader) = decoder.read_info().expect("png failed to read info"); assert_eq!(ref_info.width, info.width as u32); assert_eq!(ref_info.height, info.height as u32); - assert_eq!(ref_info.bit_depth, png::BitDepth::Eight); let mut ref_data = vec![0; ref_info.buffer_size()]; ref_reader.next_frame(&mut ref_data).expect("png decode failed"); let mut ref_pixel_format = ref_info.color_type; - if ref_pixel_format == png::ColorType::RGBA { + if ref_pixel_format == png::ColorType::RGBA { ref_data = rgba_to_rgb(&ref_data); ref_pixel_format = png::ColorType::RGB; } - match pixel_format { - jpeg::PixelFormat::L8 => assert_eq!(ref_pixel_format, png::ColorType::Grayscale), - jpeg::PixelFormat::RGB24 => assert_eq!(ref_pixel_format, png::ColorType::RGB), + let (refdata_16, data_u16) : (Vec, Vec) = match pixel_format { + jpeg::PixelFormat::L8 => { + assert_eq!(ref_pixel_format, png::ColorType::Grayscale); + assert_eq!(ref_info.bit_depth, png::BitDepth::Eight); + (ref_data.iter().map(|x| *x as u16).collect(), data.iter().map(|x| *x as u16).collect()) + }, + jpeg::PixelFormat::L16 => { + assert_eq!(ref_pixel_format, png::ColorType::Grayscale); + assert_eq!(ref_info.bit_depth, png::BitDepth::Sixteen); + (ref_data.chunks_exact(2).map(|a| u16::from_be_bytes([a[0],a[1]])).collect(), + data.chunks_exact(2).map(|a| u16::from_ne_bytes([a[0],a[1]])).collect()) + }, + jpeg::PixelFormat::RGB24 => { + assert_eq!(ref_pixel_format, png::ColorType::RGB); + assert_eq!(ref_info.bit_depth, png::BitDepth::Eight); + (ref_data.iter().map(|x| *x as u16).collect(), data.iter().map(|x| *x as u16).collect()) + }, _ => panic!(), - } - - assert_eq!(data.len(), ref_data.len()); + }; + assert_eq!(data_u16.len(), refdata_16.len()); let mut max_diff = 0; - let pixels: Vec = data.iter().zip(ref_data.iter()).map(|(&a, &b)| { - let diff = (a as i16 - b as i16).abs(); + let pixels: Vec = data_u16.iter().zip(refdata_16.iter()).map(|(&a, &b)| { + let diff = (a as isize - b as isize).abs(); max_diff = cmp::max(diff, max_diff); // FIXME: Only a diff of 1 should be allowed? - if diff <= 2 { + if (info.coding_process != jpeg::CodingProcess::Lossless && diff <= 2) || diff == 0 { // White for correct 0xFF } else {