Skip to content

Commit

Permalink
Add support for JPEG XL
Browse files Browse the repository at this point in the history
  • Loading branch information
oscar-rc1 committed Jun 24, 2021
1 parent 5d518e7 commit 9ed4f5f
Show file tree
Hide file tree
Showing 26 changed files with 358 additions and 11 deletions.
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

0 comments on commit 9ed4f5f

Please sign in to comment.