diff --git a/dll/src/lib.rs b/dll/src/lib.rs index 9f4f645..7097329 100644 --- a/dll/src/lib.rs +++ b/dll/src/lib.rs @@ -924,8 +924,8 @@ mod tests { "iphonecrop2", "iphoneprogressive", "iphoneprogressive2", - "progressive_late_dht", // image has huffman tables that come very late which causes a verification failure - "out_of_order_dqt", // image with quanatization table dqt that comes after image definition SOF + "progressive_late_dht", + "out_of_order_dqt", "narrowrst", "nofsync", "slrcity", @@ -935,8 +935,8 @@ mod tests { "trailingrst", "trailingrst2", "trunc", - "eof_and_trailingrst", // the lepton format has a wrongly set unexpected eof and trailing rst - "eof_and_trailinghdrdata" // the lepton format has a wrongly set unexpected eof and trailing header data + "eof_and_trailingrst", + "eof_and_trailinghdrdata" )] file: &str, ) { diff --git a/images/README.md b/images/README.md new file mode 100644 index 0000000..e0e0a51 --- /dev/null +++ b/images/README.md @@ -0,0 +1,10 @@ +# Images + +- `dc_init_non_interleaved` is a progressive JPEG with DC initial scans that do not contain all components. Associated Lepton file produced by the original implementation. +- `eof_and_trailinghdrdata` the lepton format has a wrongly set unexpected EOF and trailing header data. +- `eof_and_trailingrst` the Lepton format has a wrongly set unexpected EOF and trailing RST. +- `non-interleaved-multiple-restart` is a sequential JPEG with non-interleaved scans and chroma subsampling. The restart interval is set to one MCU row, meaning that there are multiple restart intervals defined on the JPEG. Associated Lepton file produced by the original implementation. +- `out_of_order_dqt` has a quantization table that comes after the image definition SOF. +- `progressive_late_dht` has Huffman tables that come very late which causes a verification failure. +- `scan_order_reversed` is a sequential, non-interleaved JPEG where the first scan does not contain the first component. Associated Lepton file produced by the original implementation. +- `truncbad` the Lepton format is truncated and invalid. diff --git a/images/dc_init_non_interleaved.jpg b/images/dc_init_non_interleaved.jpg new file mode 100644 index 0000000..281f342 Binary files /dev/null and b/images/dc_init_non_interleaved.jpg differ diff --git a/images/dc_init_non_interleaved.lep b/images/dc_init_non_interleaved.lep new file mode 100644 index 0000000..e7423a1 Binary files /dev/null and b/images/dc_init_non_interleaved.lep differ diff --git a/images/non-interleaved-multiple-restart.jpg b/images/non-interleaved-multiple-restart.jpg new file mode 100644 index 0000000..fd1d677 Binary files /dev/null and b/images/non-interleaved-multiple-restart.jpg differ diff --git a/images/non-interleaved-multiple-restart.lep b/images/non-interleaved-multiple-restart.lep new file mode 100644 index 0000000..e0924ff Binary files /dev/null and b/images/non-interleaved-multiple-restart.lep differ diff --git a/images/scan_order_reversed.jpg b/images/scan_order_reversed.jpg new file mode 100644 index 0000000..48dfacb Binary files /dev/null and b/images/scan_order_reversed.jpg differ diff --git a/images/scan_order_reversed.lep b/images/scan_order_reversed.lep new file mode 100644 index 0000000..e3a810b Binary files /dev/null and b/images/scan_order_reversed.lep differ diff --git a/lib/src/consts.rs b/lib/src/consts.rs index 6f9b338..853c5d5 100644 --- a/lib/src/consts.rs +++ b/lib/src/consts.rs @@ -85,8 +85,8 @@ pub const MAX_THREADS_SUPPORTED_BY_LEPTON_FORMAT: usize = 16; // Number of threa pub const EOI: [u8; 2] = [0xFF, jpeg_code::EOI]; // EOI segment pub const SOI: [u8; 2] = [0xFF, jpeg_code::SOI]; // SOI segment pub const LEPTON_FILE_HEADER: [u8; 2] = [0xcf, 0x84]; // the tau symbol for a tau lepton in utf-8 -pub const LEPTON_HEADER_BASELINE_JPEG_TYPE: [u8; 1] = [b'Z']; -pub const LEPTON_HEADER_PROGRESSIVE_JPEG_TYPE: [u8; 1] = [b'X']; +pub const LEPTON_HEADER_SEQUENTIAL_SINGLE_SCAN_JPEG_TYPE: [u8; 1] = [b'Z']; +pub const LEPTON_HEADER_MULTI_SCAN_JPEG_TYPE: [u8; 1] = [b'X']; pub const LEPTON_HEADER_MARKER: [u8; 3] = *b"HDR"; pub const LEPTON_HEADER_PAD_MARKER: [u8; 3] = *b"P0D"; pub const LEPTON_HEADER_JPG_RESTARTS_MARKER: [u8; 3] = *b"CRS"; diff --git a/lib/src/jpeg/bit_reader.rs b/lib/src/jpeg/bit_reader.rs index 26be200..ffb7482 100644 --- a/lib/src/jpeg/bit_reader.rs +++ b/lib/src/jpeg/bit_reader.rs @@ -245,12 +245,33 @@ impl BitReader { // can recode it again just by remembering the pad bit. self.undo_read_ahead(); + if self.inner.fill_buf()?.is_empty() { + self.eof = true; + return Ok(()); + } + let mut h = [0u8; 2]; - self.inner.read_exact(&mut h)?; - if h[0] != 0xff || h[1] != (jpeg_code::RST0 + (self.cpos as u8 & 7)) { + self.inner.read_exact(&mut h[0..1])?; + if h[0] != 0xff { + return err_exit_code( + ExitCode::InvalidResetCode, + format!( + "reset code does not start with 0xff but with 0x{:x} found in stream", + h[0] + ), + ); + } + + if self.inner.fill_buf()?.is_empty() { + self.eof = true; + return Ok(()); + } + + self.inner.read_exact(&mut h[1..2])?; + if h[1] != (jpeg_code::RST0 + (self.cpos as u8 & 7)) { return err_exit_code( ExitCode::InvalidResetCode, - format!("invalid reset code {0:x} {1:x} found in stream", h[0], h[1]), + format!("invalid reset code 0xff {:x} found in stream", h[1]), ); } diff --git a/lib/src/jpeg/jpeg_header.rs b/lib/src/jpeg/jpeg_header.rs index 10ed3f2..d18f151 100644 --- a/lib/src/jpeg/jpeg_header.rs +++ b/lib/src/jpeg/jpeg_header.rs @@ -33,11 +33,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ use std::fmt::Debug; -use std::io::{Cursor, Read, Write}; +use std::io::{BufRead, Cursor, Read, Write}; use std::num::NonZeroU32; use crate::LeptonError; -use crate::consts::JpegType; +use crate::consts::{EOI, JpegType}; use crate::enabled_features::EnabledFeatures; use crate::helpers::*; use crate::lepton_error::{AddContext, ExitCode, Result, err_exit_code}; @@ -88,17 +88,20 @@ impl RestartSegmentCodingInfo { pub struct ReconstructionInfo { /// the maximum component in a truncated progressive image. /// - /// This is meant to be used for progressive images but is not yet implemented. + /// This is meant to be used for images that are sequential with multiple scans or are + /// progressive, but was never implemented in the original codebase, nor is it implemented here. pub max_cmp: u32, /// the maximum band in a truncated progressive image /// - /// This is meant to be used for progressive images but is not yet implemented. + /// This is meant to be used for images that are sequential with multiple scans or are + /// progressive, but was never implemented in the original codebase, nor is it implemented here. pub max_bpos: u32, /// The maximum bit in a truncated progressive image. /// - /// This is meant to be used for progressive images but is not yet implemented. + /// This is meant to be used for images that are sequential with multiple scans or are + /// progressive, but was never implemented in the original codebase, nor is it implemented here. pub max_sah: u8, /// the maximum dpos in a truncated image @@ -137,7 +140,7 @@ pub struct ReconstructionInfo { pub garbage_data: Vec, } -pub fn parse_jpeg_header( +pub fn parse_jpeg_header( reader: &mut R, enabled_features: &EnabledFeatures, jpeg_header: &mut JpegHeader, @@ -152,19 +155,9 @@ pub fn parse_jpeg_header( let mut mirror = Mirror::new(reader, &mut output_cursor); - if jpeg_header.parse(&mut mirror, enabled_features).context()? { - // append the header if it was not the end of file marker - rinfo.raw_jpeg_header.append(&mut output); - return Ok(true); - } else { - // if the output was more than 2 bytes then was a trailing header, so keep that around as well, - // but we don't want the EOI since that goes into the garbage data. - if output.len() > 2 { - rinfo.raw_jpeg_header.extend(&output[0..output.len() - 2]); - } - - return Ok(false); - } + let hit_sos = jpeg_header.parse(&mut mirror, enabled_features).context()?; + rinfo.raw_jpeg_header.append(&mut output); + return Ok(hit_sos); } // internal utility we use to collect the header that we read for later @@ -193,6 +186,16 @@ impl Read for Mirror<'_, R, W> { } } +impl BufRead for Mirror<'_, R, W> { + fn fill_buf(&mut self) -> std::io::Result<&[u8]> { + self.read.fill_buf() + } + + fn consume(&mut self, amt: usize) { + self.read.consume(amt); + } +} + #[derive(Copy, Clone, Debug)] pub(crate) struct HuffCodes { pub c_val: [u16; 256], @@ -546,10 +549,13 @@ impl Default for JpegHeader { } impl JpegHeader { - /// true if this image is a single scan, which can be partitioned and decode + /// True if this image is sequential with a single scan, which can be partitioned and decoded /// completely independently by separate threads. If this is not the case, then /// we need to decode the entire image in memory and then encode the JPEG sequentially. - pub fn is_single_scan(&self) -> bool { + /// In theory progressive images with a single scan (only first DC scan) can get the same + /// treatment, though such images practically do not exist. The original codebase does not + /// give such images this treatment, and doing so would likely cause incompatibility. + pub fn is_sequential_single_scan(&self) -> bool { assert!(self.jpeg_type != JpegType::Unknown); self.jpeg_type == JpegType::Sequential && self.cmpc == self.cs_cmpc @@ -579,7 +585,7 @@ impl JpegHeader { /// until we hit either an SOS (image data) or EOI (end of image). /// /// Returns false if we hit EOI, true if we have an image to process. - pub fn parse( + pub fn parse( &mut self, reader: &mut R, enabled_features: &EnabledFeatures, @@ -695,13 +701,24 @@ impl JpegHeader { } // returns true we should continue parsing headers or false if we hit SOS and should stop - fn parse_next_segment( + fn parse_next_segment( &mut self, reader: &mut R, enabled_features: &EnabledFeatures, ) -> Result { let mut header = [0u8; 4]; + if reader.fill_buf()?.starts_with(&EOI) { + // don't consume eoi + return Ok(ParseSegmentResult::EOI); + } + + if reader.fill_buf()?.len() < 2 { + // if it's less than two remaining and not EOI, + // it's garbage and don't consume it + return Ok(ParseSegmentResult::EOI); + } + if reader.read(&mut header[0..1]).context()? == 0 { // didn't get an EOI return Ok(ParseSegmentResult::EOI); diff --git a/lib/src/jpeg/jpeg_position_state.rs b/lib/src/jpeg/jpeg_position_state.rs index bca4992..6f0edd8 100644 --- a/lib/src/jpeg/jpeg_position_state.rs +++ b/lib/src/jpeg/jpeg_position_state.rs @@ -4,7 +4,7 @@ * This software incorporates material from third parties. See NOTICE.txt for details. *--------------------------------------------------------------------------------------------*/ -use crate::consts::{JpegDecodeStatus, JpegType}; +use crate::consts::JpegDecodeStatus; use crate::lepton_error::{AddContext, ExitCode, err_exit_code}; use crate::{LeptonError, Result}; @@ -86,6 +86,11 @@ impl JpegPositionState { self.prev_eobrun = 0; } + pub fn trim_mcu(&mut self, jf: &JpegHeader) { + // After updating dpos, update the current MCU to be a fraction of that. + self.mcu = self.dpos / (jf.cmp_info[self.cmp].sfv * jf.cmp_info[self.cmp].sfh); + } + /// calculates next position (non interleaved) fn next_mcu_pos_noninterleaved(&mut self, jf: &JpegHeader) -> JpegDecodeStatus { // increment position @@ -103,11 +108,6 @@ impl JpegPositionState { self.dpos = cmp_info.bc; } - // now we've updated dpos, update the current MCU to be a fraction of that - if jf.jpeg_type == JpegType::Sequential { - self.mcu = self.dpos / (cmp_info.sfv * cmp_info.sfh); - } - // check position if self.dpos >= cmp_info.bc { return JpegDecodeStatus::ScanCompleted; diff --git a/lib/src/jpeg/jpeg_read.rs b/lib/src/jpeg/jpeg_read.rs index 8979876..e8a7bf5 100644 --- a/lib/src/jpeg/jpeg_read.rs +++ b/lib/src/jpeg/jpeg_read.rs @@ -32,7 +32,7 @@ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -use std::cmp::{self, max}; +use std::cmp::{self}; use std::io::{BufRead, Read, Seek, SeekFrom}; use crate::helpers::*; @@ -59,18 +59,18 @@ use super::jpeg_position_state::JpegPositionState; /// DCT coefficients for each block in the image (we do not perform inverse DCT, this would be lossy). /// In addition, we return a vector of `RestartSegmentCodingInfo` which contains the information /// needed to reconstruct a portion of the JPEG file starting at the given offset. This is useful -/// for baseline images where we can split the image into sections and decode them in parallel. +/// for sequential single-scan images where we can split the image into sections and decode them in parallel. /// /// The callback function is called with the JPEG header information after it has been parsed, and -/// is useful for debugging or logging purposes. Progressive images will contain multiple scans and -/// call the callback multiple times. +/// is useful for debugging or logging purposes. Images that contain multiple scans will call the +/// callback multiple times. /// -/// Non-progressive images support the idea of truncating the image (since this happens frequently) -/// where the bitstream is cut off at an arbitrary point. We assume that all subsequent data is zero, -/// but remember enough to reconstruct the bitstream until there. +/// Truncation, where the bitstream is cut off at an arbitrary point, is supported when it occurs in a scan. +/// In the case of truncation, we assume that all subsequent data is zero, but remember enough to reconstruct the +/// bitstream until there. /// /// There is also the concept of "garbage data" which is what comes after the scan data but is not -/// recognized as a header. This garbage data should be appeneded to the end of the file. +/// recognized as a header. This garbage data should be appended to the end of the file. pub fn read_jpeg_file( reader: &mut R, jpeg_header: &mut JpegHeader, @@ -97,7 +97,7 @@ pub fn read_jpeg_file( on_header_callback(jpeg_header, &rinfo.raw_jpeg_header); - if !enabled_features.progressive && !jpeg_header.is_single_scan() { + if !enabled_features.progressive && !jpeg_header.is_sequential_single_scan() { return err_exit_code( ExitCode::ProgressiveUnsupported, "file is progressive or contains multiple scans, but this is disabled", @@ -125,62 +125,92 @@ pub fn read_jpeg_file( )?); } - let start_scan_position = reader.stream_position()?; - + // Only the first scan needs to write the handoffs. Sequential single scan files will write the full + // handoff including information to start decoding at an arbitrary location. Other files will just + // write the luminance bounds. let mut partitions = Vec::new(); - read_first_scan( + read_scan( &jpeg_header, reader, - &mut partitions, + &mut Some(&mut partitions), &mut image_data[..], rinfo, ) .context()?; let mut end_scan_position = reader.stream_position()?; - if start_scan_position + 2 > end_scan_position { - return err_exit_code(ExitCode::UnsupportedJpeg, "no scan data found in JPEG file") - .context(); + // If it is known that no more scans are expected (when `is_sequential_single_scan`), do not call + // `prepare_to_decode_next_scan` but instead treat all remaining as garbage. In principle + // `prepare_to_decode_next_scan` can be modified to support many cases of truncation, but until + // that is the case, distinguishing here with an if-statement here allows to support more types of + // truncated sequential single scan images. + if !jpeg_header.is_sequential_single_scan() { + // For images with multiple scans, loop around reading headers and decoding until we complete image_data. + let mut prev_raw_jpeg_header_len = rinfo.raw_jpeg_header.len(); + while prepare_to_decode_next_scan(jpeg_header, rinfo, reader, enabled_features).context()? { + on_header_callback( + jpeg_header, + &&rinfo.raw_jpeg_header[prev_raw_jpeg_header_len..], + ); + prev_raw_jpeg_header_len = rinfo.raw_jpeg_header.len(); + + if rinfo.early_eof_encountered { + break; + } + + // Partitions only need to be written for the first scan. + read_scan( + &jpeg_header, + reader, + &mut None, /* partitions */ + &mut image_data[..], + rinfo, + ) + .context()?; + + end_scan_position = reader.stream_position()?; + + if rinfo.early_eof_encountered { + break; + } + } } - if partitions.len() == 0 { - return err_exit_code( - ExitCode::UnsupportedJpeg, - "no scan information found in JPEG file", - ) - .context(); + // We must distinguish the case where there are no bytes remaining. If this is the case, + // it is treated as an EEE and the garbage must contain the previous two actual bytes. + // If no such distinction is made, the garbage will be empty which is interpreted as containing + // the two-byte EOI. + if reader.fill_buf()?.is_empty() { + rinfo.early_eof_encountered = true; } - if jpeg_header.is_single_scan() { - if rinfo.early_eof_encountered { - if enabled_features.stop_reading_at_eoi { - return err_exit_code(ExitCode::ShortRead, "early EOF encountered"); - } + if rinfo.early_eof_encountered { + if enabled_features.stop_reading_at_eoi { + return err_exit_code(ExitCode::ShortRead, "early EOF encountered"); + } - rinfo - .truncate_components - .set_truncation_bounds(&jpeg_header, rinfo.max_dpos); - - // If we got an early EOF, then seek backwards and capture the last two bytes and store them as garbage. - // This is necessary since the decoder will assume that zero garbage always means a properly terminated JPEG - // even if early EOF was set to true. - end_scan_position = reader.seek(SeekFrom::Current(-2))?.try_into().unwrap(); - - // make sure we don't return any partitions that are beyond the - // adjusted end position - for i in 0..partitions.len() { - if partitions[i].0 >= end_scan_position { - return err_exit_code( - ExitCode::UnsupportedJpeg, - "Partition conflicts with garbage data", - ); - } - } + rinfo + .truncate_components + .set_truncation_bounds(&jpeg_header, rinfo.max_dpos); - rinfo.garbage_data.resize(2, 0); - reader.read_exact(&mut rinfo.garbage_data)?; + // If we got an early EOF, then seek backwards and capture the last two bytes and store them as garbage. + // This is necessary since the decoder will assume that zero garbage always means a properly terminated JPEG + // even if early EOF was set to true. + end_scan_position = reader.seek(SeekFrom::Current(-2))?.try_into().unwrap(); + + // Make sure we do not return any partitions that are beyond the adjusted end position. + for i in 0..partitions.len() { + if partitions[i].0 >= end_scan_position { + return err_exit_code( + ExitCode::UnsupportedJpeg, + "Partition conflicts with garbage data", + ); + } } + rinfo.garbage_data.resize(2, 0); + reader.read_exact(&mut rinfo.garbage_data)?; + } else { if enabled_features.stop_reading_at_eoi { // ensure there is an actual EOI marker since we haven't consumed it yet let mut end_of_file = [0u8; 2]; @@ -199,59 +229,21 @@ pub fn read_jpeg_file( // read the rest of the file to garbage data reader.read_to_end(&mut rinfo.garbage_data).context()?; } - } else { - assert!(jpeg_header.jpeg_type != JpegType::Unknown); - - if rinfo.early_eof_encountered { - return err_exit_code( - ExitCode::UnsupportedJpeg, - "truncation is only supported for baseline images", - ) - .context(); - } - - // for progressive images, loop around reading headers and decoding until we a complete image_data - let mut prev_raw_jpeg_header_len = rinfo.raw_jpeg_header.len(); - - while prepare_to_decode_next_scan(jpeg_header, rinfo, reader, enabled_features).context()? { - on_header_callback( - jpeg_header, - &&rinfo.raw_jpeg_header[prev_raw_jpeg_header_len..], - ); - prev_raw_jpeg_header_len = rinfo.raw_jpeg_header.len(); - - read_progressive_scan(&jpeg_header, reader, &mut image_data[..], rinfo).context()?; - - if rinfo.early_eof_encountered { - return err_exit_code( - ExitCode::UnsupportedJpeg, - "truncation is only supported for baseline images", - ) - .context(); - } - } - - end_scan_position = reader.stream_position()?; - - // Since prepare_to_decode_next_scan consumed the EOI, - // we need to add EOI to the beginning of the garbage data (if there is any). - // - // If there was actually no garbage data, this is still ok since - // the marker will be appended, then removed when file gets truncated by the - // overall file limit. - rinfo.garbage_data = Vec::from(EOI); + } - if !enabled_features.stop_reading_at_eoi { - // append the rest of the file to the buffer - reader.read_to_end(&mut rinfo.garbage_data).context()?; - } + if partitions.len() == 0 { + return err_exit_code( + ExitCode::UnsupportedJpeg, + "no scan information found in JPEG file", + ) + .context(); } Ok((image_data, partitions, end_scan_position)) } // false means we hit the end of file marker -fn prepare_to_decode_next_scan( +fn prepare_to_decode_next_scan( jpeg_header: &mut JpegHeader, rinfo: &mut ReconstructionInfo, reader: &mut R, @@ -277,15 +269,14 @@ fn prepare_to_decode_next_scan( return Ok(true); } -/// Reads the scan from the JPEG file, writes the image data to the image_data array and +/// Reads a scan from the JPEG file, writes the image data to the image_data array and /// partitions it into restart segments using the partition callback. -/// -/// This only works for sequential JPEGs or the first scan in a progressive image. -/// For subsequent scans, use the `read_progressive_scan`. -fn read_first_scan( +/// Writing partitions should only be done for the first scan, which can only be a +/// sequential one or a DC initial scan. +fn read_scan( jf: &JpegHeader, reader: &mut R, - partitions: &mut Vec<(u64, RestartSegmentCodingInfo)>, + partitions: &mut Option<&mut Vec<(u64, RestartSegmentCodingInfo)>>, image_data: &mut [BlockBasedImage], reconstruct_info: &mut ReconstructionInfo, ) -> Result<()> { @@ -303,7 +294,7 @@ fn read_first_scan( state.reset_rstw(jf); // restart wait counter if jf.jpeg_type == JpegType::Sequential { - sta = decode_baseline_rst( + sta = decode_sequential_rst( &mut state, &mut bit_reader, image_data, @@ -314,6 +305,8 @@ fn read_first_scan( ) .context()?; } else if jf.cs_to == 0 && jf.cs_sah == 0 { + // ---> DC successive approximation first stage <--- + // only need DC jf.verify_huffman_table(true, false).context()?; @@ -328,15 +321,20 @@ fn read_first_scan( // // TODO: get rid of this and just chop up the scan into sections in Lepton code if do_handoff { - partitions.push(( - 0, - RestartSegmentCodingInfo::new(0, 0, [0; 4], state.get_mcu(), jf), - )); + if let Some(parts) = partitions { + parts.push(( + 0, + RestartSegmentCodingInfo::new(0, 0, [0; 4], state.get_mcu(), jf), + )); + } do_handoff = false; } - // ---> succesive approximation first stage <--- + if !bit_reader.is_eof() { + reconstruct_info.max_dpos[state.get_cmp()] = + cmp::max(state.get_dpos(), reconstruct_info.max_dpos[state.get_cmp()]); + } // diff coding & bitshifting for dc let coef = read_dc(&mut bit_reader, jf.get_huff_dc_tree(state.get_cmp()))?; @@ -352,65 +350,14 @@ fn read_first_scan( if state.get_mcu() % jf.mcuh == 0 && old_mcu != state.get_mcu() { do_handoff = true; } - } - } else { - return err_exit_code( - ExitCode::UnsupportedJpeg, - "progress must start with DC stage", - ) - .context(); - } - - // if we saw a pad bit at the end of the block, then remember whether they were 1s or 0s. This - // will be used later on to reconstruct the padding - bit_reader - .read_and_verify_fill_bits(&mut reconstruct_info.pad_bit) - .context()?; - - // verify that we got the right RST code here since the above should do 1 mcu. - // If we didn't then we won't re-encode the file binary identical so there's no point in continuing - if sta == JpegDecodeStatus::RestartIntervalExpired { - bit_reader.verify_reset_code().context()?; - - sta = JpegDecodeStatus::DecodeInProgress; - } - } - - Ok(()) -} - -/// Reads a scan for progressive images where the image is encoded in multiple passes. -/// Between each scan are a bunch of header than need to be parsed containing information -/// like updated Huffman tables and quantization tables. -fn read_progressive_scan( - jf: &JpegHeader, - reader: &mut R, - image_data: &mut [BlockBasedImage], - reconstruct_info: &mut ReconstructionInfo, -) -> Result<()> { - // track to see how far we got in progressive encoding in case of truncated images, however this - // was never actually implemented in the original C++ code - reconstruct_info.max_sah = max(reconstruct_info.max_sah, max(jf.cs_sal, jf.cs_sah)); - - let mut bit_reader = BitReader::new(reader); - - // init variables for decoding - let mut state = JpegPositionState::new(jf, 0); - - // JPEG imagedata decoding routines - let mut sta = JpegDecodeStatus::DecodeInProgress; - while sta != JpegDecodeStatus::ScanCompleted { - // decoding for interleaved data - state.reset_rstw(&jf); // restart wait counter - if jf.cs_to == 0 { - if jf.cs_sah == 0 { - return err_exit_code( - ExitCode::UnsupportedJpeg, - "progress can't have two DC first stages", - ) - .context(); + if bit_reader.is_eof() { + sta = JpegDecodeStatus::ScanCompleted; + reconstruct_info.early_eof_encountered = true; + } } + } else if jf.cs_to == 0 { + // ---> DC successive approximation later stage <--- // only need DC jf.verify_huffman_table(true, false).context()?; @@ -431,125 +378,127 @@ fn read_progressive_scan( ); sta = state.next_mcu_pos(jf); - } - } else { - // ---> progressive AC encoding <--- - if jf.cs_from == 0 || jf.cs_to >= 64 || jf.cs_from >= jf.cs_to { - return err_exit_code( - ExitCode::UnsupportedJpeg, - format!( - "progressive encoding range was invalid {0} to {1}", - jf.cs_from, jf.cs_to - ), - ); + if bit_reader.is_eof() { + sta = JpegDecodeStatus::ScanCompleted; + reconstruct_info.early_eof_encountered = true; + } } + } else if jf.cs_sah == 0 { + // ---> AC successive approximation first stage <--- // only need AC jf.verify_huffman_table(false, true).context()?; - if jf.cs_sah == 0 { - if jf.cs_cmpc != 1 { - return err_exit_code( - ExitCode::UnsupportedJpeg, - "Progressive AC encoding cannot be interleaved", - ); - } - - // ---> succesive approximation first stage <--- - let mut block = [0; 64]; + let mut block = [0; 64]; - while sta == JpegDecodeStatus::DecodeInProgress { - let current_block = image_data[state.get_cmp()].get_block_mut(state.get_dpos()); - - if state.eobrun == 0 { - // only need to do something if we are not in a zero-block run - let eob = decode_ac_prg_fs( - &mut bit_reader, - jf.get_huff_ac_tree(state.get_cmp()), - &mut block, - &mut state, - jf.cs_from, - jf.cs_to, - ) - .context()?; + while sta == JpegDecodeStatus::DecodeInProgress { + let current_block = image_data[state.get_cmp()].get_block_mut(state.get_dpos()); + if state.eobrun == 0 { + // only need to do something if we are not in a zero-block run + let eob = decode_ac_prg_fs( + &mut bit_reader, + jf.get_huff_ac_tree(state.get_cmp()), + &mut block, + &mut state, + jf.cs_from, + jf.cs_to, + ) + .context()?; + + if !bit_reader.is_eof() { state .check_optimal_eobrun( eob == jf.cs_from, jf.get_huff_ac_codes(state.get_cmp()), ) .context()?; + } - for bpos in jf.cs_from..eob { - current_block.set_transposed_from_zigzag( - usize::from(bpos), - block[usize::from(bpos)] << jf.cs_sal, - ); - } + for bpos in jf.cs_from..eob { + current_block.set_transposed_from_zigzag( + usize::from(bpos), + block[usize::from(bpos)] << jf.cs_sal, + ); } + } - sta = state.skip_eobrun(&jf).context()?; + sta = state.skip_eobrun(&jf).context()?; - // proceed only if no error encountered - if sta == JpegDecodeStatus::DecodeInProgress { - sta = state.next_mcu_pos(jf); - } + // proceed only if no error encountered + if sta == JpegDecodeStatus::DecodeInProgress { + sta = state.next_mcu_pos(jf); } - } else { - // ---> succesive approximation later stage <--- - let mut block = [0; 64]; + if bit_reader.is_eof() { + sta = JpegDecodeStatus::ScanCompleted; + reconstruct_info.early_eof_encountered = true; + } + } + } else { + // ---> AC successive approximation later stage <--- + + // only need AC + jf.verify_huffman_table(false, true).context()?; - while sta == JpegDecodeStatus::DecodeInProgress { - let current_block = image_data[state.get_cmp()].get_block_mut(state.get_dpos()); + let mut block = [0; 64]; - for bpos in jf.cs_from..jf.cs_to + 1 { - block[usize::from(bpos)] = - current_block.get_transposed_from_zigzag(usize::from(bpos)); - } + while sta == JpegDecodeStatus::DecodeInProgress { + let current_block = image_data[state.get_cmp()].get_block_mut(state.get_dpos()); - if state.eobrun == 0 { - // decode block (long routine) - let eob = decode_ac_prg_sa( - &mut bit_reader, - jf.get_huff_ac_tree(state.get_cmp()), - &mut block, - &mut state, - jf.cs_from, - jf.cs_to, - ) - .context()?; + for bpos in jf.cs_from..jf.cs_to + 1 { + block[usize::from(bpos)] = + current_block.get_transposed_from_zigzag(usize::from(bpos)); + } + if state.eobrun == 0 { + // decode block (long routine) + let eob = decode_ac_prg_sa( + &mut bit_reader, + jf.get_huff_ac_tree(state.get_cmp()), + &mut block, + &mut state, + jf.cs_from, + jf.cs_to, + ) + .context()?; + + if !bit_reader.is_eof() { state .check_optimal_eobrun( eob == jf.cs_from, jf.get_huff_ac_codes(state.get_cmp()), ) .context()?; - } else { - // decode zero run block (short routine) - decode_eobrun_sa( - &mut bit_reader, - &mut block, - &mut state, - jf.cs_from, - jf.cs_to, - ) - .context()?; } + } else { + // decode zero run block (short routine) + decode_eobrun_sa( + &mut bit_reader, + &mut block, + &mut state, + jf.cs_from, + jf.cs_to, + ) + .context()?; + } - // copy back to colldata - for bpos in jf.cs_from..jf.cs_to + 1 { - current_block.set_transposed_from_zigzag( - usize::from(bpos), - current_block - .get_transposed_from_zigzag(usize::from(bpos)) - .wrapping_add(block[usize::from(bpos)] << jf.cs_sal), - ); - } + // copy back to colldata + for bpos in jf.cs_from..jf.cs_to + 1 { + current_block.set_transposed_from_zigzag( + usize::from(bpos), + current_block + .get_transposed_from_zigzag(usize::from(bpos)) + .wrapping_add(block[usize::from(bpos)] << jf.cs_sal), + ); + } - sta = state.next_mcu_pos(jf); + sta = state.next_mcu_pos(jf); + + if bit_reader.is_eof() { + sta = JpegDecodeStatus::ScanCompleted; + reconstruct_info.early_eof_encountered = true; } } } @@ -573,14 +522,14 @@ fn read_progressive_scan( } /// reads an entire interval until the RST code -fn decode_baseline_rst( +fn decode_sequential_rst( state: &mut JpegPositionState, bit_reader: &mut BitReader, image_data: &mut [BlockBasedImage], do_handoff: &mut bool, jpeg_header: &JpegHeader, reconstruct_info: &mut ReconstructionInfo, - partitions: &mut Vec<(u64, RestartSegmentCodingInfo)>, + partitions: &mut Option<&mut Vec<(u64, RestartSegmentCodingInfo)>>, ) -> Result { // should have both AC and DC components jpeg_header.verify_huffman_table(true, true).context()?; @@ -592,16 +541,18 @@ fn decode_baseline_rst( if *do_handoff { let (bits_already_read, byte_being_read) = bit_reader.overhang(); - partitions.push(( - bit_reader.stream_position(), - RestartSegmentCodingInfo::new( - byte_being_read, - bits_already_read, - lastdc, - state.get_mcu(), - &jpeg_header, - ), - )); + if let Some(parts) = partitions { + parts.push(( + bit_reader.stream_position(), + RestartSegmentCodingInfo::new( + byte_being_read, + bits_already_read, + lastdc, + state.get_mcu(), + &jpeg_header, + ), + )); + } *do_handoff = false; } @@ -641,6 +592,13 @@ fn decode_baseline_rst( let old_mcu = state.get_mcu(); sta = state.next_mcu_pos(&jpeg_header); + // Correct the MCU position if there is only a single component in a sequential scan. + // Note that JPEG writing only performs this if there is a single component altogether, + // in accordance with the original Lepton implementation. + if jpeg_header.cs_cmpc == 1 { + state.trim_mcu(jpeg_header); + } + if state.get_mcu() % jpeg_header.mcuh == 0 && old_mcu != state.get_mcu() { *do_handoff = true; } @@ -815,7 +773,7 @@ fn decode_ac_prg_fs( let mut z = l; let s = r; let n = bit_reader.read(u32::from(s))?; - if (z + bpos) > to { + if (z + bpos) > to && !bit_reader.is_eof() { return err_exit_code(ExitCode::UnsupportedJpeg, "run is too long"); } @@ -975,38 +933,81 @@ mod tests { let mut file = read_file(filename, ".jpg"); let mut enabled_features = crate::EnabledFeatures::compat_lepton_scalar_read(); - let mut cursor = std::io::Cursor::new(&file); - let (rinfo, _jh) = read_jpeg(&mut cursor, &enabled_features); + { + let mut cursor = std::io::Cursor::new(&file); + let (rinfo, _jh) = read_jpeg(&mut cursor, &enabled_features); - assert_eq!( - &rinfo.garbage_data[..], - [0xff, 0xd9], - "Expected garbage data to match what was written" - ); + assert_eq!( + &rinfo.garbage_data[..], + [0xff, 0xd9], + "Expected garbage data to match what was written" + ); + } + { + // now add some garbage data to the end of the file + file.extend_from_slice(b"hi"); // EOI + some garbage - // now add some garbage data to the end of the file - file.extend_from_slice(b"hi"); // EOI + some garbage + let mut cursor = std::io::Cursor::new(&file); + let (rinfo, _jh) = read_jpeg(&mut cursor, &enabled_features); - let mut cursor = std::io::Cursor::new(&file); - let (rinfo, _jh) = read_jpeg(&mut cursor, &enabled_features); + assert_eq!( + &rinfo.garbage_data[..], + [0xff, 0xd9, b'h', b'i'], + "Expected garbage data to match what was written" + ); + } + { + enabled_features.stop_reading_at_eoi = true; + let mut cursor = std::io::Cursor::new(&file); + let (rinfo, _jh) = read_jpeg(&mut cursor, &enabled_features); + + assert_eq!(cursor.position(), file.len() as u64 - 2); + assert_eq!( + &rinfo.garbage_data[..], + [0xff, 0xd9], + "Expected garbage data to match what was written when stop_reading_at_eoi is true" + ); + enabled_features.stop_reading_at_eoi = false; + } + { + // remove the garbage and also half of the eoi marker + file.truncate(file.len() - 3); - assert_eq!( - &rinfo.garbage_data[..], - [0xff, 0xd9, b'h', b'i'], - "Expected garbage data to match what was written" - ); + let mut cursor = std::io::Cursor::new(&file); + let (rinfo, _jh) = read_jpeg(&mut cursor, &enabled_features); - enabled_features.stop_reading_at_eoi = true; - let mut cursor = std::io::Cursor::new(&file); - let (rinfo, _jh) = read_jpeg(&mut cursor, &enabled_features); + assert_eq!( + &rinfo.garbage_data[..], + [0xff], + "Expected garbage data to match what was written" + ); + } + { + // remove another byte, resulting in no eoi marker + file.truncate(file.len() - 1); - assert_eq!(cursor.position(), file.len() as u64 - 2); + let mut cursor = std::io::Cursor::new(&file); + let (rinfo, _jh) = read_jpeg(&mut cursor, &enabled_features); - assert_eq!( - &rinfo.garbage_data[..], - [0xff, 0xd9], - "Expected garbage data to match what was written when stop_reading_at_eoi is true" - ); + assert_eq!( + &rinfo.garbage_data[..], + &file[file.len() - 2..], + "In the absence of EOI, expected garbage data to match the final two scan bytes" + ); + } + { + // remove five more bytes + file.truncate(file.len() - 5); + + let mut cursor = std::io::Cursor::new(&file); + let (rinfo, _jh) = read_jpeg(&mut cursor, &enabled_features); + + assert_eq!( + &rinfo.garbage_data[..], + &file[file.len() - 2..], + "Upon truncation, expected garbage data to match the final two scan bytes" + ); + } } /// test function to read a JPEG file and returns the reconstruction info and JPEG header diff --git a/lib/src/jpeg/jpeg_write.rs b/lib/src/jpeg/jpeg_write.rs index 5efdae7..e8d3c7a 100644 --- a/lib/src/jpeg/jpeg_write.rs +++ b/lib/src/jpeg/jpeg_write.rs @@ -205,6 +205,13 @@ fn recode_one_mcu_row( ); sta = state.next_mcu_pos(&jf); + + // Correct the MCU position if there is only a single component in the frame. + // Note that JPEG reading performs this for every scan that has only a single component, + // in accordance with the original Lepton implementation. + if jf.cmpc == 1 { + state.trim_mcu(jf); + } } else if jf.cs_to == 0 { // ---> progressive DC encoding <--- if jf.cs_sah == 0 { @@ -794,7 +801,7 @@ mod tests { let mut reconstructed = Vec::new(); reconstructed.extend_from_slice(&SOI); - if jpeg_header.is_single_scan() { + if jpeg_header.is_sequential_single_scan() { // sequential JPEG consists of a single header + scan reconstructed.extend_from_slice(rinfo.raw_jpeg_header.as_slice()); @@ -818,7 +825,7 @@ mod tests { reconstructed.extend_from_slice(&EOI); } else { - // progressive JPEG consists of header + scan, header + scan, etc + // multi-scan JPEG consists of header + scan, header + scan, etc let mut scnc = 0; for (jh, raw_header) in headers { @@ -833,10 +840,10 @@ mod tests { scnc += 1; } - reconstructed.extend_from_slice(&EOI); - // progressive includes EOI in the scan assert_eq!(reconstructed.len(), end_scan_position as usize); + + reconstructed.extend_from_slice(&EOI); } reconstructed diff --git a/lib/src/structs/lepton_file_reader.rs b/lib/src/structs/lepton_file_reader.rs index aa6af9b..84fa4bb 100644 --- a/lib/src/structs/lepton_file_reader.rs +++ b/lib/src/structs/lepton_file_reader.rs @@ -534,7 +534,7 @@ impl<'a> LeptonFileReader<'a> { // the jpeg decoding. This permits multiple scans that are each encoded in two cases: // - progressive images // - baseline multiscan images (rare but permitted) - Ok(if !lh.jpeg_header.is_single_scan() { + Ok(if !lh.jpeg_header.is_sequential_single_scan() { let mux = Self::run_lepton_decoder_threads( lh, enabled_features, diff --git a/lib/src/structs/lepton_header.rs b/lib/src/structs/lepton_header.rs index 5d2699f..5113f4e 100644 --- a/lib/src/structs/lepton_header.rs +++ b/lib/src/structs/lepton_header.rs @@ -69,8 +69,8 @@ impl LeptonHeader { format!("incompatible file with version {0}", header[3]), ); } - if header[3] != LEPTON_HEADER_BASELINE_JPEG_TYPE[0] - && header[3] != LEPTON_HEADER_PROGRESSIVE_JPEG_TYPE[0] + if header[3] != LEPTON_HEADER_SEQUENTIAL_SINGLE_SCAN_JPEG_TYPE[0] + && header[3] != LEPTON_HEADER_MULTI_SCAN_JPEG_TYPE[0] { return err_exit_code( ExitCode::BadLeptonFile, @@ -365,10 +365,16 @@ impl LeptonHeader { writer.write_all(&LEPTON_FILE_HEADER)?; writer.write_u8(LEPTON_VERSION)?; - if self.jpeg_header.jpeg_type == JpegType::Progressive { - writer.write_all(&LEPTON_HEADER_PROGRESSIVE_JPEG_TYPE)?; + // The original Lepton codebase uses "baseline" to denote a sequential JPEG with a single, interleaved scan + // that contains all components. In the JPEG specification, "baseline" refers to a sequential (so + // non-progressive) JPEG with interleaved and/or non-interleaved scans. To avoid confusion, use "sequential + // single scan" and "multi scan" here, since a progressive JPEG may also only have a single scan. + if self.jpeg_header.jpeg_type == JpegType::Sequential + && self.jpeg_header.cs_cmpc == self.jpeg_header.cmpc + { + writer.write_all(&LEPTON_HEADER_SEQUENTIAL_SINGLE_SCAN_JPEG_TYPE)?; } else { - writer.write_all(&LEPTON_HEADER_BASELINE_JPEG_TYPE)?; + writer.write_all(&LEPTON_HEADER_MULTI_SCAN_JPEG_TYPE)?; } writer.write_u8(self.thread_handoff.len() as u8)?; diff --git a/tests/end_to_end.rs b/tests/end_to_end.rs index 373017d..ab5c763 100644 --- a/tests/end_to_end.rs +++ b/tests/end_to_end.rs @@ -5,6 +5,8 @@ *--------------------------------------------------------------------------------------------*/ use core::result::Result; +use rand::Rng; +use rand::SeedableRng; use std::fs::read_dir; use std::io::Cursor; use std::path::Path; @@ -78,6 +80,7 @@ fn verify_decode( "colorswap", "cathedral_db_non_int", "cathedral_db_non_int_rustold", + "dc_init_non_interleaved", "gray2sf", "grayscale", "hq", @@ -91,10 +94,12 @@ fn verify_decode( "iphonecrop2", "iphoneprogressive", "iphoneprogressive2", - "progressive_late_dht", // image has huffman tables that come very late which causes a verification failure - "out_of_order_dqt", // image with quanatization table dqt that comes after image definition SOF + "progressive_late_dht", + "out_of_order_dqt", "narrowrst", "nofsync", + "non-interleaved-multiple-restart", + "scan_order_reversed", "slrcity", "slrhills", "slrindoor", @@ -102,9 +107,9 @@ fn verify_decode( "trailingrst", "trailingrst2", "trunc", - "truncbad", // the lepton format is truncated and invalid - "eof_and_trailingrst", // the lepton format has a wrongly set unexpected eof and trailing rst - "eof_and_trailinghdrdata" // the lepton format has a wrongly set unexpected eof and trailing header data + "truncbad", + "eof_and_trailingrst", + "eof_and_trailinghdrdata" )] file: &str, ) { @@ -166,10 +171,13 @@ fn verify_encode( "androidprogressive_garbage", "androidtrail", "colorswap", + "cathedral_db_non_int", + "cathedral_db_non_int_rustold", + "dc_init_non_interleaved", "gray2sf", "grayscale", "hq", - //"half_scan", + "half_scan", "iphone", "iphonecity", "iphonecity_with_16KGarbage", @@ -178,10 +186,12 @@ fn verify_encode( "iphonecrop2", "iphoneprogressive", "iphoneprogressive2", - "progressive_late_dht", // image has huffman tables that come very late which caused a verification failure + "progressive_late_dht", "out_of_order_dqt", //"narrowrst", //"nofsync", + "non-interleaved-multiple-restart", + "scan_order_reversed", "slrcity", "slrhills", "slrindoor", @@ -216,9 +226,68 @@ fn verify_encode( assert_eq_array(&input, &output); } +fn scan_header_size(num_components: usize) -> usize { + 2 + 2 + 1 + num_components * 2 + 3 +} + +#[rstest] +fn verify_encode_truncated_scan( + #[values( // scan begin and end offsets found using jpegdump + ("android", 0x224f + scan_header_size(3), 0x1f996), + ("androidprogressive", 0x01824 + scan_header_size(3), 0x05428), + ("androidprogressive", 0x05460 + scan_header_size(1), 0x0a321), + ("androidprogressive", 0x0a355 + scan_header_size(1), 0x0b2f1), + ("androidprogressive", 0x0b31f + scan_header_size(1), 0x0ca1b), + ("androidprogressive", 0x0ca60 + scan_header_size(1), 0x0fa70), + ("androidprogressive", 0x0fa98 + scan_header_size(1), 0x165e5), + ("androidprogressive", 0x165eb + scan_header_size(3), 0x170a7), + ("androidprogressive", 0x170c9 + scan_header_size(1), 0x185c4), + ("androidprogressive", 0x185e9 + scan_header_size(1), 0x19f45), + ("androidprogressive", 0x19f73 + scan_header_size(1), 0x23980) + )] + test: (&str, usize, usize), +) { + let (filename, scan_begin, scan_end) = test; + let file = read_file(filename, ".jpg"); + + let mut rng = rand_chacha::ChaCha12Rng::from_seed([1; 32]); + + let scan_size = scan_end - scan_begin; + let mut offset: usize = 0; + loop { + // Include at least one new byte. Increase the range to test fewer samples. + offset += rng.gen_range(1..scan_size / 3); + if offset > scan_size { + break; + } + + let input = &file[0..scan_begin + offset]; + let mut lepton = Vec::new(); + let mut output = Vec::new(); + + encode_lepton( + &mut Cursor::new(&input), + &mut Cursor::new(&mut lepton), + &EnabledFeatures::compat_lepton_vector_write(), + &DEFAULT_THREAD_POOL, + ) + .unwrap(); + + decode_lepton( + &mut Cursor::new(lepton), + &mut output, + &EnabledFeatures::compat_lepton_vector_read(), + &DEFAULT_THREAD_POOL, + ) + .unwrap(); + + assert_eq_array(&input, &output); + } +} + /// these files are expected to fail encoding due to unsupported features or roundtrip errors #[rstest] -fn verify_fail_encode(#[values("half_scan", "narrowrst", "nofsync")] file: &str) { +fn verify_fail_encode(#[values("narrowrst", "nofsync")] file: &str) { let input = read_file(file, ".jpg"); let result = encode_lepton_verify(