Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
54 changes: 32 additions & 22 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,41 @@ harness = false

[features]
default = [
"aesprite",
"bmp",
"dds",
"exr",
"farbfeld",
"gif",
"hdr",
"ico",
"ilbm",
"jpeg",
"jxl",
"ktx2",
"mod",
"png",
"pnm",
"psd",
"qoi",
"tga",
"tiff",
"vtf",
"webp",
"heif",
"aesprite",
"astc",
"atc",
"bmp",
"dds",
"eac",
"etc2",
"exr",
"farbfeld",
"gif",
"hdr",
"ico",
"ilbm",
"jpeg",
"jxl",
"ktx2",
"mod",
"png",
"pnm",
"pvrtc",
"psd",
"qoi",
"tga",
"tiff",
"vtf",
"webp",
"heif",
]
aesprite = []
astc = []
atc = []
bmp = []
dds = []
eac = []
etc2 = []
exr = []
farbfeld = []
gif = []
Expand All @@ -62,6 +71,7 @@ ktx2 = []
mod = []
png = []
pnm = []
pvrtc = []
psd = []
qoi = []
tga = []
Expand Down
69 changes: 67 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ 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.

## Features
- **Fast**: Reads only the necessary bytes to determine image dimensions
- **Lightweight**: Minimal dependencies
- **Texture Format Support**: Detects compression algorithms in DDS, PowerVR, PKM, and other texture containers
- **Cross-Container Queries**: Helper methods to identify compression families across different container formats
- **Backward Compatible**: All existing APIs remain unchanged

## Usage
Add the following to your Cargo.toml:
```toml
Expand All @@ -13,10 +20,11 @@ imagesize = "0.14"
```

## Supported Image Formats

### Simple Image Formats
* Aseprite
* Avif
* BMP
* DDS
* EXR
* Farbfeld
* GIF
Expand All @@ -26,7 +34,6 @@ imagesize = "0.14"
* ILBM (IFF)
* JPEG
* JPEG XL
* KTX2
* PNG
* PNM (PBM, PGM, PPM)
* PSD / PSB
Expand All @@ -36,6 +43,14 @@ imagesize = "0.14"
* VTF
* WEBP

### Texture Container Formats with Compression Detection
* **DDS** - DirectDraw Surface with BC1-7 (DXT1-5) compression detection
* **PKM** - ETC1/ETC2/EAC compressed textures
* **PowerVR** - PVRTC, ETC2, and EAC compressed textures
* **ATC** - Adaptive Texture Compression (Qualcomm Adreno)
* **ASTC** - Adaptive Scalable Texture Compression
* **KTX2** - Khronos Texture Container

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.
Expand All @@ -59,6 +74,56 @@ match imagesize::blob_size(&data) {
}
```

### Texture Format Detection
For texture container formats, you can detect both the container type and compression algorithm:

```rust
use imagesize::{image_type, ImageType, CompressionFamily};

let data = std::fs::read("texture.dds").unwrap();
match image_type(&data) {
Ok(ImageType::Dds(compression)) => {
println!("DDS texture with {:?} compression", compression);
}
Ok(ImageType::Pvrtc(compression)) => {
println!("PowerVR texture with {:?} compression", compression);
}
Ok(other) => println!("Other format: {:?}", other),
Err(e) => println!("Error: {:?}", e),
}
```

### Cross-Container Compression Queries
Use helper methods to query compression information across different container formats:

```rust
use imagesize::{image_type, CompressionFamily};

let data = std::fs::read("texture.pvr").unwrap();
if let Ok(img_type) = image_type(&data) {
// Group related compression algorithms regardless of container
match img_type.compression_family() {
Some(CompressionFamily::Etc) => println!("ETC family compression"),
Some(CompressionFamily::BlockCompression) => println!("BC/DXT compression"),
Some(CompressionFamily::Pvrtc) => println!("PVRTC compression"),
_ => println!("Other or no compression"),
}

// Query container and compression properties
if img_type.is_block_compressed() {
println!("Uses block compression (BC1-7)");
}

if let Some(container) = img_type.container_format() {
println!("Container format: {}", container);
}

if img_type.is_multi_compression_container() {
println!("Container supports multiple compression types");
}
}
```

[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
Expand Down
107 changes: 107 additions & 0 deletions src/container/atc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use std::io::{BufRead, Seek, SeekFrom};

use crate::{
util::{read_u16, read_u32, Endian},
ImageResult, ImageSize,
};

/// Compression formats for ATC containers
///
/// Adaptive Texture Compression (ATC) is used primarily on Adreno GPUs.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AtcCompression {
/// ATC RGB (no alpha)
Rgb,
/// ATC RGBA with explicit alpha
RgbaExplicit,
/// ATC RGBA with interpolated alpha
RgbaInterpolated,
/// Other/Unknown ATC format
Unknown,
}

pub fn size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
// ATC files typically use DDS container format
// But also can be in PKM format or custom ATC format

// Try DDS format first
let mut header = [0u8; 4];
reader.read_exact(&mut header)?;

if header == *b"DDS " {
// DDS format - seek to dimensions
reader.seek(SeekFrom::Start(12))?;
let height = read_u32(reader, &Endian::Little)? as usize;
let width = read_u32(reader, &Endian::Little)? as usize;
return Ok(ImageSize { width, height });
}

// Try PKM format (used by some ATC implementations)
reader.seek(SeekFrom::Start(0))?;
let mut pkm_header = [0u8; 8];
reader.read_exact(&mut pkm_header)?;

if pkm_header.starts_with(b"PKM ")
&& (pkm_header[4..6] == [b'1', b'0'] || pkm_header[4..6] == [b'2', b'0'])
{
// Check for ATC-specific data types
let data_type = u16::from_be_bytes([pkm_header[6], pkm_header[7]]);
if matches!(data_type, 0x8C92 | 0x8C93 | 0x87EE) {
// ATC_RGB, ATC_RGBA_EXPLICIT_ALPHA, and ATC_RGBA_INTERPOLATED_ALPHA
reader.seek(SeekFrom::Start(8))?; // Skip magic + version + data type
let _extended_width = read_u16(reader, &Endian::Big)?;
let _extended_height = read_u16(reader, &Endian::Big)?;
let width = read_u16(reader, &Endian::Big)? as usize;
let height = read_u16(reader, &Endian::Big)? as usize;
return Ok(ImageSize { width, height });
}
}

// Fallback: assume basic ATC dimensions at a standard location
reader.seek(SeekFrom::Start(4))?;
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 {
// Only check for PKM format with ATC data types
// DDS files with ATC compression should be handled by the DDS format detector
if header.len() >= 8
&& header.starts_with(b"PKM ")
&& (header[4..6] == [b'1', b'0'] || header[4..6] == [b'2', b'0'])
{
let data_type = u16::from_be_bytes([header[6], header[7]]);
return matches!(data_type, 0x8C92 | 0x8C93 | 0x87EE);
}

false
}

pub fn detect_compression<R: BufRead + Seek>(reader: &mut R) -> ImageResult<AtcCompression> {
// Check if it's a PKM format first
let mut header = [0u8; 8];
reader.seek(SeekFrom::Start(0))?;
reader.read_exact(&mut header)?;

if header.starts_with(b"PKM ") && (header[4..6] == [b'1', b'0'] || header[4..6] == [b'2', b'0'])
{
let data_type = u16::from_be_bytes([header[6], header[7]]);
let compression = match data_type {
0x8C92 => AtcCompression::Rgb, // ATC_RGB
0x8C93 => AtcCompression::RgbaExplicit, // ATC_RGBA_EXPLICIT_ALPHA
0x87EE => AtcCompression::RgbaInterpolated, // ATC_RGBA_INTERPOLATED_ALPHA
_ => AtcCompression::Unknown,
};
return Ok(compression);
}

// Check if it's DDS format
if header[0..4] == *b"DDS " {
// For DDS, we'd need to check the pixel format section for ATC FourCC
// This is a more complex check that would examine the DDS pixel format
return Ok(AtcCompression::Unknown); // Default for DDS-contained ATC
}

Ok(AtcCompression::Unknown)
}
103 changes: 103 additions & 0 deletions src/container/dds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use std::io::{BufRead, Seek, SeekFrom};

use crate::{
util::{read_u32, Endian},
ImageResult, ImageSize,
};

/// Compression formats for DDS containers
///
/// DirectDraw Surface (DDS) files can contain various compressed and uncompressed formats.
/// This enum identifies the specific compression algorithm used within the DDS container.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DdsCompression {
/// Block Compression 1 (DXT1) - RGB, 1-bit alpha
Bc1,
/// Block Compression 2 (DXT3) - RGBA with explicit alpha
Bc2,
/// Block Compression 3 (DXT5) - RGBA with interpolated alpha
Bc3,
/// Block Compression 4 (ATI1) - Single channel
Bc4,
/// Block Compression 5 (ATI2) - Two channel (RG)
Bc5,
/// Block Compression 6H - HDR format
Bc6h,
/// Block Compression 7 - High quality RGB/RGBA
Bc7,
/// Uncompressed RGBA32
Rgba32,
/// Uncompressed RGB24
Rgb24,
/// Other/Unknown DDS format
Unknown,
}

pub fn size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
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 ")
}

pub fn detect_compression<R: BufRead + Seek>(reader: &mut R) -> ImageResult<DdsCompression> {
// DDS header structure:
// Signature: "DDS " (4 bytes)
// Header size: 124 (4 bytes)
// Flags: various flags (4 bytes)
// Height, Width: (4 bytes each)
// PitchOrLinearSize: (4 bytes)
// Depth: (4 bytes)
// MipMapCount: (4 bytes)
// Reserved1: (44 bytes)
// Pixel Format: (32 bytes)
// - Size: 32 (4 bytes)
// - Flags: (4 bytes)
// - FourCC: (4 bytes) - this tells us the compression format
// - RGBBitCount: (4 bytes)
// - RBitMask, GBitMask, BBitMask, ABitMask: (16 bytes)

reader.seek(SeekFrom::Start(84))?; // Skip to pixel format FourCC
let mut fourcc = [0u8; 4];
reader.read_exact(&mut fourcc)?;

let compression = match &fourcc {
b"DXT1" => DdsCompression::Bc1,
b"DXT3" => DdsCompression::Bc2,
b"DXT5" => DdsCompression::Bc3,
b"ATI1" | b"BC4U" | b"BC4S" => DdsCompression::Bc4,
b"ATI2" | b"BC5U" | b"BC5S" => DdsCompression::Bc5,
b"BC6H" => DdsCompression::Bc6h,
b"BC7U" | b"BC7L" => DdsCompression::Bc7,
b"DX10" => {
// DX10 extended header starts right after the main DDS header (128 bytes from start)
// Skip the rest of the pixel format, caps, and reserved2 fields first
reader.seek(SeekFrom::Start(128))?; // Jump to DX10 extended header
let dxgi_format = read_u32(reader, &Endian::Little)?;
match dxgi_format {
95 => DdsCompression::Bc6h, // DXGI_FORMAT_BC6H_UF16
96 => DdsCompression::Bc6h, // DXGI_FORMAT_BC6H_SF16
98 => DdsCompression::Bc7, // DXGI_FORMAT_BC7_UNORM
99 => DdsCompression::Bc7, // DXGI_FORMAT_BC7_UNORM_SRGB
_ => DdsCompression::Unknown,
}
}
[0, 0, 0, 0] => {
// No FourCC, check if it's uncompressed
// We need to check the RGB bit count and masks
let rgb_bit_count = read_u32(reader, &Endian::Little)?;
match rgb_bit_count {
32 => DdsCompression::Rgba32,
24 => DdsCompression::Rgb24,
_ => DdsCompression::Unknown,
}
}
_ => DdsCompression::Unknown,
};

Ok(compression)
}
2 changes: 1 addition & 1 deletion src/container/heif.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ fn skip_to_tag<R: BufRead + Seek>(reader: &mut R, tag: &[u8]) -> ImageResult<u32
} else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid heif box size: {}", size),
format!("Invalid heif box size: {size}"),
)
.into());
}
Expand Down
Loading
Loading