Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for JPEG XL #13

Merged
merged 2 commits into from
Jun 27, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ imagesize = "0.8"
* GIF
* HEIC / HEIF
* JPEG
* JPEG XL
* PNG
* PSD / PSB
* TIFF
Expand Down
202 changes: 192 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub enum ImageType {
Gif,
Heif,
Jpeg,
Jxl,
Png,
Psd,
Tiff,
Expand Down Expand Up @@ -89,10 +90,13 @@ pub fn image_type(header: &[u8]) -> ImageResult<ImageType> {
} else if header.len() >= 8 &&
&header[4..8] == b"ftyp" {
return Ok(ImageType::Heif);
} else if header.len() >= 12 &&
} else if header.len() >= 12 &&
&header[0..4] == b"RIFF" &&
&header[8..12] == b"WEBP"{
return Ok(ImageType::Webp);
} else if (header.len() >= 2 && &header[0..2] == b"\xFF\x0A") ||
(header.len() >= 12 && &header[0..12] == b"\x00\x00\x00\x0CJXL \x0D\x0A\x87\x0A") {
return Ok(ImageType::Jxl);
} else {
return Err(ImageError::NotSupported);
}
Expand Down Expand Up @@ -131,7 +135,7 @@ pub fn image_type(header: &[u8]) -> ImageResult<ImageType> {
/// Err(why) => println!("Error getting size: {:?}", why)
/// }
/// ```
///
///
/// [`ImageError`]: enum.ImageError.html
pub fn size<P>(path: P) -> ImageResult<ImageSize> where P: AsRef<Path> {
let file = File::open(path)?;
Expand Down Expand Up @@ -168,7 +172,7 @@ pub fn size<P>(path: P) -> ImageResult<ImageSize> where P: AsRef<Path> {
///
/// assert_eq!(blob_size(&data).is_err(), true);
/// ```
///
///
/// [`ImageError`]: enum.ImageError.html
pub fn blob_size(data: &[u8]) -> ImageResult<ImageSize> {
let mut reader = Cursor::new(&data[..]);
Expand All @@ -190,6 +194,7 @@ fn dispatch_header<R: BufRead + Seek>(reader: &mut R, header: &[u8]) -> ImageRes
ImageType::Gif => gif_size(header),
ImageType::Heif => heif_size(reader),
ImageType::Jpeg => jpeg_size(reader),
ImageType::Jxl => jxl_size(reader),
ImageType::Png => png_size(reader),
ImageType::Psd => psd_size(reader),
ImageType::Tiff => tiff_size(reader),
Expand Down Expand Up @@ -246,7 +251,7 @@ fn heif_size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
read_u32(reader, &Endian::Big)?; // Discard junk value
let width = read_u32(reader, &Endian::Big)? as usize;
let height = read_u32(reader, &Endian::Big)? as usize;

// Assign new largest size by area
if width * height > max_width * max_height {
max_width = width;
Expand Down Expand Up @@ -277,9 +282,9 @@ fn heif_size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
std::mem::swap(&mut max_width, &mut max_height);
}

Ok(ImageSize {
Ok(ImageSize {
width: max_width,
height: max_height
height: max_height
})
}

Expand Down Expand Up @@ -328,7 +333,7 @@ fn jpeg_size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
}

let page = marker[1];

// Check for valid SOFn markers. C4, C8, and CC aren't dimension markers.
if (page >= 0xC0 && page <= 0xC3) || (page >= 0xC5 && page <= 0xC7) ||
(page >= 0xC9 && page <= 0xCB) || (page >= 0xCD && page <= 0xCF) {
Expand All @@ -345,8 +350,8 @@ fn jpeg_size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
if depth < 0 {
return Err(ImageError::CorruptedImage);
}
}
}

// Read the marker length and skip over it entirely
let page_size = read_u16(reader, &Endian::Big)? as i64;
reader.seek(SeekFrom::Current(page_size - 2))?;
Expand All @@ -358,6 +363,175 @@ fn jpeg_size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
})
}

fn jxl_size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
let mut file_header = [0; 16]; // The size is variable, but doesn't exceed 16 bytes
let mut header_size = 0;

reader.seek(SeekFrom::Start(0))?;
reader.read_exact(&mut file_header[..2])?;

if &file_header[..2] == b"\xFF\x0A" {
// Raw data: Read header directly
header_size = reader.read(&mut file_header[2..])? + 2;
} else {
// Container format: Read from a single jxlc box or multiple jxlp boxes
reader.seek(SeekFrom::Start(12))?;

loop {
let (box_type, box_size) = next_tag(reader)?;
let box_start = reader.stream_position()? - 8;

// If box_size is 1, the real size is stored in the first 8 bytes of content.
// If box_size is 0, the box ends at EOF.

let box_size = match box_size {
1 => {
let mut box_size = [0; 8];
reader.read_exact(&mut box_size)?;
u64::from_be_bytes(box_size)
},
_ => box_size as u64,
};

let box_end = box_start.checked_add(box_size).ok_or(ImageError::CorruptedImage)?;
let box_header_size = reader.stream_position()? - box_start;

if box_size < box_header_size && box_size != 0 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Invalid size for {} box: {}", box_type, box_size)).into());
}

// The jxlc box must contain the complete codestream

if box_type == "jxlc" {
let read_end = match box_size {
0 => 16,
_ => std::cmp::min(box_size - box_header_size, 16) as usize,
};

header_size = reader.read(&mut file_header[..read_end])?;
break;
}

// Or it could be stored as part of multiple jxlp boxes

if box_type == "jxlp" {
let mut jxlp_index = [0; 4];
reader.read_exact(&mut jxlp_index)?;

if box_size - box_header_size < 4 && box_size != 0 {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Invalid size for jxlp box: {}", box_size)).into());
}

let read_end = match box_size {
0 => 16 - header_size,
_ => std::cmp::min(box_size - box_header_size - 4, 16 - header_size as u64) as usize + header_size,
};

header_size += reader.read(&mut file_header[header_size..read_end])?;

// If jxlp_index has the high bit set to 1, this is the final jxlp box

if header_size == 16 || (jxlp_index[0] & 0x80) != 0 {
break;
}
}

if box_size == 0 {
break;
}

reader.seek(SeekFrom::Start(box_end))?;
}
}

if header_size < 2 {
return Err(ImageError::CorruptedImage);
}

if &file_header[0..2] != b"\xFF\x0A" {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid JXL signature").into());
}

// Parse the header data

let file_header = u128::from_le_bytes(file_header);
let header_size = 8*header_size;

let is_small = read_bits(file_header, 1, 16, header_size)? != 0;

// Extract image height:
// For small images, the height is stored in the next 5 bits
// For non-small images, the next two bits are used to determine the number of bits to read

let height_selector = read_bits(file_header, 2, 17, header_size)?;

let (height_bits, height_offset, height_shift) = match (is_small, height_selector) {
(true, _) => (5, 17, 3),
(false, 0) => (9, 19, 0),
(false, 1) => (13, 19, 0),
(false, 2) => (18, 19, 0),
(false, 3) => (30, 19, 0),
(false, _) => (0, 0, 0),
};

let height = (read_bits(file_header, height_bits, height_offset, header_size)? + 1) << height_shift;

// Extract image width:
// If ratio is 0, use the same logic as before
// Otherwise, the width is calculated using a predefined aspect ratio

let ratio = read_bits(file_header, 3, height_bits + height_offset, header_size)?;
let width_selector = read_bits(file_header, 2, height_bits + height_offset + 3, 128)?;

let (width_bits, width_offset, width_shift) = match (is_small, width_selector) {
(true, _) => (5, 25, 3),
(false, 0) => (9, height_bits + height_offset + 5, 0),
(false, 1) => (13, height_bits + height_offset + 5, 0),
(false, 2) => (18, height_bits + height_offset + 5, 0),
(false, 3) => (30, height_bits + height_offset + 5, 0),
(false, _) => (0, 0, 0),
};

let width = match ratio {
1 => height, // 1:1
2 => (height / 10) * 12, // 12:10
3 => (height / 3) * 4, // 4:3
4 => (height / 2) * 3, // 3:2
5 => (height / 9) * 16, // 16:9
6 => (height / 4) * 5, // 5:4
7 => height * 2, // 2:1
_ => (read_bits(file_header, width_bits, width_offset, header_size)? + 1) << width_shift,
};

// Extract orientation:
// This value overrides the orientation in EXIF metadata

let metadata_offset = match ratio {
0 => width_bits + width_offset,
_ => height_bits + height_offset + 3,
};

let all_default = read_bits(file_header, 1, metadata_offset, header_size)? != 0;

let orientation = match all_default {
true => 0,
false => {
let extra_fields = read_bits(file_header, 1, metadata_offset + 1, header_size)? != 0;

match extra_fields {
false => 0,
true => read_bits(file_header, 3, metadata_offset + 2, header_size)?,
}
},
};

if orientation < 4 {
Ok(ImageSize { width, height })
} else {
Ok(ImageSize { width: height, height: width })
}
}

fn png_size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
reader.seek(SeekFrom::Start(0x10))?;

Expand Down Expand Up @@ -531,4 +705,12 @@ fn read_u8<R: BufRead + Seek>(reader: &mut R) -> ImageResult<u8> {
let mut buf = [0; 1];
reader.read_exact(&mut buf)?;
Ok(buf[0])
}
}

fn read_bits(source: u128, num_bits: usize, offset: usize, size: usize) -> ImageResult<usize> {
if offset + num_bits < size {
Ok((source >> offset) as usize & ((1 << num_bits) - 1))
} else {
Err(ImageError::CorruptedImage)
}
}
Binary file added test/jxl/err_box.jxl
Binary file not shown.
Binary file added test/jxl/err_header.jxl
Binary file not shown.
Binary file added test/jxl/err_signature.jxl
Binary file not shown.
Binary file added test/jxl/valid_13w_9h.jxl
Binary file not shown.
Binary file added test/jxl/valid_18w_9h.jxl
Binary file not shown.
Binary file added test/jxl/valid_30w_9h.jxl
Binary file not shown.
Binary file added test/jxl/valid_9w_13h.jxl
Binary file not shown.
Binary file added test/jxl/valid_9w_18h.jxl
Binary file not shown.
Binary file added test/jxl/valid_9w_30h.jxl
Binary file not shown.
Binary file added test/jxl/valid_9w_9h.jxl
Binary file not shown.
Binary file added test/jxl/valid_box_jxlc.jxl
Binary file not shown.
Binary file added test/jxl/valid_box_jxlp.jxl
Binary file not shown.
Binary file added test/jxl/valid_orientation0.jxl
Binary file not shown.
Binary file added test/jxl/valid_orientation4.jxl
Binary file not shown.
Binary file added test/jxl/valid_ratio1.jxl
Binary file not shown.
Binary file added test/jxl/valid_ratio2.jxl
Binary file not shown.
Binary file added test/jxl/valid_ratio3.jxl
Binary file not shown.
Binary file added test/jxl/valid_ratio4.jxl
Binary file not shown.
Binary file added test/jxl/valid_ratio5.jxl
Binary file not shown.
Binary file added test/jxl/valid_ratio6.jxl
Binary file not shown.
Binary file added test/jxl/valid_ratio7.jxl
Binary file not shown.
Binary file added test/jxl/valid_small.jxl
Binary file not shown.
2 changes: 1 addition & 1 deletion tests/jpeg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ fn jpg_wrong_size() {
let dim = size("test/jpg/wrong_size.jpg").unwrap();
assert_eq!(dim.width, 1080);
assert_eq!(dim.height, 1080);
}
}
Loading