Question Most effective/fast way to get an image's resolution?

Strahan

Member
Joined
Mar 28, 2015
Messages
11
Programming Experience
3-5
Hello. I'm working on a wallpaper shuffling program. Everything is going swimmingly, but I ran into a snag. I added the ability when it shuffles to be able to limit the resolution, so it only picks files where the width or height meets or exceeds a set value. As soon as I added that, the whole thing blew up lol. Works fine for directories with only a few files, but it crashes "out of memory" on larger folders. I made a test console app to remove everything from the equation but just the simple act of getting res and I have the same problem.

C#:
static void Main(string[] args) {
  foreach (string file in Directory.GetFiles(Environment.CurrentDirectory)) {
    using (Image img = Image.FromFile(file)) {
      Console.WriteLine("[" + img.Width + "][" + img.Height + "]: " + file);
    }
  }
}

Results in:

C#:
(..206 prior..)
[1920][1080]: F:\D\Media\Pics\Wallpaper\e7519050a71166d9d24c2a7989afcb40.png
[1920][1080]: F:\D\Media\Pics\Wallpaper\ea21e39bc7982a334dea213d2d8d9a6e.png
[1920][1080]: F:\D\Media\Pics\Wallpaper\ebaa92965dfaf6d424a143630024b5f1.png

Unhandled Exception: OutOfMemoryException.

I was getting annoyed, so I tried avoiding the problem by shelling out to MediaInfo. To process the folder still takes forever; 38 seconds. Interestingly, I made a PHP script to dump resolutions and that took only a little over one second...?? I don't really think though bundling PHP with my application is the ideal solution, lol.

Does anyone know a way to parse a lot of files' resolutions in a similarly fast manner in C# directly?

EDIT: I found this interesting. I was thinking, can't I just read the values right from the file? I assume they are in the header of the file. I used a PNG to test, and sure enough, the resolutions are right there as hex values. So I did:

C#:
static void Main(string[] args) {
  foreach (string filePath in Directory.GetFiles(Environment.CurrentDirectory)) {
    try {
      using (FileStream fileStream = File.OpenRead(filePath)) {
        if (fileStream.Length < 24) continue;

        byte[] buffer = new byte[24];
        int bytesRead = fileStream.Read(buffer, 0, buffer.Length);
        int width = Convert.ToInt32(buffer[18].ToString("X") + buffer[19].ToString("X"), 16);
        int height = Convert.ToInt32(buffer[22].ToString("X") + buffer[23].ToString("X"), 16);
        Console.WriteLine("[" + width + "][" + height + "]: " + Path.GetFileName(filePath));
      }
    } catch (IOException ex) {
      Console.WriteLine("An error occurred while reading the file: {0}", ex.Message);
    }
  }
}

I ran it against my folder; it only took 2.72 seconds :) Problem is, I don't know if this is reliable. Is it always the same offset? I also need to find it for JPEG as well then. I guess I should Google and look for the header specification for the two formats. Is this a good way to go though?
 
Last edited:
Although most implementations would likely save the IHDR chunk right after the file signature, there is guarantee that every implementation will do so. You have to read the file format specification for the files that you are read. For PNG files, read about it here:

Also, since you only deal with one file at a time, use Directory.EnumerateFiles() instead of Directory.GetFiles(). The former will return you the files one at a time. The latter will return you all the files in one big array.
 
Although most implementations would likely save the IHDR chunk right after the file signature, there is guarantee that every implementation will do so. You have to read the file format specification for the files that you are read. For PNG files, read about it here:

Also, since you only deal with one file at a time, use Directory.EnumerateFiles() instead of Directory.GetFiles(). The former will return you the files one at a time. The latter will return you all the files in one big array.
Thanks, I'll check that out and switch to enumeration. That's handy to know.

I also forgot to mention. From the documentation for Image.FromFile():
Ahh, that explains that. After updating to catch that, the process completes successfully. However, doing

C#:
foreach (string file in Directory.EnumerateFiles(Environment.CurrentDirectory)) {
  try {
    using (Image img = Image.FromFile(file)) {
      Console.WriteLine("[" + img.Width + "][" + img.Height + "]: " + file);
    }
  } catch {
    Console.WriteLine("Skipping " + file + ", bad format");
  }
}

Takes 16.13 seconds to complete the scan. That's still crazy long compared to the ~2 seconds to just read the bytes. I looked into the specifications; I'm just a noob it gives me a headache lol. I don't know if I can make reliable code to accomplish that, jpeg's looks pretty complex too. Out of curiosity, am I doing it correctly as far as using more "native" approaches? Is using Image and then the Width/Height the right way to get image specs in C# or is there a better way? Rather, a better way that doesn't require a bunch of complex file analysis heh.

EDIT: It just occurred to me, I should see if GPT-4 has any ideas. It gave me some code so I stuck them all together:

C#:
static void Main(string[] args) {
  string mode = args[0];

  DateTime started = DateTime.Now;
  switch (mode.ToLower()) {
  case "using":
    foreach (string file in Directory.EnumerateFiles(Environment.CurrentDirectory)) {
      try {
        using (Image img = Image.FromFile(file)) {
          Console.WriteLine("[" + img.Width + "][" + img.Height + "]: " + file);
        }
      } catch {
        Console.WriteLine("Skipping " + file + ", bad format");
      }
    }
    break;

  case "header":
    foreach (string filePath in Directory.EnumerateFiles(Environment.CurrentDirectory)) {

      try {
        using (FileStream fileStream = File.OpenRead(filePath)) {
          if (fileStream.Length < 24) continue;

          byte[] buffer = new byte[24];
          int bytesRead = fileStream.Read(buffer, 0, buffer.Length);
          int width = Convert.ToInt32(buffer[18].ToString("X") + buffer[19].ToString("X"), 16);
          int height = Convert.ToInt32(buffer[22].ToString("X") + buffer[23].ToString("X"), 16);
          Console.WriteLine("[" + width + "][" + height + "]: " + Path.GetFileName(filePath));
        }
      } catch (IOException ex) {
        Console.WriteLine("An error occurred while reading the file: {0}", ex.Message);
      }
    }
    break;

  case "gpt":
    foreach (string file in Directory.EnumerateFiles(Environment.CurrentDirectory)) {
      try {
        var res = GetImageResolution(file);
        Console.WriteLine("[" + res.Item1 + "][" + res.Item2 + "]: " + Path.GetFileName(file));
      } catch (Exception ex) {
        Console.WriteLine("[" + ex.Message + "]: " + Path.GetFileName(file));
      }
    }
    break;
  }

  TimeSpan ts = DateTime.Now - started;
  Console.WriteLine("This took " + ts.TotalSeconds + " second(s), or " + ts.TotalMilliseconds + " ms");
}

public static (int, int) GetImageResolution(string filePath) {
  using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) {
    var binaryReader = new BinaryReader(fileStream);

    // Read file header (the first couple of bytes) to determine the image type
    byte[] header = binaryReader.ReadBytes(10);

    if (header[0] == 'B' && header[1] == 'M') // BMP
    {
      binaryReader.BaseStream.Seek(18, SeekOrigin.Begin);
      int width = binaryReader.ReadInt32();
      int height = binaryReader.ReadInt32();
      return (width, height);
    } else if (header[1] == 'P' && header[2] == 'N' && header[3] == 'G') // PNG
      {
      binaryReader.BaseStream.Seek(16, SeekOrigin.Begin);
      int width = binaryReader.ReadInt32();
      int height = binaryReader.ReadInt32();
      return (System.Net.IPAddress.NetworkToHostOrder(width), System.Net.IPAddress.NetworkToHostOrder(height));
    } else if (header[6] == 'J' && header[7] == 'F' && header[8] == 'I' && header[9] == 'F') // JPG
      {
      while (binaryReader.ReadByte() == 0xFF) {
        byte marker = binaryReader.ReadByte();
        short chunkLength = binaryReader.ReadInt16();

        if (marker == 0xC0 || marker == 0xC1 || marker == 0xC2 || marker == 0xC3) {
          binaryReader.ReadByte();
          int height = binaryReader.ReadInt16();
          int width = binaryReader.ReadInt16();
          return (System.Net.IPAddress.NetworkToHostOrder(width), System.Net.IPAddress.NetworkToHostOrder(height));
        }

        binaryReader.BaseStream.Seek(chunkLength - 2, SeekOrigin.Current);
      }
    }

    throw new ArgumentException("File format not supported.", "filePath");
  }
}

Doing "imageinfo using" in my folder got results in 13.68 seconds, "imageinfo header" did it in 1.67 seconds and "imageinfo gpt" did it in 1.38 seconds. Nifty :)
 
Last edited:
Takes 16.13 seconds to complete the scan

That's because it is reading in the entire file, and allocating all the necessary data structures, and decompressing all the image pixels for each file.
 
If this were my problem to solve, I'd probably hand it off to ImageMagick or something, and also store the dimensions plus file write time in a database, so I only botehred scanning files that had changed between successive runs
 
It is also possible to get extended properties from Windows Shell, the ones you see in Properties window in File Explorer.
Add COM reference to "Microsoft Shell Controls And Automation".
C#:
//namespace import: using Shell32;

var shell = new Shell();
var folder = shell.NameSpace(@"D:\path");

foreach (FolderItem item in folder.Items())
{
    if (!item.IsFolder)
    {
        var ext = Path.GetExtension(item.Name).ToLower();
        if (ext == ".png" || ext == ".jpg")
        {
            var w = ((FolderItem2)item).ExtendedProperty("System.Image.HorizontalSize");
            var h = ((FolderItem2)item).ExtendedProperty("System.Image.VerticalSize");
            Console.WriteLine($"{item.Name} : {w} x {h}");
        }
    }
}

Marshal.ReleaseComObject(shell);
This requires STA thread (console app is MTA by default), if not you get cast exception E_NOINTERFACE when creating the Shell object. Either add [STAThread] to Main or start a new Thread and .SetApartmentState(ApartmentState.STA) to it.
 
Thanks all. I realized after I made the update to my post that the scan I did with GPT's code had no JPG files. I scanned JPG, and every single one reports "file format not supported". (sigh) After some back and forth, it got me code that works but not reliably. I scanned 297 JPG files, and only about 60-70% of them returned resolutions. That's too much failure to be acceptable IMO.

That's because it is reading in the entire file, and allocating all the necessary data structures, and decompressing all the image pixels for each file.
Ahh. It'd be nice if you could tell C# just to read the header for speed's sake.


If this were my problem to solve, I'd probably hand it off to ImageMagick or something, and also store the dimensions plus file write time in a database, so I only botehred scanning files that had changed between successive runs
Yea, I think that's the way I'll have to go. I just dislike having to use a library of media; I like being able to drop a file right into the filesystem and know it's automatically in the mix. I decided to do a hybrid; it will use straight filesystem by default, but if the user indicates they want to filter their content by resolution, it pops up saying "You need to use the MySQL media library to allow for this functionality with appropriate performance. Scan your paper path to populate DB now?"


It is also possible to get extended properties from Windows Shell, the ones you see in Properties window in File Explorer.
Add COM reference to "Microsoft Shell Controls And Automation".
C#:
//namespace import: using Shell32;

var shell = new Shell();
var folder = shell.NameSpace(@"D:\path");

foreach (FolderItem item in folder.Items())
{
    if (!item.IsFolder)
    {
        var ext = Path.GetExtension(item.Name).ToLower();
        if (ext == ".png" || ext == ".jpg")
        {
            var w = ((FolderItem2)item).ExtendedProperty("System.Image.HorizontalSize");
            var h = ((FolderItem2)item).ExtendedProperty("System.Image.VerticalSize");
            Console.WriteLine($"{item.Name} : {w} x {h}");
        }
    }
}

Marshal.ReleaseComObject(shell);
This requires STA thread (console app is MTA by default), if not you get cast exception E_NOINTERFACE when creating the Shell object. Either add [STAThread] to Main or start a new Thread and .SetApartmentState(ApartmentState.STA) to it.

Interesting, I'll give that a whirl too.
 
it pops up saying "You need to use the MySQL media library to allow for this functionality with appropriate performance. Scan your paper path to populate DB now?"

I think I'd have asked "to filter by resolution we'll need to perform a scan of media files. This can take a while. Scan now?"

They don't need to know you're using MySQL (and perhaps SQLite would be a better choice)
 
True. Most likely the only people who'll ever use this will be me and my sister, lol, but I like to design things to be distributable just in case so I shouldn't always think in the context of my own head :)

Thanks
 

Latest posts

Back
Top Bottom