Search Unity

Getting original size of texture asset in pixels

Discussion in 'Editor & General Support' started by numberkruncher, Jan 7, 2013.

  1. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    949
    Hey guys

    Unity does not provide a documented API for accessing the width and height of a texture asset before any processing has been applied by the texture importer. So if you have a texture that measures 210x210 in Photoshop, when imported into Unity the texture will likely read 256x256 due to NPOT and/or maximum resolution.

    It is often useful to access the original width and height of the texture asset from editor scripts when performing certain calculations, especially when working with things like atlases. Having searched the web thoroughly the best solution that I could come across was to temporarily alter the texture importer settings for a texture asset, and then restore them. This worked but was very slow due to the need to reimport the texture asset twice.

    For example of this approach see: http://philippseifried.com/blog/2012/07/30/unity3d-code-original-texture-siz/

    After chatting with some developers in the IRC channel we discovered the internal function "TextureImporter.GetWidthAndHeight". Using reflection to invoke this function it seems to return the original size of the texture (as opposed to the imported size).

    Here is a wrapper function that I wrote which seems to work fine, and more importantly is significantly faster and more efficient than the other approach:

    Code (csharp):
    1. public static bool GetImageSize(Texture2D asset, out int width, out int height) {
    2.     if (asset != null) {
    3.         string assetPath = AssetDatabase.GetAssetPath(asset);
    4.         TextureImporter importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
    5.  
    6.         if (importer != null) {
    7.             object[] args = new object[2] { 0, 0 };
    8.             MethodInfo mi = typeof(TextureImporter).GetMethod("GetWidthAndHeight", BindingFlags.NonPublic | BindingFlags.Instance);
    9.             mi.Invoke(importer, args);
    10.  
    11.             width = (int)args[0];
    12.             height = (int)args[1];
    13.  
    14.             return true;
    15.         }
    16.     }
    17.  
    18.     height = width = 0;
    19.     return false;
    20. }
    The ability to access the original width and height of a texture asset is very useful for extension developers. In fact there are a number of forum, Q&A and blog posts by editor developers who have asked for this functionality. An important feature of an asset that I am working on requires this functionality for sure!

    Please vote here to have the internal function made public and documented for safe usage:

    http://feedback.unity3d.com/unity/all-categories/1/hot/active/get-original-width-and-height-of
     
    Last edited: Jan 7, 2013
  2. KyleStaves

    KyleStaves

    Joined:
    Nov 4, 2009
    Posts:
    820
    Thanks, we found this quite useful!
     
  3. Juruhn

    Juruhn

    Joined:
    Jun 25, 2013
    Posts:
    8
    Hi,

    I voted on the idea on the given site. I am really more a unityscript user so don’t really know how to implement this given solution.
    It is exactly what I was looking for so if you can give me a shot in the right direction I would appreciate it.

    Thnx
     
  4. numberkruncher

    numberkruncher

    Joined:
    Feb 18, 2012
    Posts:
    949
    I have converted this to UnityScript for you, though have made a few changes to workaround the fact that UnityScript does not support parametrized outputs:
    Code (csharp):
    1.  
    2. #pragma strict
    3.  
    4. import System.Reflection;
    5.  
    6. class ImageSize extends System.ValueType {
    7.  
    8.     public static var zero:ImageSize = new ImageSize(0, 0);
    9.  
    10.     public static function GetImageSize(asset:Texture2D):ImageSize {
    11.         if (asset != null) {
    12.             var assetPath:String = AssetDatabase.GetAssetPath(asset);
    13.             var importer:TextureImporter = AssetImporter.GetAtPath(assetPath) as TextureImporter;
    14.            
    15.             if (importer != null) {
    16.                 var args:System.Object[] = [ 0, 0 ];
    17.                 var mi:MethodInfo = typeof(TextureImporter).GetMethod('GetWidthAndHeight', BindingFlags.NonPublic | BindingFlags.Instance);
    18.                 mi.Invoke(importer, args);
    19.                
    20.                 return new ImageSize(args[0], args[1]);
    21.             }
    22.         }
    23.        
    24.         return zero;
    25.     }
    26.  
    27.     public var width:int;
    28.     public var height:int;
    29.    
    30.     public function ImageSize(width:int, height:int) {
    31.         this.width = width;
    32.         this.height = height;
    33.     }
    34.    
    35. }
    36.  
    And a window to test this:
    Code (csharp):
    1.  
    2. #pragma strict
    3.  
    4. class ImageSizeTest extends EditorWindow {
    5.  
    6.     @MenuItem('Window/Test')
    7.     static function Show() {
    8.         GetWindow.<ImageSizeTest>();
    9.     }
    10.    
    11.     var texture:Texture2D;
    12.     var size:ImageSize;
    13.    
    14.     function OnGUI() {
    15.         texture = EditorGUILayout.ObjectField(texture, typeof(Texture2D)) as Texture2D;
    16.         if (GUILayout.Button('Find Size'))
    17.             size = ImageSize.GetImageSize(texture);
    18.        
    19.         GUILayout.Label(String.Format('{0} x {1}', size.width, size.height));
    20.     }
    21.    
    22. }
    23.  
     
  5. andymads

    andymads

    Joined:
    Jun 16, 2011
    Posts:
    1,463
    Thanks very much for this.
     
  6. LoTekK

    LoTekK

    Joined:
    Jul 19, 2010
    Posts:
    136
    Thanks much for this. I ran into this issue just today, so your method has basically solved any issues I was having. Would definitely be nice to have this be officially available to the API, but for now this'll do nicely (since it's for an internal tool).

    As a side note, I made a modification to the method, since I'm currently using it exclusively in a custom AssetPostprocessor. I'm basically passing it the TextureImporter directly, instead of a texture:

    Code (csharp):
    1. public static bool GetImageSize(TextureImporter importer, out int width, out int height)
    Which saves the step of needing the AssetDatabase to find the file.
     
    jcuriel_glu likes this.
  7. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    350
    Here's my version that caches the reflection in a delegate and should be much faster if used frequently. It also has overloads for both Texture2D and TextureImporter and returns the size instead of using out parameters.

    Code (CSharp):
    1. private delegate void GetWidthAndHeight(TextureImporter importer, ref int width, ref int height);
    2. private static GetWidthAndHeight getWidthAndHeightDelegate;
    3.  
    4. public struct Size {
    5.     public int width;
    6.     public int height;
    7. }
    8.  
    9. public static Size GetOriginalTextureSize(Texture2D texture)
    10. {
    11.     if (texture == null)
    12.         throw new NullReferenceException();
    13.  
    14.     var path = AssetDatabase.GetAssetPath(texture);
    15.     if (string.IsNullOrEmpty(path))
    16.         throw new Exception("Texture2D is not an asset texture.");
    17.  
    18.     var importer = AssetImporter.GetAtPath(path) as TextureImporter;
    19.     if (importer == null)
    20.         throw new Exception("Failed to get Texture importer for " + path);
    21.  
    22.     return GetOriginalTextureSize(importer);
    23. }
    24.  
    25. public static Size GetOriginalTextureSize(TextureImporter importer)
    26. {
    27.     if (getWidthAndHeightDelegate == null) {
    28.         var method = typeof(TextureImporter).GetMethod("GetWidthAndHeight", BindingFlags.NonPublic | BindingFlags.Instance);
    29.         getWidthAndHeightDelegate = Delegate.CreateDelegate(typeof(GetWidthAndHeight), null, method) as GetWidthAndHeight;
    30.     }
    31.  
    32.     var size = new Size();
    33.     getWidthAndHeightDelegate(importer, ref size.width, ref size.height);
    34.  
    35.     return size;
    36. }
     
    fdsagizi2, vexe, howong and 2 others like this.
  8. Wolkenschauer

    Wolkenschauer

    Joined:
    Jan 27, 2014
    Posts:
    2
    And how would you guys get the real size on runtime? I mean not unityeditor only wise? What is the point in hiding the aspect rate from the developer anyway?
     
  9. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    350
    @Wolkenschauer
    You don't. That information is not accessible and Unity likely only has the imported scaled-down texture without any information about the original image at runtime.

    Your only option is to collect the information yourself at edit-time (using the methods lined out above), somehow save the information (e.g. in a ScriptableObject, text file, on some prefab etc) and then retrieve that information at runtime.

    Or figure out a way where you don't need this information in the first place.
     
  10. nicloay

    nicloay

    Joined:
    Jul 11, 2012
    Posts:
    453
    Hey.. thanks for this function.

    Still useful =)

    Here is autofix post processor
    Code (CSharp):
    1. public class AtlasTexturePostprocessor : AssetPostprocessor {
    2.     public const int PixelsPerUnit = 100;
    3.     void OnPreprocessTexture(){
    4.         Texture2D tex = AssetDatabase.LoadAssetAtPath(assetPath, typeof(Texture2D )) as Texture2D;
    5.         TextureImporter importer = assetImporter as TextureImporter;
    6.         int textureHeight;
    7.         FixTextureSize(tex,importer, out textureHeight);
    8.     }
    9.     static int[] textureSizes = new int[] {
    10.         32,
    11.         64,
    12.         128,
    13.         256,
    14.         512,
    15.         1024,
    16.         2048,
    17.         4096
    18.     };
    19.     void FixTextureSize (Texture2D tex, TextureImporter importer, out int textureRealHeigh)
    20.     {
    21.         int width, height, max;
    22.         GetImageSize(tex,out width, out height);
    23.         textureRealHeigh = height;
    24.         max = Mathf.Max(width, height);
    25.         int size = 1024; //Default size
    26.         for (int i = 0; i < textureSizes.Length; i++) {
    27.             if (textureSizes[i] >= max){
    28.                 size = textureSizes[i];
    29.                 break;          
    30.             }
    31.         }
    32.         importer.maxTextureSize = size;
    33.     }
    34. }

    One issue here, is that GetWidthAndHeight return zero when you use "Reimport All"
     
    Last edited: Feb 18, 2015
  11. Diablo404

    Diablo404

    Joined:
    Mar 15, 2013
    Posts:
    135
    Thoses scripts are great but I needed to do it in a standalone export, and since theses ones are using UnityEditor, it was non exploitable.

    So I found a solution that could maybe help someone. In this link: http://www.codeproject.com/Articles/35978/Reading-Image-Headers-to-Get-Width-and-Height , they read the binary header of the file to determine which type of file it is, then read the appropriate size. I've modified it a bit so I don't have to import the unwanted System.Drawing. Here is the modified (and only) script needed:

    ImageHeader.cs
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using System.IO;
    4. using System.Linq;
    5.  
    6.  
    7. public class Vector2Int
    8. {
    9.     public int x;
    10.     public int y;
    11.    
    12.     public Vector2Int( int _x, int _y )
    13.     {
    14.         x = _x;
    15.         y = _y;
    16.     }
    17. }
    18.  
    19. /// <summary>
    20. /// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349
    21. /// Minor improvements including supporting unsigned 16-bit integers when decoding Jfif and added logic
    22. /// to load the image using new Bitmap if reading the headers fails
    23. /// </summary>
    24. public static class ImageHeader
    25. {
    26.     const string errorMessage = "Could not recognise image format.";
    27.  
    28.     private static Dictionary<byte[], Func<BinaryReader, Vector2Int>> imageFormatDecoders = new Dictionary<byte[], Func<BinaryReader, Vector2Int>>()
    29.     {
    30.         { new byte[] { 0x42, 0x4D }, DecodeBitmap },
    31.         { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif },
    32.         { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif },
    33.         { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng },
    34.         { new byte[] { 0xff, 0xd8 }, DecodeJfif },
    35.     };
    36.  
    37.     /// <summary>      
    38.     /// Gets the dimensions of an image.      
    39.     /// </summary>      
    40.     /// <param name="path">The path of the image to get the dimensions of.</param>      
    41.     /// <returns>The dimensions of the specified image.</returns>      
    42.     /// <exception cref="ArgumentException">The image was of an unrecognised format.</exception>      
    43.     public static Vector2Int GetDimensions(string path)
    44.     {
    45.         try
    46.         {
    47.             using (BinaryReader binaryReader = new BinaryReader(File.OpenRead(path)))
    48.             {
    49.                 try
    50.                 {
    51.                     return GetDimensions(binaryReader);
    52.                 }
    53.                 catch (ArgumentException e)
    54.                 {
    55.                     string newMessage = string.Format("{0} file: '{1}' ", errorMessage, path);
    56.  
    57.                     throw new ArgumentException(newMessage, "path", e);
    58.                 }
    59.             }
    60.         }
    61.         catch (ArgumentException)
    62.         {
    63.             UnityEngine.Debug.LogError("And error occured");
    64.             return new Vector2Int(0,0);
    65.         }
    66.     }
    67.  
    68.     /// <summary>      
    69.     /// Gets the dimensions of an image.      
    70.     /// </summary>      
    71.     /// <param name="path">The path of the image to get the dimensions of.</param>      
    72.     /// <returns>The dimensions of the specified image.</returns>      
    73.     /// <exception cref="ArgumentException">The image was of an unrecognised format.</exception>          
    74.     public static Vector2Int GetDimensions(BinaryReader binaryReader)
    75.     {
    76.         int maxMagicBytesLength = imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length;
    77.         byte[] magicBytes = new byte[maxMagicBytesLength];
    78.         for (int i = 0; i < maxMagicBytesLength; i += 1)
    79.         {
    80.             magicBytes[i] = binaryReader.ReadByte();
    81.             foreach (var kvPair in imageFormatDecoders)
    82.             {
    83.                 if (StartsWith(magicBytes, kvPair.Key))
    84.                 {
    85.                     return kvPair.Value(binaryReader);
    86.                 }
    87.             }
    88.         }
    89.  
    90.         throw new ArgumentException(errorMessage, "binaryReader");
    91.     }
    92.  
    93.     private static bool StartsWith(byte[] thisBytes, byte[] thatBytes)
    94.     {
    95.         for (int i = 0; i < thatBytes.Length; i += 1)
    96.         {
    97.             if (thisBytes[i] != thatBytes[i])
    98.             {
    99.                 return false;
    100.             }
    101.         }
    102.  
    103.         return true;
    104.     }
    105.  
    106.     private static short ReadLittleEndianInt16(BinaryReader binaryReader)
    107.     {
    108.         byte[] bytes = new byte[sizeof(short)];
    109.  
    110.         for (int i = 0; i < sizeof(short); i += 1)
    111.         {
    112.             bytes[sizeof(short) - 1 - i] = binaryReader.ReadByte();
    113.         }
    114.         return BitConverter.ToInt16(bytes, 0);
    115.     }
    116.  
    117.     private static ushort ReadLittleEndianUInt16(BinaryReader binaryReader)
    118.     {
    119.         byte[] bytes = new byte[sizeof(ushort)];
    120.  
    121.         for (int i = 0; i < sizeof(ushort); i += 1)
    122.         {
    123.             bytes[sizeof(ushort) - 1 - i] = binaryReader.ReadByte();
    124.         }
    125.         return BitConverter.ToUInt16(bytes, 0);
    126.     }
    127.  
    128.     private static int ReadLittleEndianInt32(BinaryReader binaryReader)
    129.     {
    130.         byte[] bytes = new byte[sizeof(int)];
    131.         for (int i = 0; i < sizeof(int); i += 1)
    132.         {
    133.             bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte();
    134.         }
    135.         return BitConverter.ToInt32(bytes, 0);
    136.     }
    137.  
    138.     private static Vector2Int DecodeBitmap(BinaryReader binaryReader)
    139.     {
    140.         binaryReader.ReadBytes(16);
    141.         int width = binaryReader.ReadInt32();
    142.         int height = binaryReader.ReadInt32();
    143.         return new Vector2Int(width, height);
    144.     }
    145.  
    146.     private static Vector2Int DecodeGif(BinaryReader binaryReader)
    147.     {
    148.         int width = binaryReader.ReadInt16();
    149.         int height = binaryReader.ReadInt16();
    150.         return new Vector2Int(width, height);
    151.     }
    152.  
    153.     private static Vector2Int DecodePng(BinaryReader binaryReader)
    154.     {
    155.         binaryReader.ReadBytes(8);
    156.         int width = ReadLittleEndianInt32(binaryReader);
    157.         int height = ReadLittleEndianInt32(binaryReader);
    158.         return new Vector2Int(width, height);
    159.     }
    160.  
    161.     private static Vector2Int DecodeJfif(BinaryReader binaryReader)
    162.     {
    163.         while (binaryReader.ReadByte() == 0xff)
    164.         {
    165.             byte marker = binaryReader.ReadByte();
    166.             short chunkLength = ReadLittleEndianInt16(binaryReader);
    167.             if (marker == 0xc0)
    168.             {
    169.                 binaryReader.ReadByte();
    170.                 int height = ReadLittleEndianInt16(binaryReader);
    171.                 int width = ReadLittleEndianInt16(binaryReader);
    172.                 return new Vector2Int(width, height);
    173.             }
    174.  
    175.             if (chunkLength < 0)
    176.             {
    177.                 ushort uchunkLength = (ushort)chunkLength;
    178.                 binaryReader.ReadBytes(uchunkLength - 2);
    179.             }
    180.             else
    181.             {
    182.                 binaryReader.ReadBytes(chunkLength - 2);
    183.             }
    184.         }
    185.  
    186.         throw new ArgumentException(errorMessage);
    187.     }
    188. }
    189.  
    And here is how to use it:
    Code (CSharp):
    1. Vector2Int imgSize  = ImageHeader.GetDimensions(Application.persistentDataPath+"/Mouse.png");
    2.         Debug.Log("imgSize.x =" + imgSize.x);
    3. Debug.Log("imgSize.y =" + imgSize.y);
     
    rtr989, seltar_, Beloudest and 2 others like this.
  12. Beloudest

    Beloudest

    Joined:
    Mar 13, 2015
    Posts:
    206
    Looks good to me. Thx
     
    Diablo404 likes this.
  13. 670ok

    670ok

    Joined:
    Jun 6, 2017
    Posts:
    1
    This is very useful, thanks bro
     
    Diablo404 likes this.
  14. jtok4j

    jtok4j

    Joined:
    Dec 6, 2013
    Posts:
    280
    I'd like to pipe-up and confirm that the script and usage lines of code by Diablo404 are working for me as well.
    Thanks and...
    Keep on Creating!
     
    Diablo404 likes this.
  15. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    412
    Just a tip for using these methods:
    You will have to include both system and reflection libraries for them to compile.
    Add this lines at the top of your script:
    Code (CSharp):
    1. using System;
    2. using System.Reflection;
     
    Diablo404 likes this.
  16. MeessenPhilips

    MeessenPhilips

    Joined:
    Oct 3, 2016
    Posts:
    4
    Still useful 6 years later, thank you.
     
    jtok4j likes this.
  17. jtok4j

    jtok4j

    Joined:
    Dec 6, 2013
    Posts:
    280
    So, So true @MeessenPhilips