Editor utility that reads image metadata without decoding pixels. It detects PNG, BMP, JPEG, Photoshop (PSD), TIFF, TGA and EXR headers to return width/height or EXR compression, and includes helpers for power-of-two checks and nearest power-of-two calculation used by texture import logic.
using System;
using System.IO;
namespace ImportUnityPackage;
/// <summary>Reads image dimensions from file headers without decoding pixels. Unknown formats return null.</summary>
internal static class UnityImageSize
{
internal static (int Width, int Height)? Read( string path )
{
try
{
using var stream = File.OpenRead( path );
var header = new byte[30];
if ( stream.Read( header ) < 26 ) return null;
int Big32( int at ) => header[at] << 24 | header[at + 1] << 16 | header[at + 2] << 8 | header[at + 3];
int Little16( int at ) => header[at] | header[at + 1] << 8;
if ( header[0] == 0x89 && header[1] == 'P' && header[2] == 'N' && header[3] == 'G' ) return Valid( Big32( 16 ), Big32( 20 ) );
if ( header[0] == 'B' && header[1] == 'M' ) return Valid( Little16( 18 ) | Little16( 20 ) << 16, Math.Abs( Little16( 22 ) | Little16( 24 ) << 16 ) );
if ( header[0] == 0xFF && header[1] == 0xD8 ) return Jpeg( stream );
// Photoshop: signature, version, 6 reserved bytes, channels, then height and width (big-endian).
if ( header[0] == '8' && header[1] == 'B' && header[2] == 'P' && header[3] == 'S' ) return Valid( Big32( 18 ), Big32( 14 ) );
if ( header[0] == 'I' && header[1] == 'I' && header[2] == 42 || header[0] == 'M' && header[1] == 'M' && header[3] == 42 ) return Tiff( stream, header[0] == 'I' );
// TGA has no signature; accept only uncompressed or RLE true-color, greyscale and palette images.
if ( Path.GetExtension( path ).Equals( ".tga", StringComparison.OrdinalIgnoreCase ) && header[2] is 1 or 2 or 3 or 9 or 10 or 11 )
return Valid( Little16( 12 ), Little16( 14 ) );
return null;
}
catch ( IOException ) { return null; }
}
/// <summary>A TIFF's first image size: the ImageWidth (256) and ImageLength (257) entries of its first directory.</summary>
static (int, int)? Tiff( Stream stream, bool little )
{
var buffer = new byte[12];
int U16( int at ) => little ? buffer[at] | buffer[at + 1] << 8 : buffer[at] << 8 | buffer[at + 1];
long U32( int at ) => little ? (uint)(buffer[at] | buffer[at + 1] << 8 | buffer[at + 2] << 16 | buffer[at + 3] << 24) : (uint)(buffer[at] << 24 | buffer[at + 1] << 16 | buffer[at + 2] << 8 | buffer[at + 3]);
stream.Position = 4;
if ( stream.Read( buffer, 0, 4 ) != 4 ) return null;
var directory = U32( 0 );
if ( directory <= 0 || directory >= stream.Length ) return null;
stream.Position = directory;
if ( stream.Read( buffer, 0, 2 ) != 2 ) return null;
var count = U16( 0 );
int width = 0, height = 0;
for ( var i = 0; i < count && i < 512; i++ )
{
if ( stream.Read( buffer, 0, 12 ) != 12 ) return null;
var tag = U16( 0 );
if ( tag is not (256 or 257) ) continue;
// SHORT (3) values sit in the first two bytes of the value field, LONG (4) in all four.
var value = U16( 2 ) == 3 ? U16( 8 ) : (int)U32( 8 );
if ( tag == 256 ) width = value; else height = value;
}
return Valid( width, height );
}
static (int, int)? Valid( int width, int height ) => width > 0 && height > 0 && width <= 65536 && height <= 65536 ? (width, height) : null;
static (int, int)? Jpeg( Stream stream )
{
// Walk marker segments to the first start-of-frame (SOF0-SOF15, except DHT/JPG/DAC).
stream.Position = 2;
var buffer = new byte[7];
while ( stream.Read( buffer, 0, 4 ) == 4 )
{
if ( buffer[0] != 0xFF ) return null;
var marker = buffer[1];
var length = buffer[2] << 8 | buffer[3];
if ( length < 2 ) return null;
if ( marker is >= 0xC0 and <= 0xCF and not 0xC4 and not 0xC8 and not 0xCC )
{
if ( stream.Read( buffer, 0, 5 ) != 5 ) return null;
return Valid( buffer[3] << 8 | buffer[4], buffer[1] << 8 | buffer[2] );
}
stream.Position += length - 2;
}
return null;
}
/// <summary>An OpenEXR file's compression attribute (0 none … 8 DWAA, 9 DWAB), or null when it cannot be read.</summary>
internal static int? ExrCompression( string path )
{
try
{
using var stream = File.OpenRead( path );
var header = new byte[Math.Min( 65536, stream.Length )];
stream.ReadExactly( header );
if ( header.Length < 8 || header[0] != 0x76 || header[1] != 0x2F || header[2] != 0x31 || header[3] != 0x01 ) return null;
// Attributes: name\0 type\0 size(int32) value, until an empty name.
var at = 8;
while ( at < header.Length && header[at] != 0 )
{
var nameEnd = Array.IndexOf( header, (byte)0, at );
var typeEnd = nameEnd < 0 ? -1 : Array.IndexOf( header, (byte)0, nameEnd + 1 );
if ( typeEnd < 0 || typeEnd + 5 > header.Length ) return null;
var name = System.Text.Encoding.ASCII.GetString( header, at, nameEnd - at );
var size = BitConverter.ToInt32( header, typeEnd + 1 );
if ( size < 0 ) return null; // Corrupt: a negative size would move backwards and never end.
if ( name == "compression" && size == 1 && typeEnd + 5 < header.Length ) return header[typeEnd + 5];
at = typeEnd + 5 + size;
}
return null;
}
catch ( IOException ) { return null; }
}
internal static bool IsPowerOfTwo( int value ) => value > 0 && (value & (value - 1)) == 0;
/// <summary>Unity's ClosestPowerOfTwo, used by the texture importer's nPOTScale "ToNearest" (ties round down).</summary>
internal static int NearestPowerOfTwo( int value )
{
if ( value <= 1 ) return 1;
var next = 1;
while ( next < value ) next <<= 1;
var previous = next >> 1;
return next - value < value - previous ? next : previous;
}
}