2

Objective

I want to "correct" the orientation of an image. My plan is to extract the orientation from the Exif data stored with the image and use that to inform a re-orientation of the image.

Problem

It's probably me, but for this particular Exif property, the GetDescription (or GetString) simply returns null; all other properties that I have tried (x15) return a value. In the sample code below (a Console App), "Approach 1" uses the preferred and efficient GetDescription approach to grab the image Orientation, while "Approach 2" uses an inefficient foreach loop to iterate through directories and tags searching for the Orientation.

using MetadataExtractor;
using MetadataExtractor.Formats.Exif;

string Filename = @"D:\Users\Simon\OneDrive\My Stuff\My Source\TestFuelPriceTracker\Originals\IMG_8490.jpg";

IEnumerable<MetadataExtractor.Directory> directories = ImageMetadataReader.ReadMetadata(Filename);

// Approach 1
var subIfdDirectory = directories.OfType<ExifSubIfdDirectory>().FirstOrDefault();
var Orientation1 = subIfdDirectory?.GetDescription(ExifDirectoryBase.TagOrientation);
Console.WriteLine($"Approach 1: Orientation = \"{Orientation1}\"");

// Approach 2
foreach (var directory in directories)
{
    foreach (var tag in directory.Tags)
    {
        switch (tag.Name)
        {
            case "Orientation":
                Console.WriteLine($"Approach 2: Orientation = \"{tag.Description}\"");
                break;
        }
    }
}

// Approach 3
var Orientation2 = directories
    .OfType<ExifSubIfdDirectory>()
    .FirstOrDefault(s => string.Equals(s?.GetDescription(ExifDirectoryBase.TagOrientation), "Orientation", StringComparison.OrdinalIgnoreCase));
Console.WriteLine($"Approach 3: Orientation = \"{Orientation2}\"");

When run, I get the following results...

Approach 1: Orientation = ""
Approach 2: Orientation = "Top, left side (Horizontal / normal)"
Approach 3: Orientation = ""

Approach 2 shows that the Orientation information is actually present in the image. Please note that I have tried numerous images and I get the same problem. Not sure if this is relevant, but all images were taken on an iPhone 12.

Approach 3 added based on a suggestion by @aybe.

Environment

I am using Visual Studio 2022 on a Windows 11 Professional machine, all software patched to latest versions. The framework is .NET 6. MetadataExtractor version 2.7.2.

Mashed Spud
  • 452
  • 4
  • 12
  • I'd use something like that or so: `directories.OfType().First(s=> string.Equals(s?.GetDescription(...), "Whatever", OrdinalIgnoreCase)` – aybe Mar 26 '22 at 13:53
  • @aybe Not sure that will fly because of the nested list of tags within each directory. I plugged it into the sample and it comes up with ```Unhandled exception. System.InvalidOperationException: Sequence contains no matching element```. Nice try though. – Mashed Spud Mar 26 '22 at 14:05
  • Sorry I meant `FirstOrDefault`. However, there's nothing wrong with approach #2 unless of course you realize it takes much longer than #1. – aybe Mar 26 '22 at 14:06
  • @aybe That makes sense! :-) Unfortunately the result is the same as Approach 1. I have edited the original post and added your idea in as "Approach 3". – Mashed Spud Mar 26 '22 at 14:10
  • I've done a quick test and I vaguely remind of one thing, this information may not be present, guess what, on all the files I've tried, neither had it! – aybe Mar 26 '22 at 14:24
  • @aybe I thought that to start with, but then that does not explain why when I iterate through the Exif properties the orientation is available (as in Approach 2). I have numerous images with a number of them whose orientation needs adjusting. Using the ```foreach``` approach I can see the orientation information and it seems to correlate to each image's orientation. It's just the ```GetDescription``` approach that returns null. And it seems only for this property. – Mashed Spud Mar 26 '22 at 14:33
  • I guess I'm starting to understand, some of the images may have it in IPTC rather than EXIF, hence why it's safer to iterate through all of them. – aybe Mar 26 '22 at 14:39
  • 1
    I have found the error to your problem, it's deceptively simple, string equals overload is object and tag orientation is an int, thus, it'll never match "Orientation" – aybe Mar 26 '22 at 14:47
  • [Property item descriptions](https://learn.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-constant-property-item-descriptions) => `PropertyTagOrientation`, `PropertyTagThumbnailOrientation` => `ValueType: Short` – Jimi Mar 26 '22 at 14:54
  • @Jimi This is interesting but there's no GDI for .NET 6 as far as I can tell – aybe Mar 26 '22 at 15:03
  • 1
    It doesn't matter, those are standard (international) values. The section of the Orientation Tag is not JPEG or SUB IFD, it's EXIF (IFD0). I.e., `.OfType().FirstOrDefault()` – Jimi Mar 26 '22 at 15:11
  • @Jimi Not sure what you mean, nobody is talking about JPEG but only EXIF. – aybe Mar 26 '22 at 15:15
  • @aybe Did I say anything different? It's about the section that contains the information. It's `... = directories.OfType().FirstOrDefault()?.GetDescription(ExifDirectoryBase.TagOrientation);` -- So, `ExifIfd0Directory`, not `ExifSubIfdDirectory`. – Jimi Mar 26 '22 at 15:19
  • Oh yes, you're absolutely right, I realized that with the sample I've posted. – aybe Mar 26 '22 at 15:23
  • @aybe You could ignore the section Type entirely and write, e.g., `var orientation = directories.FirstOrDefault(dir => dir.ContainsTag(ExifDirectoryBase.TagOrientation))?.GetDescription(ExifDirectoryBase.TagOrientation);` – Jimi Mar 26 '22 at 16:20
  • I've already deleted the project. – aybe Mar 26 '22 at 16:55
  • @aybe @jimi in your comments above I believe you have hit the problem I was facing... (1) @aybe pointing out that Orientation is in fact an ```int```, and (2) @jimi pointing out that the Exif directory is in fact "EXIF IFD0" and not "EXIF SubIFD" as I had in my sample code. Good spots! – Mashed Spud Mar 26 '22 at 20:10
  • Based on the above two points... I can first look in the correct directory (doh!)... EXIF D0 and pull back an integer value using something like ```Orientation = ifd0Directory.TryGetInt32(ExifDirectoryBase.TagOrientation, out int value) ? value : -1;``` – Mashed Spud Mar 26 '22 at 20:12

2 Answers2

2

Proposed final solution is...

IEnumerable<MetadataExtractor.Directory> directories = ImageMetadataReader.ReadMetadata(Filename);
var ifd0Directory = directories.OfType<ExifIfd0Directory>().FirstOrDefault();
int Orientation = -1;
if (ifd0Directory != null) {
    Orientation = ifd0Directory.TryGetInt32(ExifDirectoryBase.TagOrientation, out int value) ? value : -1;
}

If successful it returns an integer value from 1 to 8 inclusive, or -1 if there is a problem. For anyone whose interested, the meaning of these numbers is discussed well in the following article...

JPEG Image Orientation and Exif

I would also point you at the following MIT paper which has captured the data types for each of the Exif tags; along with a lot of other good information too...

Description of Exif file format

Mashed Spud
  • 452
  • 4
  • 12
  • Note that `ifd0Directory` can be null. You're not handling that. -- The range of values of the Orientation Tag is `(1;8)`. You can also see it [in the MSDN Docs](https://learn.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-constant-property-item-descriptions#propertytagorientation) I linked before (and rectangles have 4 sides :) – Jimi Mar 27 '22 at 00:07
0

Your code would never work because it was comparing the integer value of the tag, that would of course never match "Orientation".

This works but with how this might fail as some images don't necessarily have the tags as I said, it might be worth looking at computing the orientation either from other tags or from image dimensions as a fallback solution.

private static void GetOrientation(Stream stream)
{
    var directories = ImageMetadataReader.ReadMetadata(stream);

    if (TryGetOrientationTag(directories, out var result))
    {
    }
    else
    {
        throw new NotImplementedException("Compute orientation from image size instead");
    }
}

private static bool TryGetOrientationTag(IEnumerable<Directory> directories, out string result)
{
    result = null!;

    foreach (var directory in directories)
    {
        switch (directory)
        {
            case ExifDirectoryBase e:
                foreach (var tag in e.Tags)
                {
                    if (tag.Type is not ExifDirectoryBase.TagOrientation || tag.Description is null)
                        continue;

                    result = tag.Description;
                    return true;
                }

                break;
            case IptcDirectory i:
                foreach (var tag in i.Tags)
                {
                    if (tag.Type is not IptcDirectory.TagImageOrientation || tag.Description is null)
                        continue;

                    result = tag.Description;
                    return true;
                }

                break;
        }
    }

    return false;
}
aybe
  • 15,516
  • 9
  • 57
  • 105
  • Because this suggested solution is iterating through the directories looking for the right tag, I'm not sure it's the best approach on the grounds that it's inefficient from a CPU cycle perspective. In the grand scheme of things it probably wouldn't make a big deal to my solution, but nonetheless I think a better approach is to use the Metadata Extractor ```Get...``` or ```TryGet...``` methods. Having said that, I wouldn't have got to that conclusion without your and @Jimi comments, so big thanks. – Mashed Spud Mar 26 '22 at 20:17