diff --git a/test-images/jpeg/mjpeg_huffman.jpg b/test-images/jpeg/mjpeg_huffman.jpg new file mode 100644 index 00000000..e339e5d7 Binary files /dev/null and b/test-images/jpeg/mjpeg_huffman.jpg differ diff --git a/zune-jpeg/Cargo.toml b/zune-jpeg/Cargo.toml index 65399a38..f10f9b2a 100644 --- a/zune-jpeg/Cargo.toml +++ b/zune-jpeg/Cargo.toml @@ -24,3 +24,6 @@ default = ["x86", "neon", "std"] log = "0.4.11" # logging facilities zune-core = { path = "../zune-core", version = "0.2" } + +[dev-dependencies] +zune-ppm = { path = "../zune-ppm" } diff --git a/zune-jpeg/src/decoder.rs b/zune-jpeg/src/decoder.rs index bc34fb3e..37bd4db2 100644 --- a/zune-jpeg/src/decoder.rs +++ b/zune-jpeg/src/decoder.rs @@ -13,9 +13,10 @@ use alloc::string::ToString; use alloc::vec::Vec; use alloc::{format, vec}; +use zune_core::bit_depth::BitDepth; use zune_core::bytestream::{ZByteReader, ZReaderTrait}; use zune_core::colorspace::ColorSpace; -use zune_core::options::DecoderOptions; +use zune_core::options::{DecoderOptions, EncoderOptions}; use crate::color_convert::choose_ycbcr_to_rgb_convert_func; use crate::components::{Components, SampleRatios}; @@ -146,7 +147,8 @@ pub struct JpegDecoder // exif data, lifted from app2 pub(crate) exif_data: Option>, - pub(crate) icc_data: Vec + pub(crate) icc_data: Vec, + pub(crate) is_mjpeg: bool } impl JpegDecoder @@ -189,7 +191,8 @@ where headers_decoded: false, seen_sof: false, exif_data: None, - icc_data: vec![] + icc_data: vec![], + is_mjpeg: false } } /// Decode a buffer already in memory @@ -514,7 +517,7 @@ where //APP(0) segment Marker::APP(0) => { - let length = self.stream.get_u16_be_err()?; + let mut length = self.stream.get_u16_be_err()?; if length < 2 { @@ -523,6 +526,16 @@ where ))); } // skip for now + if length > 5 && self.stream.has(5) + { + let mut buffer = [0u8; 5]; + self.stream.read_exact(&mut buffer).unwrap(); + if &buffer == b"AVI1\0" + { + self.is_mjpeg = true; + } + length -= 5; + } self.stream.skip((length - 2) as usize); //parse_app(buf, m, &mut self.info)?; diff --git a/zune-jpeg/src/huffman.rs b/zune-jpeg/src/huffman.rs index 15e0c94e..fea5fe7a 100644 --- a/zune-jpeg/src/huffman.rs +++ b/zune-jpeg/src/huffman.rs @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2023. + * + * This software is free software; + * + * You can redistribute it or modify it under terms of the MIT, Apache License or Zlib license + */ + //! This file contains a single struct `HuffmanTable` that //! stores Huffman tables needed during `BitStream` decoding. #![allow(clippy::similar_names, clippy::module_name_repetitions)] @@ -61,6 +69,17 @@ impl HuffmanTable Ok(p) } + /// Create a new huffman tables with values that aren't fixed + /// used by fill_mjpeg_tables + pub fn new_unfilled( + codes: &[u8; 17], values: &[u8], is_dc: bool, is_progressive: bool + ) -> Result + { + let mut buf = [0; 256]; + buf[..values.len()].copy_from_slice(values); + HuffmanTable::new(codes, buf, is_dc, is_progressive) + } + /// Compute derived values for a Huffman table /// /// This routine performs some validation checks on the table diff --git a/zune-jpeg/src/mcu.rs b/zune-jpeg/src/mcu.rs index 36b7f801..08008b26 100644 --- a/zune-jpeg/src/mcu.rs +++ b/zune-jpeg/src/mcu.rs @@ -112,6 +112,19 @@ impl JpegDecoder mcu_width = ((self.info.width + 7) / 8) as usize; mcu_height = ((self.info.height + 7) / 8) as usize; } + if self.is_interleaved + && self.input_colorspace.num_components() > 1 + && self.options.jpeg_get_out_colorspace().num_components() == 1 + { + // For a specific set of images, e.g interleaved, + // when converting from YcbCr to grayscale, we need to + // take into account mcu height since the MCU decoding needs to take + // it into account for padding purposes and the post processor + // parses two rows per mcu width. + // + //TODO: Check if this test works over time + mcu_height /= self.h_max; + } if self.input_colorspace.num_components() > self.components.len() { diff --git a/zune-jpeg/src/misc.rs b/zune-jpeg/src/misc.rs index 1897046e..9633cc2d 100644 --- a/zune-jpeg/src/misc.rs +++ b/zune-jpeg/src/misc.rs @@ -18,6 +18,7 @@ use zune_core::colorspace::ColorSpace; use crate::components::SampleRatios; use crate::errors::DecodeErrors; +use crate::huffman::HuffmanTable; use crate::JpegDecoder; /// Start of baseline DCT Huffman coding @@ -292,9 +293,14 @@ pub(crate) fn setup_component_params( )); } - // delete quantization tables, we'll extract them from the components when - // needed - img.qt_tables = [None, None, None, None]; + if img.is_mjpeg + { + fill_default_mjpeg_tables( + img.is_progressive, + &mut img.dc_huffman_tables, + &mut img.ac_huffman_tables + ); + } Ok(()) } @@ -328,3 +334,117 @@ pub fn calculate_padded_width(actual_width: usize, sub_sample: SampleRatios) -> } } } + +// https://www.loc.gov/preservation/digital/formats/fdd/fdd000063.shtml +// "Avery Lee, writing in the rec.video.desktop newsgroup in 2001, commented that "MJPEG, or at +// least the MJPEG in AVIs having the MJPG fourcc, is restricted JPEG with a fixed -- and +// *omitted* -- Huffman table. The JPEG must be YCbCr colorspace, it must be 4:2:2, and it must +// use basic Huffman encoding, not arithmetic or progressive.... You can indeed extract the +// MJPEG frames and decode them with a regular JPEG decoder, but you have to prepend the DHT +// segment to them, or else the decoder won't have any idea how to decompress the data. +// The exact table necessary is given in the OpenDML spec."" +pub fn fill_default_mjpeg_tables( + is_progressive: bool, dc_huffman_tables: &mut [Option], + ac_huffman_tables: &mut [Option] +) +{ + // Section K.3.3 + trace!("Filling with default mjpeg tables"); + + if dc_huffman_tables[0].is_none() + { + // Table K.3 + dc_huffman_tables[0] = Some( + HuffmanTable::new_unfilled( + &[ + 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ], + &[ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B + ], + true, + is_progressive + ) + .unwrap() + ); + } + if dc_huffman_tables[1].is_none() + { + // Table K.4 + dc_huffman_tables[1] = Some( + HuffmanTable::new_unfilled( + &[ + 0x00, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00 + ], + &[ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B + ], + true, + is_progressive + ) + .unwrap() + ); + } + if ac_huffman_tables[0].is_none() + { + // Table K.5 + ac_huffman_tables[0] = Some( + HuffmanTable::new_unfilled( + &[ + 0x00, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, + 0x00, 0x00, 0x01, 0x7D + ], + &[ + 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, + 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, + 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0A, + 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, + 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, + 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, + 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, + 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, + 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, + 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, + 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, + 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA + ], + false, + is_progressive + ) + .unwrap() + ); + } + if ac_huffman_tables[1].is_none() + { + // Table K.6 + ac_huffman_tables[1] = Some( + HuffmanTable::new_unfilled( + &[ + 0x00, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, + 0x00, 0x01, 0x02, 0x77 + ], + &[ + 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, + 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, + 0xC1, 0x09, 0x23, 0x33, 0x52, 0xF0, 0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, + 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A, 0x26, 0x27, 0x28, 0x29, 0x2A, + 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, + 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, + 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, + 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, + 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, + 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, + 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, + 0xDA, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, + 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA + ], + false, + is_progressive + ) + .unwrap() + ); + } +} diff --git a/zune-tests/tests/jpeg.json b/zune-tests/tests/jpeg.json index 43240377..6e347a64 100644 --- a/zune-tests/tests/jpeg.json +++ b/zune-tests/tests/jpeg.json @@ -100,5 +100,10 @@ "hash": 145881769686590241353562665580548083291, "comment": "testing-bgr support", "colorspace": "bgr" + }, + { + "name": "mjpeg_huffman.jpg", + "hash": 309069272274389382301715239622093501038, + "comment": "Image needs to be filled with default huffman tables" } ] \ No newline at end of file