imagesize-0.12.0/.cargo_vcs_info.json0000644000000001360000000000100130710ustar { "git": { "sha1": "c33cdb8e8293becaf1bb12f5280f24b1e81732dd" }, "path_in_vcs": "" }imagesize-0.12.0/.github/workflows/ci.yml000064400000000000000000000013210072674642500164210ustar 00000000000000name: CI on: pull_request: branches: - master push: branches: - master env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 RUSTFLAGS: -Dwarnings jobs: check-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: "Install minimal stable with clippy" uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable components: clippy - name: "Clippy" uses: actions-rs/cargo@v1 with: command: clippy args: --all-targets - name: "Test" uses: actions-rs/cargo@v1 with: command: test args: --tests --benches --examples imagesize-0.12.0/.gitignore000064400000000000000000000004470072674642500137060ustar 00000000000000# Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock Cargo.lock /fuzz/ afl_fuzz_target/ crashpad/ .DS_Store imagesize-0.12.0/.travis.yml000064400000000000000000000004730072674642500140260ustar 00000000000000language: rust rust: - stable - beta - nightly os: - linux - osx cache: cargo: true timeout: 900 before_cache: # Travis can't cache files that are not readable by "others" - chmod -R a+r $HOME/.cargo matrix: allow_failures: - nightly script: - cargo build - cargo testimagesize-0.12.0/Cargo.toml0000644000000021250000000000100110670ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "imagesize" version = "0.12.0" authors = ["Maid Dog "] exclude = ["tests/*"] description = "Quick probing of image dimensions without loading the entire file." documentation = "https://docs.rs/imagesize" readme = "README.md" keywords = [ "image", "size", ] categories = [ "multimedia", "multimedia::images", ] license = "MIT" repository = "https://github.com/Roughsketch/imagesize" [[bench]] name = "parse_benchmark" harness = false [dependencies] [dev-dependencies.criterion] version = "0.4" features = ["html_reports"] [dev-dependencies.walkdir] version = "2.3.3" imagesize-0.12.0/Cargo.toml.orig000064400000000000000000000011260072674642500146000ustar 00000000000000[package] name = "imagesize" version = "0.12.0" authors = ["Maid Dog "] edition = "2018" description = "Quick probing of image dimensions without loading the entire file." documentation = "https://docs.rs/imagesize" repository = "https://github.com/Roughsketch/imagesize" readme = "README.md" keywords = ["image", "size"] categories = ["multimedia", "multimedia::images"] license = "MIT" exclude = ["tests/*"] [dependencies] [dev-dependencies] criterion = { version = "0.4", features = ["html_reports"] } walkdir = "2.3.3" [[bench]] name = "parse_benchmark" harness = falseimagesize-0.12.0/LICENSE000064400000000000000000000020750072674642500127220ustar 00000000000000MIT License Copyright (c) 2017 Maiddog 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. imagesize-0.12.0/README.md000064400000000000000000000031740072674642500131750ustar 00000000000000[![crates.io version]][crates.io link] [![docs-badge][]][docs] # imagesize Quickly probe the size of various image formats without reading the entire file. The goal of this crate is to be able to read the dimensions of a supported image without loading unnecessary data, and without pulling in more dependencies. Most reads only require 16 bytes or less, and more complex formats take advantage of skipping junk data. ## Usage Add the following to your Cargo.toml: ```toml [dependencies] imagesize = "0.12" ``` ## Supported Image Formats * Aseprite * BMP * DDS * EXR * Farbfeld * GIF * HDR * HEIC / HEIF * ICO* * JPEG * JPEG XL * KTX2 * PNG * PNM (PBM, PGM, PPM) * PSD / PSB * QOI * TGA * TIFF * VTF * WEBP If you have a format you think should be added, feel free to create an issue. *ICO files can contain multiple images, `imagesize` will give the dimensions of the largest one. ## Examples ### From a file ```rust match imagesize::size("example.webp") { Ok(size) => println!("Image dimensions: {}x{}", size.width, size.height), Err(why) => println!("Error getting dimensions: {:?}", why) } ``` ### From a vector ```rust let data = vec![0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x64, 0x00, 0x64, 0x00]; match imagesize::blob_size(&data) { Ok(size) => println!("Image dimensions: {}x{}", size.width, size.height), Err(why) => println!("Error getting dimensions: {:?}", why), } ``` [crates.io link]: https://crates.io/crates/imagesize [crates.io version]: https://img.shields.io/crates/v/imagesize.svg?style=flat-square [docs]: https://docs.rs/imagesize [docs-badge]: https://img.shields.io/badge/docs-online-5023dd.svg?style=flat-square imagesize-0.12.0/benches/parse_benchmark.rs000064400000000000000000000014440072674642500170150ustar 00000000000000use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; fn size_benchmarks(c: &mut Criterion) { let mut group = c.benchmark_group("imagesize"); let mut paths = Vec::new(); for file in walkdir::WalkDir::new("tests/images") .into_iter() .filter_map(|file| file.ok()) { if file.metadata().unwrap().is_file() { paths.push(std::fs::canonicalize(file.path()).unwrap()); } } group.bench_with_input( BenchmarkId::from_parameter(paths.len()), &paths, |b, paths| b.iter(|| for path in paths { let _ = imagesize::size(black_box(path)); } ), ); group.finish(); } criterion_group!(benches, size_benchmarks); criterion_main!(benches); imagesize-0.12.0/src/formats/aesprite.rs000064400000000000000000000010640072674642500163360ustar 00000000000000use crate::util::*; use crate::{ImageResult, ImageSize}; use std::io::{BufRead, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { // aseprite header: https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md#header reader.seek(SeekFrom::Start(0x8))?; Ok(ImageSize { width: read_u16(reader, &Endian::Little)? as usize, height: read_u16(reader, &Endian::Little)? as usize, }) } pub fn matches(header: &[u8]) -> bool { header.len() >= 12 && &header[4..6] == b"\xE0\xA5" } imagesize-0.12.0/src/formats/avif.rs000064400000000000000000000014470072674642500154540ustar 00000000000000// AVIF is a special case of HEIF. Image size methods are defined in there and should work for both. // Only difference is that we want to specify the image type as AVIF instead of wrapping it into HEIF. pub fn matches(header: &[u8]) -> bool { if header.len() < 12 || &header[4..8] != b"ftyp" { return false; } let header_brand = &header[8..12]; // Since other non-AVIF files may contain ftype in the header // we try to use brands to distinguish image files specifically. // List of brands from here: https://mp4ra.org/#/brands let valid_brands = [ b"avif", b"avio", b"avis", b"MA1A", b"MA1B", ]; for brand in valid_brands { if brand == header_brand { return true; } } false } imagesize-0.12.0/src/formats/bmp.rs000064400000000000000000000006740072674642500153060ustar 00000000000000use crate::util::*; use crate::{ImageResult, ImageSize}; use std::io::{BufRead, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(0x12))?; Ok(ImageSize { width: read_u32(reader, &Endian::Little)? as usize, height: read_u32(reader, &Endian::Little)? as usize, }) } pub fn matches(header: &[u8]) -> bool { header.starts_with(b"\x42\x4D") } imagesize-0.12.0/src/formats/dds.rs000064400000000000000000000007220072674642500152740ustar 00000000000000use std::io::{BufRead, Seek, SeekFrom}; use crate::{ util::{read_u32, Endian}, ImageResult, ImageSize, }; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(12))?; let height = read_u32(reader, &Endian::Little)? as usize; let width = read_u32(reader, &Endian::Little)? as usize; Ok(ImageSize { width, height }) } pub fn matches(header: &[u8]) -> bool { header.starts_with(b"DDS ") } imagesize-0.12.0/src/formats/exr.rs000064400000000000000000000036040072674642500153220ustar 00000000000000use std::io::{self, BufRead, Seek, SeekFrom}; use crate::{ util::{read_i32, read_null_terminated_string, read_u32, Endian}, ImageResult, ImageSize, }; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(4))?; // If long names flag is set then max attribute name and type name is 255, otherwise it's only 31 let flags = read_u32(reader, &Endian::Little)?; let long_names = flags & 0x400 != 0; let max_name_size = if long_names { 255 } else { 31 }; // Read header attributes until we find the dataWindow attribute loop { let attr_name = read_null_terminated_string(reader, max_name_size)?; if attr_name.is_empty() { break; // End of the header } let attr_type = read_null_terminated_string(reader, max_name_size)?; // Skip attr_size let attr_size = read_u32(reader, &Endian::Little)?; if attr_name == "dataWindow" && attr_type == "box2i" { // Read the data window values let x_min = read_i32(reader, &Endian::Little)? as i64; let y_min = read_i32(reader, &Endian::Little)? as i64; let x_max = read_i32(reader, &Endian::Little)? as i64; let y_max = read_i32(reader, &Endian::Little)? as i64; if x_min > x_max || y_min > y_max { continue; } let width = (x_max - x_min + 1) as usize; let height = (y_max - y_min + 1) as usize; return Ok(ImageSize { width, height }); } else { // Skip the attribute value reader.seek(SeekFrom::Current(attr_size as i64))?; } } Err(io::Error::new(io::ErrorKind::InvalidData, "Data window not found").into()) } pub fn matches(header: &[u8]) -> bool { let exr_magic_number = [0x76, 0x2f, 0x31, 0x01]; header.starts_with(&exr_magic_number) } imagesize-0.12.0/src/formats/farbfeld.rs000064400000000000000000000007040072674642500162670ustar 00000000000000use std::io::{BufRead, Seek, SeekFrom}; use crate::{ util::{read_u32, Endian}, ImageResult, ImageSize, }; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(8))?; Ok(ImageSize { width: read_u32(reader, &Endian::Big)? as usize, height: read_u32(reader, &Endian::Big)? as usize, }) } pub fn matches(header: &[u8]) -> bool { header.starts_with(b"farbfeld") } imagesize-0.12.0/src/formats/gif.rs000064400000000000000000000006720072674642500152730ustar 00000000000000use crate::{util::{read_u16, Endian}, ImageResult, ImageSize}; use std::io::{BufRead, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(6))?; Ok(ImageSize { width: read_u16(reader, &Endian::Little)? as usize, height: read_u16(reader, &Endian::Little)? as usize, }) } pub fn matches(header: &[u8]) -> bool { header.starts_with(b"GIF8") } imagesize-0.12.0/src/formats/hdr.rs000064400000000000000000000067360072674642500153120ustar 00000000000000use std::io::{self, BufRead, Seek, SeekFrom}; use crate::{util::read_line_capped, ImageResult, ImageSize}; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(0))?; // Read the first line and check if it's a valid HDR format identifier // Only read max of 11 characters which is max for longest valid header let format_identifier = read_line_capped(reader, 11)?; if !format_identifier.starts_with("#?RADIANCE") && !format_identifier.starts_with("#?RGBE") { return Err( io::Error::new(io::ErrorKind::InvalidData, "Invalid HDR format identifier").into(), ); } loop { // Assuming no line will ever go above 256. Just a random guess at the moment. // If a line goes over the capped length we will return InvalidData which I think // is better than potentially reading a malicious file and exploding memory usage. let line = read_line_capped(reader, 256)?; if line.trim().is_empty() { continue; } // HDR image dimensions can be stored in 8 different ways based on orientation // Using EXIF orientation as a reference: // https://web.archive.org/web/20220924095433/https://sirv.sirv.com/website/exif-orientation-values.jpg // // -Y N +X M => Standard orientation (EXIF 1) // -Y N -X M => Flipped horizontally (EXIF 2) // +Y N -X M => Flipped vertically and horizontally (EXIF 3) // +Y N +X M => Flipped vertically (EXIF 4) // +X M -Y N => Rotate 90 CCW and flip vertically (EXIF 5) // -X M -Y N => Rotate 90 CCW (EXIF 6) // -X M +Y N => Rotate 90 CW and flip vertically (EXIF 7) // +X M +Y N => Rotate 90 CW (EXIF 8) // // For EXIF 1-4 we can treat the dimensions the same. Flipping horizontally/vertically does not change them. // For EXIF 5-8 we need to swap width and height because the image was rotated 90/270 degrees. // // Because of the ordering and rotations I believe that means that lines that start with Y will always // be read as `height` then `width` and ones that start with X will be read as `width` then `height, // but since any line that starts with X is rotated 90 degrees they will be flipped. Essentially this // means that no matter whether the line starts with X or Y, it will be read as height then width. // Extract width and height information if line.starts_with("-Y") || line.starts_with("+Y") || line.starts_with("-X") || line.starts_with("+X") { let dimensions: Vec<&str> = line.split_whitespace().collect(); if dimensions.len() != 4 { return Err(io::Error::new( io::ErrorKind::InvalidData, "Invalid HDR dimensions line", ) .into()); } let height_parsed = dimensions[1].parse::().ok(); let width_parsed = dimensions[3].parse::().ok(); if let (Some(width), Some(height)) = (width_parsed, height_parsed) { return Ok(ImageSize { width, height }); } break; } } Err(io::Error::new(io::ErrorKind::InvalidData, "HDR dimensions not found").into()) } pub fn matches(header: &[u8]) -> bool { let radiance_header = b"#?RADIANCE\n"; let rgbe_header = b"#?RGBE\n"; header.starts_with(radiance_header) || header.starts_with(rgbe_header) } imagesize-0.12.0/src/formats/heif.rs000064400000000000000000000100330072674642500154310ustar 00000000000000use crate::util::*; use crate::{ImageError, ImageResult, ImageSize}; use std::io::{BufRead, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(0))?; // Read the ftyp header size let ftyp_size = read_u32(reader, &Endian::Big)?; // Jump to the first actual box offset reader.seek(SeekFrom::Start(ftyp_size.into()))?; // Skip to meta tag which contains all the metadata skip_to_tag(reader, b"meta")?; read_u32(reader, &Endian::Big)?; // Meta has a junk value after it skip_to_tag(reader, b"iprp")?; // Find iprp tag let mut ipco_size = skip_to_tag(reader, b"ipco")? as usize; // Find ipco tag // Keep track of the max size of ipco tag let mut max_width = 0usize; let mut max_height = 0usize; let mut found_ispe = false; let mut rotation = 0u8; while let Ok((tag, size)) = read_tag(reader) { // Size of tag length + tag cannot be under 8 (4 bytes each) if size < 8 { return Err(ImageError::CorruptedImage); } // ispe tag has a junk value followed by width and height as u32 if tag == "ispe" { found_ispe = true; 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; max_height = height; } } else if tag == "irot" { // irot is 9 bytes total: size, tag, 1 byte for rotation (0-3) rotation = read_u8(reader)?; } else if size >= ipco_size { // If we've gone past the ipco boundary, then break break; } else { // If we're still inside ipco, consume all bytes for // the current tag, minus the bytes already read in `read_tag` ipco_size -= size; reader.seek(SeekFrom::Current(size as i64 - 8))?; } } // If no ispe found, then we have no actual dimension data to use if !found_ispe { return Err( std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Not enough data").into(), ); } // Rotation can only be 0-3. 1 and 3 are 90 and 270 degrees respectively (anti-clockwise) // If we have 90 or 270 rotation, flip width and height if rotation == 1 || rotation == 3 { std::mem::swap(&mut max_width, &mut max_height); } Ok(ImageSize { width: max_width, height: max_height, }) } pub fn matches(header: &[u8]) -> bool { if header.len() < 12 || &header[4..8] != b"ftyp" { return false; } let header_brand = &header[8..12]; // Since other non-heif files may contain ftype in the header // we try to use brands to distinguish image files specifically. // List of brands from here: https://mp4ra.org/#/brands let valid_brands = [ // HEIF specific b"avci", b"avcs", b"heic", b"heim", b"heis", b"heix", b"hevc", b"hevm", b"hevs", b"hevx", b"jpeg", b"jpgs", b"mif1", b"msf1", b"mif2", b"pred", // AVIF specific b"avif", b"avio", b"avis", b"MA1A", b"MA1B", ]; for brand in valid_brands { if brand == header_brand { return true; } } false } fn skip_to_tag(reader: &mut R, tag: &[u8]) -> ImageResult { let mut tag_buf = [0; 4]; loop { let size = read_u32(reader, &Endian::Big)?; reader.read_exact(&mut tag_buf)?; if tag_buf == tag { return Ok(size); } if size >= 8 { reader.seek(SeekFrom::Current(size as i64 - 8))?; } else { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, format!("Invalid heif box size: {}", size), ) .into()); } } } imagesize-0.12.0/src/formats/ico.rs000064400000000000000000000023700072674642500152750ustar 00000000000000use crate::util::*; use crate::{ImageError, ImageResult, ImageSize}; use std::io::{BufRead, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(4))?; let img_count = read_u16(reader, &Endian::Little)?; let mut sizes = Vec::with_capacity(img_count as usize); for _ in 0..img_count { if let Ok(size) = ico_image_size(reader) { sizes.push(size) } else { // if we don't have all the bytes of the headers, just // return the largest one found so far break; } // each ICONDIRENTRY (image header) is 16 bytes, skip the last 14 reader.seek(SeekFrom::Current(14))?; } sizes.into_iter().max().ok_or(ImageError::CorruptedImage) } pub fn matches(header: &[u8]) -> bool { header.starts_with(&[0, 0, 1, 0]) } /// Reads two bytes to determine an individual image's size within an ICO fn ico_image_size(reader: &mut R) -> ImageResult { // ICO dimensions are 1-256 pixels, with a byte value of 0 representing 256 Ok(ImageSize { width: read_u8(reader)?.wrapping_sub(1) as usize + 1, height: read_u8(reader)?.wrapping_sub(1) as usize + 1, }) } imagesize-0.12.0/src/formats/jpeg.rs000064400000000000000000000033260072674642500154520ustar 00000000000000use crate::util::*; use crate::{ImageError, ImageResult, ImageSize}; use std::io::{BufRead, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { let mut marker = [0; 2]; let mut depth = 0i32; // Go to the first tag after FF D8 reader.seek(SeekFrom::Start(2))?; loop { // Read current marker (FF XX) reader.read_exact(&mut marker)?; if marker[0] != 0xFF { // Did not read a marker. Assume image is corrupt. return Err(ImageError::CorruptedImage); } let page = marker[1]; // Check for valid SOFn markers. C4, C8, and CC aren't dimension markers. if (0xC0..=0xC3).contains(&page) || (0xC5..=0xC7).contains(&page) || (0xC9..=0xCB).contains(&page) || (0xCD..=0xCF).contains(&page) { // Only get outside image size if depth == 0 { // Correct marker, go forward 3 bytes so we're at height offset reader.seek(SeekFrom::Current(3))?; break; } } else if page == 0xD8 { depth += 1; } else if page == 0xD9 { depth -= 1; 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))?; } Ok(ImageSize { height: read_u16(reader, &Endian::Big)? as usize, width: read_u16(reader, &Endian::Big)? as usize, }) } pub fn matches(header: &[u8]) -> bool { header.starts_with(b"\xFF\xD8\xFF") } imagesize-0.12.0/src/formats/jxl.rs000064400000000000000000000137350072674642500153270ustar 00000000000000use crate::util::*; use crate::{ImageError, ImageResult, ImageSize}; use std::io::{BufRead, Read, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { 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) = read_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 != 0 && box_size < box_header_size { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, format!("Invalid size for {} box: {}", box_type, box_size), ) .into()); } let mut box_reader = match box_size { 0 => reader.take(file_header.len() as u64), _ => reader.take(box_size - box_header_size), }; // The jxlc box must contain the complete codestream if box_type == "jxlc" { header_size = box_reader.read(&mut file_header)?; break; } // Or it could be stored as part of multiple jxlp boxes if box_type == "jxlp" { let mut jxlp_index = [0; 4]; box_reader.read_exact(&mut jxlp_index)?; header_size += box_reader.read(&mut file_header[header_size..])?; // If jxlp_index has the high bit set to 1, this is the final jxlp box if header_size == file_header.len() || (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, }) } } pub fn matches(header: &[u8]) -> bool { header.starts_with(b"\xFF\x0A") || header.starts_with(b"\x00\x00\x00\x0CJXL \x0D\x0A\x87\x0A") } imagesize-0.12.0/src/formats/ktx2.rs000064400000000000000000000011200072674642500154030ustar 00000000000000use std::io::{BufRead, Seek, SeekFrom}; use crate::{ util::{read_u32, Endian}, ImageResult, ImageSize, }; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(16))?; let width = read_u32(reader, &Endian::Little)? as usize; let height = read_u32(reader, &Endian::Little)? as usize; Ok(ImageSize { width, height }) } pub fn matches(header: &[u8]) -> bool { let ktx2_identifier = [ 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A, ]; header.starts_with(&ktx2_identifier) } imagesize-0.12.0/src/formats/mod.rs000064400000000000000000000050510072674642500153010ustar 00000000000000pub mod aesprite; pub mod avif; pub mod bmp; pub mod dds; pub mod exr; pub mod farbfeld; pub mod gif; pub mod hdr; pub mod heif; pub mod ico; pub mod jpeg; pub mod jxl; pub mod ktx2; pub mod png; pub mod pnm; pub mod psd; pub mod qoi; pub mod tga; pub mod tiff; pub mod vtf; pub mod webp; use crate::{ImageError, ImageResult, ImageType}; use std::io::{BufRead, Seek}; pub fn image_type(reader: &mut R) -> ImageResult { let mut header = [0; 12]; reader.read_exact(&mut header)?; // Currently there are no formats where 1 byte is enough to determine format if header.len() < 2 { return Err( std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Not enough data").into(), ); } // This is vaguely organized in what I assume are the most commonly used formats. // I don't know how much this matters for actual execution time. if jpeg::matches(&header) { return Ok(ImageType::Jpeg); } if png::matches(&header) { return Ok(ImageType::Png); } if gif::matches(&header) { return Ok(ImageType::Gif); } if tiff::matches(&header) { return Ok(ImageType::Tiff); } if webp::matches(&header) { return Ok(ImageType::Webp); } if heif::matches(&header) { return Ok(ImageType::Heif); } if avif::matches(&header) { return Ok(ImageType::Avif); } if jxl::matches(&header) { return Ok(ImageType::Jxl); } if bmp::matches(&header) { return Ok(ImageType::Bmp); } if psd::matches(&header) { return Ok(ImageType::Psd); } if ico::matches(&header) { return Ok(ImageType::Ico); } if aesprite::matches(&header) { return Ok(ImageType::Aseprite); } if exr::matches(&header) { return Ok(ImageType::Exr); } if hdr::matches(&header) { return Ok(ImageType::Hdr); } if dds::matches(&header) { return Ok(ImageType::Dds); } if ktx2::matches(&header) { return Ok(ImageType::Ktx2); } if qoi::matches(&header) { return Ok(ImageType::Qoi); } if farbfeld::matches(&header) { return Ok(ImageType::Farbfeld); } if pnm::matches(&header) { return Ok(ImageType::Pnm); } if vtf::matches(&header) { return Ok(ImageType::Vtf); } // Keep TGA last because it has the highest probability of false positives if tga::matches(&header, reader) { return Ok(ImageType::Tga); } Err(ImageError::NotSupported) } imagesize-0.12.0/src/formats/png.rs000064400000000000000000000006650072674642500153140ustar 00000000000000use crate::util::*; use crate::{ImageResult, ImageSize}; use std::io::{BufRead, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(0x10))?; Ok(ImageSize { width: read_u32(reader, &Endian::Big)? as usize, height: read_u32(reader, &Endian::Big)? as usize, }) } pub fn matches(header: &[u8]) -> bool { header.starts_with(b"\x89PNG") } imagesize-0.12.0/src/formats/pnm.rs000064400000000000000000000040340072674642500153140ustar 00000000000000use crate::util::*; use crate::{ImageResult, ImageSize}; use std::io::{self, BufRead, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(2))?; // We try to loop until we find a line that does not start with a comment // or is empty. After that, we should expect width and height back to back // separated by an arbitrary amount of whitespace. loop { // Lines can be arbitrarily long, but 1k is a good enough cap I think. // Anything higher and I blame whoever made the file. let line = read_until_whitespace(reader, 1024)?; let trimmed_line = line.trim(); // If it's a comment, skip until newline if trimmed_line.starts_with('#') { read_until_capped(reader, b'\n', 1024)?; continue } // If it's just empty skip if trimmed_line.is_empty() { continue; } // The first thing we read that isn't empty or a comment should be the width let raw_width = line; // Read in the next non-whitespace section as the height let line = read_until_whitespace(reader, 1024)?; let raw_height = line.trim(); // Try to parse the width and height let width_parsed = raw_width.parse::().ok(); let height_parsed = raw_height.parse::().ok(); // If successful return it if let (Some(width), Some(height)) = (width_parsed, height_parsed) { return Ok(ImageSize { width, height }); } // If no successful then assume that it cannot be read // If this happens we need to gather test files for those cases break; } Err(io::Error::new(io::ErrorKind::InvalidData, "PNM dimensions not found").into()) } pub fn matches(header: &[u8]) -> bool { if header[0] != b'P' { return false; } // We only support P1 to P6. Currently ignoring P7, PF, PFM if header[1] < b'1' && header[1] > b'6' { return false; } true } imagesize-0.12.0/src/formats/psd.rs000064400000000000000000000006620072674642500153130ustar 00000000000000use crate::util::*; use crate::{ImageResult, ImageSize}; use std::io::{BufRead, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(0x0E))?; Ok(ImageSize { height: read_u32(reader, &Endian::Big)? as usize, width: read_u32(reader, &Endian::Big)? as usize, }) } pub fn matches(header: &[u8]) -> bool { header.starts_with(b"8BPS") } imagesize-0.12.0/src/formats/qoi.rs000064400000000000000000000007000072674642500153060ustar 00000000000000use std::io::{BufRead, Seek, SeekFrom}; use crate::{ util::{read_u32, Endian}, ImageResult, ImageSize, }; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(4))?; Ok(ImageSize { width: read_u32(reader, &Endian::Big)? as usize, height: read_u32(reader, &Endian::Big)? as usize, }) } pub fn matches(header: &[u8]) -> bool { header.starts_with(b"qoif") } imagesize-0.12.0/src/formats/tga.rs000064400000000000000000000101350072674642500152740ustar 00000000000000use std::io::{BufRead, Seek, SeekFrom}; use crate::{util::*, ImageResult, ImageSize}; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(12))?; let width = read_u16(reader, &Endian::Little)? as usize; let height = read_u16(reader, &Endian::Little)? as usize; Ok(ImageSize { width, height }) } pub fn matches(header: &[u8], reader: &mut R) -> bool { // Do a naive check first to filter out any obviously non-TGA files let colormap_type = header[1]; let image_type = header[2]; // Check the image type (byte 2) to be one of the uncompressed or RLE compressed types // Note: I've seen mention of types 0, 32, and 33 but have no example files so have omitted them. if image_type != 1 && image_type != 2 && image_type != 3 && image_type != 9 && image_type != 10 && image_type != 11 { return false; } // Check that the colormap type (byte 1) is either 0 (no colormap) or 1 (colormap present) // Technically 2-127 is reserved and 128-255 are usable by devs, but for simplicity we ignore them if colormap_type >= 2 { return false; } is_tga(reader, image_type, colormap_type).unwrap_or(false) } fn is_tga(reader: &mut R, image_type: u8, colormap_type: u8) -> ImageResult { // Attempt to go to footer section. This also doubles as a size check since // if there aren't 18 bytes available it will return an error. reader.seek(SeekFrom::End(-18))?; // Look for Version 2 TGA footer signature as it's the only concrete data to verify it's a TGA let mut signature = [0; 18]; reader.read_exact(&mut signature)?; // If signature is found then we should be confident it's a TGA // // We do not reset the seek here because size methods should // be seeking themselves as a first step anyway. if &signature == b"TRUEVISION-XFILE.\0" { return Ok(true); } // Now we're into heuristic territory. // With no footer I don't believe there is a 100% way to verify whether given bytes // are a TGA or not. To get around this we add a few corroborating byte checks and // if they make up a valid TGA configuration we assume that it's a TGA. // If image type is color mapped, then color map type must be set to 1 if (image_type == 1 || image_type == 9) && colormap_type != 1 { return Ok(false); } // Start reading the header information reader.seek(SeekFrom::Start(3))?; let colormap_offset = read_u32(reader, &Endian::Little)?; let colormap_size = read_u8(reader)?; // If there is no color map then assume that origin, length, and entry size must be 0 if colormap_type == 0 { if colormap_offset != 0 { return Ok(false); } if colormap_size != 0 { return Ok(false); } } // Assume color map sizes must be a multiple of 8 if colormap_type == 1 && (colormap_size != 0 && colormap_size != 8 && colormap_size != 16 && colormap_size != 24 && colormap_size != 32) { return Ok(false); } reader.seek(SeekFrom::Start(16))?; let pixel_size = read_u8(reader)?; let descriptor = read_u8(reader)?; let alpha_bits = descriptor & 0x0F; // Reserved bit, must be set to 0 if descriptor & 0x10 != 0 { return Ok(false); } // Assume pixel size must be a multiple of 8 if pixel_size != 8 && pixel_size != 16 && pixel_size != 24 && pixel_size != 32 { return Ok(false); } // Verify that the alpha bits value makes sense given pixel size // // 8 and 24 bits have no alpha if (pixel_size == 8 || pixel_size == 24) && alpha_bits != 0 { return Ok(false); } // 16 bits can either have 0 or 1 bits of alpha if pixel_size == 16 && alpha_bits >= 2 { return Ok(false); } // 32 bits must have 8 bits of alpha, although I've seen one with 0? if pixel_size == 32 && (alpha_bits != 8 && alpha_bits != 0) { return Ok(false); } Ok(true) } imagesize-0.12.0/src/formats/tiff.rs000064400000000000000000000062330072674642500154550ustar 00000000000000use crate::util::*; use crate::{ImageResult, ImageSize}; use std::io::{BufRead, Cursor, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(0))?; let mut endian_marker = [0; 2]; reader.read_exact(&mut endian_marker)?; // Get the endianness which determines how we read the input let endianness = if &endian_marker[0..2] == b"II" { Endian::Little } else if &endian_marker[0..2] == b"MM" { Endian::Big } else { // Shouldn't get here by normal means, but handle invalid header anyway return Err( std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid TIFF header").into(), ); }; // Read the IFD offset from the header reader.seek(SeekFrom::Start(4))?; let ifd_offset = read_u32(reader, &endianness)?; // IFD offset cannot be 0 if ifd_offset == 0 { return Err( std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid IFD offset").into(), ); } // Jump to the IFD offset reader.seek(SeekFrom::Start(ifd_offset.into()))?; // Read how many IFD records there are let ifd_count = read_u16(reader, &endianness)?; let mut width = None; let mut height = None; for _ifd in 0..ifd_count { let tag = read_u16(reader, &endianness)?; let kind = read_u16(reader, &endianness)?; let _count = read_u32(reader, &endianness)?; let value_bytes = match kind { // BYTE | ASCII | SBYTE | UNDEFINED 1 | 2 | 6 | 7 => 1, // SHORT | SSHORT 3 | 8 => 2, // LONG | SLONG | FLOAT | IFD 4 | 9 | 11 | 13 => 4, // RATIONAL | SRATIONAL 5 | 10 => 4 * 2, // DOUBLE | LONG8 | SLONG8 | IFD8 12 | 16 | 17 | 18 => 8, // Anything else is invalid _ => { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Invalid IFD type", ) .into()) } }; let mut value_buffer = [0; 4]; reader.read_exact(&mut value_buffer)?; let mut r = Cursor::new(&value_buffer[..]); let value = match value_bytes { 2 => Some(read_u16(&mut r, &endianness)? as u32), 4 => Some(read_u32(&mut r, &endianness)?), _ => None, }; // Tag 0x100 is the image width, 0x101 is image height if tag == 0x100 { width = value; } else if tag == 0x101 { height = value; } // If we've read both values we need, return the data if let (Some(width), Some(height)) = (width, height) { return Ok(ImageSize { width: width as usize, height: height as usize, }); } } // If no width/height pair was found return invalid data Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "No dimensions in IFD tags").into()) } pub fn matches(header: &[u8]) -> bool { header.starts_with(b"II\x2A\x00") || header.starts_with(b"MM\x00\x2A") } imagesize-0.12.0/src/formats/vtf.rs000064400000000000000000000006670072674642500153310ustar 00000000000000use crate::util::*; use crate::{ImageResult, ImageSize}; use std::io::{BufRead, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(16))?; Ok(ImageSize { width: read_u16(reader, &Endian::Little)? as usize, height: read_u16(reader, &Endian::Little)? as usize, }) } pub fn matches(header: &[u8]) -> bool { header.starts_with(b"VTF\0") } imagesize-0.12.0/src/formats/webp.rs000064400000000000000000000027600072674642500154630ustar 00000000000000use crate::util::*; use crate::{ImageResult, ImageSize}; use std::io::{BufRead, Seek, SeekFrom}; pub fn size(reader: &mut R) -> ImageResult { let mut buffer = [0; 4]; reader.read_exact(&mut buffer)?; if buffer[3] == b' ' { webp_vp8_size(reader) } else if buffer[3] == b'L' { webp_vp8l_size(reader) } else if buffer[3] == b'X' { webp_vp8x_size(reader) } else { Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid VP8 Tag").into()) } } pub fn matches(header: &[u8]) -> bool { header.len() >= 12 && &header[0..4] == b"RIFF" && &header[8..12] == b"WEBP" } fn webp_vp8x_size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(0x18))?; Ok(ImageSize { width: read_u24(reader, &Endian::Little)? as usize + 1, height: read_u24(reader, &Endian::Little)? as usize + 1, }) } fn webp_vp8l_size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(0x15))?; let dims = read_u32(reader, &Endian::Little)?; Ok(ImageSize { width: (dims & 0x3FFF) as usize + 1, height: ((dims >> 14) & 0x3FFF) as usize + 1, }) } fn webp_vp8_size(reader: &mut R) -> ImageResult { reader.seek(SeekFrom::Start(0x1A))?; Ok(ImageSize { width: read_u16(reader, &Endian::Little)? as usize, height: read_u16(reader, &Endian::Little)? as usize, }) } imagesize-0.12.0/src/lib.rs000064400000000000000000000166700072674642500136260ustar 00000000000000use std::error::Error; use std::fmt; use std::fs::File; use std::io::{BufRead, BufReader, Cursor, Seek}; use std::path::Path; mod util; mod formats; use formats::*; /// An Error type used in failure cases. #[derive(Debug)] pub enum ImageError { /// Used when the given data is not a supported format. NotSupported, /// Used when the image has an invalid format. CorruptedImage, /// Used when an IoError occurs when trying to read the given data. IoError(std::io::Error), } impl Error for ImageError {} impl fmt::Display for ImageError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::ImageError::*; match self { NotSupported => f.write_str("Could not decode image"), CorruptedImage => f.write_str("Hit end of file before finding size"), IoError(error) => error.fmt(f), } } } impl From for ImageError { fn from(err: std::io::Error) -> ImageError { ImageError::IoError(err) } } pub type ImageResult = Result; /// Types of image formats that this crate can identify. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ImageType { /// Animated sprite image format /// Aseprite, /// AV1 Image File Format Avif, /// Standard Bitmap Bmp, /// DirectDraw Surface Dds, /// OpenEXR Exr, /// Farbfeld /// Farbfeld, /// Standard GIF Gif, /// Radiance HDR Hdr, /// High Efficiency Image File Format Heif, /// Icon file Ico, /// Standard JPEG Jpeg, /// JPEG XL Jxl, /// Khronos Texture Container Ktx2, /// Standard PNG Png, /// Portable Any Map Pnm, /// Photoshop Document Psd, /// Quite OK Image Format /// Qoi, /// Truevision Graphics Adapter Tga, /// Standard TIFF Tiff, /// Valve Texture Format Vtf, /// Standard Webp Webp, } /// Holds the size information of an image. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ImageSize { /// Width of an image in pixels. pub width: usize, /// Height of an image in pixels. pub height: usize, } impl Ord for ImageSize { fn cmp(&self, other: &Self) -> std::cmp::Ordering { (self.width * self.height).cmp(&(other.width * other.height)) } } impl PartialOrd for ImageSize { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } /// Get the image type from a header /// /// # Arguments /// * `header` - The header of the file. /// /// # Remarks /// /// This will check the header to determine what image type the data is. pub fn image_type(header: &[u8]) -> ImageResult { formats::image_type(&mut Cursor::new(header)) } /// Get the image size from a local file /// /// # Arguments /// * `path` - A local path to the file to parse. /// /// # Remarks /// /// Will try to read as little of the file as possible in order to get the /// proper size information. /// /// # Error /// /// This method will return an [`ImageError`] under the following conditions: /// /// * The header isn't recognized as a supported image format /// * The data isn't long enough to find the size for the given format /// /// The minimum data required is 12 bytes. Anything shorter will return [`ImageError::IoError`]. /// /// # Examples /// /// ``` /// use imagesize::size; /// /// match size("test/test.webp") { /// Ok(dim) => { /// assert_eq!(dim.width, 716); /// assert_eq!(dim.height, 716); /// } /// Err(why) => println!("Error getting size: {:?}", why) /// } /// ``` /// /// [`ImageError`]: enum.ImageError.html pub fn size>(path: P) -> ImageResult { let file = File::open(path)?; let reader = BufReader::new(file); reader_size(reader) } /// Get the image size from a block of raw data. /// /// # Arguments /// * `data` - A Vec containing the data to parse for image size. /// /// # Error /// /// This method will return an [`ImageError`] under the following conditions: /// /// * The header isn't recognized as a supported image format /// * The data isn't long enough to find the size for the given format /// /// The minimum data required is 12 bytes. Anything shorter will return [`ImageError::IoError`]. /// /// # Examples /// /// ``` /// use imagesize::blob_size; /// /// // First few bytes of arbitrary data. /// let data = vec![0x89, 0x89, 0x89, 0x89, 0x0D, 0x0A, 0x1A, 0x0A, /// 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, /// 0x00, 0x00, 0x00, 0x7B, 0x01, 0x00, 0x01, 0x41, /// 0x08, 0x06, 0x00, 0x00, 0x00, 0x9A, 0x38, 0xC4]; /// /// assert_eq!(blob_size(&data).is_err(), true); /// ``` /// /// [`ImageError`]: enum.ImageError.html pub fn blob_size(data: &[u8]) -> ImageResult { let reader = Cursor::new(data); reader_size(reader) } /// Get the image size from a reader /// /// # Arguments /// * `reader` - A reader for the data /// /// # Error /// /// This method will return an [`ImageError`] under the following conditions: /// /// * The header isn't recognized as a supported image format /// * The data isn't long enough to find the size for the given format /// /// The minimum data required is 12 bytes. Anything shorter will return [`ImageError::IoError`]. /// /// # Examples /// /// ``` /// use std::io::Cursor; /// use imagesize::reader_size; /// /// // PNG Header with size 123x321 /// let reader = Cursor::new([ /// 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, /// 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, /// 0x00, 0x00, 0x00, 0x7B, 0x00, 0x00, 0x01, 0x41, /// 0x08, 0x06, 0x00, 0x00, 0x00, 0x9A, 0x38, 0xC4 /// ]); /// /// match reader_size(reader) { /// Ok(dim) => { /// assert_eq!(dim.width, 123); /// assert_eq!(dim.height, 321); /// } /// Err(why) => println!("Error getting reader size: {:?}", why) /// } /// ``` /// /// [`ImageError`]: enum.ImageError.html pub fn reader_size(mut reader: R) -> ImageResult { dispatch_header(&mut reader) } /// Calls the correct image size method based on the image type /// /// # Arguments /// * `reader` - A reader for the data /// * `header` - The header of the file fn dispatch_header(reader: &mut R) -> ImageResult { match formats::image_type(reader)? { ImageType::Aseprite => aesprite::size(reader), ImageType::Avif => heif::size(reader), // AVIF uses HEIF size on purpose ImageType::Bmp => bmp::size(reader), ImageType::Dds => dds::size(reader), ImageType::Exr => exr::size(reader), ImageType::Farbfeld => farbfeld::size(reader), ImageType::Gif => gif::size(reader), ImageType::Hdr => hdr::size(reader), ImageType::Heif => heif::size(reader), ImageType::Ico => ico::size(reader), ImageType::Jpeg => jpeg::size(reader), ImageType::Jxl => jxl::size(reader), ImageType::Ktx2 => ktx2::size(reader), ImageType::Png => png::size(reader), ImageType::Pnm => pnm::size(reader), ImageType::Psd => psd::size(reader), ImageType::Qoi => qoi::size(reader), ImageType::Tga => tga::size(reader), ImageType::Tiff => tiff::size(reader), ImageType::Vtf => vtf::size(reader), ImageType::Webp => webp::size(reader), } } imagesize-0.12.0/src/util.rs000064400000000000000000000110660072674642500140270ustar 00000000000000use crate::{ImageError, ImageResult}; use std::io::{self, BufRead, Seek}; /// Used for TIFF decoding pub enum Endian { Little, Big, } pub fn read_i32(reader: &mut R, endianness: &Endian) -> ImageResult { let mut attr_size_buf = [0; 4]; reader.read_exact(&mut attr_size_buf)?; match endianness { Endian::Little => Ok(i32::from_le_bytes(attr_size_buf)), Endian::Big => Ok(i32::from_be_bytes(attr_size_buf)), } } pub fn read_u32(reader: &mut R, endianness: &Endian) -> ImageResult { let mut buf = [0; 4]; reader.read_exact(&mut buf)?; match endianness { Endian::Little => Ok(((buf[3] as u32) << 24) | ((buf[2] as u32) << 16) | ((buf[1] as u32) << 8) | (buf[0] as u32)), Endian::Big => Ok(((buf[0] as u32) << 24) | ((buf[1] as u32) << 16) | ((buf[2] as u32) << 8) | (buf[3] as u32)), } } pub fn read_u24(reader: &mut R, endianness: &Endian) -> ImageResult { let mut buf = [0; 3]; reader.read_exact(&mut buf)?; match endianness { Endian::Little => Ok(((buf[2] as u32) << 16) | ((buf[1] as u32) << 8) | (buf[0] as u32)), Endian::Big => Ok(((buf[0] as u32) << 16) | ((buf[1] as u32) << 8) | (buf[2] as u32)), } } pub fn read_u16(reader: &mut R, endianness: &Endian) -> ImageResult { let mut buf = [0; 2]; reader.read_exact(&mut buf)?; match endianness { Endian::Little => Ok(((buf[1] as u16) << 8) | (buf[0] as u16)), Endian::Big => Ok(((buf[0] as u16) << 8) | (buf[1] as u16)), } } pub fn read_u8(reader: &mut R) -> ImageResult { let mut buf = [0; 1]; reader.read_exact(&mut buf)?; Ok(buf[0]) } pub fn read_bits(source: u128, num_bits: usize, offset: usize, size: usize) -> ImageResult { if offset + num_bits < size { Ok((source >> offset) as usize & ((1 << num_bits) - 1)) } else { Err(ImageError::CorruptedImage) } } /// Assumes tags are in format of 4 char string followed by big endian size for tag pub fn read_tag(reader: &mut R) -> ImageResult<(String, usize)> { let mut tag_buf = [0; 4]; let size = read_u32(reader, &Endian::Big)? as usize; reader.read_exact(&mut tag_buf)?; Ok((String::from_utf8_lossy(&tag_buf).into_owned(), size)) } pub fn read_until_capped(reader: &mut R, delimiter: u8, max_size: usize) -> io::Result> { let mut bytes = Vec::new(); let mut amount_read = 0; while amount_read < max_size { let mut byte = [0; 1]; reader.read_exact(&mut byte)?; if byte[0] == delimiter { break; } bytes.push(byte[0]); amount_read += 1; } if amount_read >= max_size { return Err(io::Error::new(io::ErrorKind::InvalidData, format!("Delimiter not found within {} bytes", max_size))); } Ok(bytes) } /// Skips all starting whitespace characters and then reads a string until the next whitespace character /// Example: /// " test string" => "test" pub fn read_until_whitespace(reader: &mut R, max_size: usize) -> io::Result { let mut bytes = Vec::new(); let mut amount_read = 0; let mut seen_non_whitespace = false; while amount_read < max_size { amount_read += 1; let mut byte = [0; 1]; reader.read_exact(&mut byte)?; if byte[0].is_ascii_whitespace() { // If we've seen a non-whitespace character before then exit if seen_non_whitespace { break; } // Skip whitespace until we found first non-whitespace character continue; } bytes.push(byte[0]); seen_non_whitespace = true; } if amount_read >= max_size { return Err(io::Error::new(io::ErrorKind::InvalidData, format!("Delimiter not found within {} bytes", max_size))); } String::from_utf8(bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) } pub fn read_line_capped(reader: &mut R, max_size: usize) -> io::Result { let bytes = read_until_capped(reader, b'\n', max_size)?; String::from_utf8(bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) } pub fn read_null_terminated_string(reader: &mut R, max_size: usize) -> io::Result { let bytes = read_until_capped(reader, 0, max_size)?; String::from_utf8(bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) }