diff --git a/Cargo.lock b/Cargo.lock index 07431fc..20c0429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,55 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -20,6 +69,52 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "heck" version = "0.5.0" @@ -31,6 +126,7 @@ name = "imgsize" version = "3.0.1" dependencies = [ "byteorder", + "clap", "paste", "pyo3", "serde", @@ -43,6 +139,12 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.10" @@ -200,6 +302,12 @@ dependencies = [ "serde", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.71" @@ -228,3 +336,82 @@ name = "unindent" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 8e3231d..fecf91f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,13 @@ crate-type = ["cdylib", "lib"] [dependencies] byteorder = "1.5.0" pyo3 = "0.22.1" +clap = { version = "4.5.16", features = ["derive"] } [dev-dependencies] +clap = { version = "4.5.16", features = ["derive"] } paste = "1.0.15" serde = { version = "1.0.195", features = ["derive"] } serde_json = "1.0.111" + +[[example]] +name = "cli" diff --git a/README.md b/README.md index 6698d55..30d2c93 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,8 @@ else: size.is_animated ``` -You should not pass the entire image data, the first kilobyte or so should suffice. +You should not pass the entire image data, the first kilobyte or so should suffice for most formats, other than GIF +where a larger amount of data may be required to determine whether the image is animated or not. ### API @@ -60,6 +61,11 @@ that does actual image parsing to determine if the data is actually an image. and does not necessarily support all features or variants of those formats, as a result, there might be false positives and false negatives. +## Example CLI + +You can use `cargo example --cli` for a simple command line tool to try out this library. See +`cargo example --cli -- --help` for details. + ## Building Use [maturin](https://www.maturin.rs/) to build: `maturin build` diff --git a/examples/cli.rs b/examples/cli.rs index 3113311..9572e9c 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -1,20 +1,32 @@ extern crate imgsize; +use clap::Parser; use std::fs::File; +use std::io; use std::io::Read; -use std::{env, io}; +use std::path::PathBuf; + +#[derive(Parser)] +struct Arguments { + #[arg(short, long, default_value_t = 1024)] + buf_size: usize, + paths: Vec, +} pub fn main() -> io::Result<()> { - let mut buffer = [0u8; 1024]; - for path in env::args().skip(1) { + let arguments = Arguments::parse(); + let mut buffer = vec![0u8; arguments.buf_size]; + for path in arguments.paths.iter() { + let name = path.to_str().unwrap(); let mut file = File::open(&path)?; - file.read(&mut buffer)?; - match imgsize::get_size(&buffer) { + let read = file.read(&mut buffer)?; + match imgsize::get_size(&buffer[..read]) { Some(size) => println!( "{}: {}x{}, {}, animated={}", - path, size.width, size.height, size.mime_type, size.is_animated + name, size.width, size.height, size.mime_type, size.is_animated ), - None => println!("{}: unsupported format", path), + None => println!("{}: unsupported format", name), } + buffer.fill(0); } Ok(()) } diff --git a/python-tests/test_sample_files.py b/python-tests/test_sample_files.py index c8c9318..e69a5db 100644 --- a/python-tests/test_sample_files.py +++ b/python-tests/test_sample_files.py @@ -1,9 +1,7 @@ import json - import pytest -from conftest import ROOT -BYTES_TO_READ = 1024 +from conftest import ROOT def find_examples(): @@ -12,7 +10,7 @@ def find_examples(): output_path = input_path.with_suffix('.output') if not output_path.exists(): continue with input_path.open('rb') as fobj: - data = fobj.read(BYTES_TO_READ) + data = fobj.read() with output_path.open('r') as fobj: output = json.load(fobj) yield pytest.param(data, output, id=input_path.stem) diff --git a/src/gif.rs b/src/gif.rs index 407853e..4e6855f 100644 --- a/src/gif.rs +++ b/src/gif.rs @@ -1,4 +1,5 @@ -use std::io::{Read, Seek, SeekFrom}; +use std::io; +use std::io::{Cursor, Seek, SeekFrom}; use byteorder::{LittleEndian, ReadBytesExt}; @@ -6,38 +7,101 @@ use crate::utils::cursor_parser; use crate::Size; const MIME_TYPE: &str = "image/gif"; -const ANIMATION_EXTENSION: [u8; 11] = [ - 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30, -]; pub fn get_size(data: &[u8]) -> Option { cursor_parser(data, |mut cursor| { + // skip header cursor.seek(SeekFrom::Start(6))?; - let width = cursor.read_u16::()?; - let height = cursor.read_u16::()?; - let gtc_flag = cursor.read_u8()?; - let gce_offset = 0xd - + if gtc_flag & (1 << 7) != 0 { - // Ref : https://www.w3.org/Graphics/GIF/spec-gif89a.txt - // 3 x 2^(Size of Global Color Table+1) - (1 << ((gtc_flag & 0x07) + 1)) * 3 - } else { - 0 - }; - cursor.seek(SeekFrom::Start(gce_offset))?; - let animated = if cursor.read_u8()? == 0x21 { - cursor.seek(SeekFrom::Start(gce_offset + 3))?; - let mut buf = [0u8; 11]; - cursor.read_exact(&mut buf)?; - buf == ANIMATION_EXTENSION - } else { - false - }; - Ok(Some(Size::new( - width as u64, - height as u64, - MIME_TYPE.to_string(), - animated, - ))) + // Logical Screen Descriptor + let width = cursor.read_u16::()? as u64; + let height = cursor.read_u16::()? as u64; + let flags = cursor.read_u8()?; + // skip Background Color Index and Pixel Aspect Ratio + cursor.seek(SeekFrom::Current(2))?; + if let Some(size) = color_table_size(flags) { + // skip Global Color Table + cursor.seek(SeekFrom::Current(size))?; + } + let mut found_image = false; + let mut gce_found = false; + loop { + match cursor.read_u8()? { + // Image Descriptor + 0x2c => { + if found_image { + return Ok(Some(Size::new(width, height, MIME_TYPE.to_string(), true))); + } else if !gce_found { + return Ok(Some(Size::new(width, height, MIME_TYPE.to_string(), false))); + } + found_image = true; + cursor.seek(SeekFrom::Current(8))?; + let flags = cursor.read_u8()?; + if let Some(size) = color_table_size(flags) { + cursor.seek(SeekFrom::Current(size))?; + } + // skip LZW Minimum Code Size + cursor.seek(SeekFrom::Current(1))?; + skip_data_sub_blocks(&mut cursor)?; + } + // Extension + 0x21 => match cursor.read_u8()? { + // Graphic Control Extension + 0xf9 => { + gce_found = true; + // skip block size (always 4) and extension data + cursor.seek(SeekFrom::Current(5))?; + skip_data_sub_blocks(&mut cursor)?; + } + // Comment Extension + 0xfe => { + skip_data_sub_blocks(&mut cursor)?; + } + // Plain Text Extension + 0x01 => { + // skip block size (always 12) and extension data + cursor.seek(SeekFrom::Current(13))?; + skip_data_sub_blocks(&mut cursor)?; + } + // Application Extension + 0xff => { + // skip block size (always 11) and extension data + cursor.seek(SeekFrom::Current(12))?; + skip_data_sub_blocks(&mut cursor)?; + } + _ => { + return Ok(None); + } + }, + // Trailer + 0x3B => { + if found_image { + return Ok(Some(Size::new(width, height, MIME_TYPE.to_string(), false))); + } + return Ok(None); + } + _ => return Ok(None), + } + } }) } + +fn color_table_size(flags: u8) -> Option { + // Ref : https://www.w3.org/Graphics/GIF/spec-gif89a.txt + // 3 x 2^(Size of Global Color Table+1) + if flags & (1 << 7) != 0 { + Some((1 << ((flags & 0x07) + 1)) * 3) + } else { + None + } +} + +fn skip_data_sub_blocks(cursor: &mut Cursor<&[u8]>) -> io::Result<()> { + loop { + match cursor.read_u8()? { + 0x00 => return Ok(()), + size => { + cursor.seek(SeekFrom::Current(size as i64))?; + } + } + } +}