7

I'm looking for the fastest way to create scaled down bitmap that honors EXIF orientation tag

Ref :https://weblogs.asp.net/bleroy/the-fastest-way-to-resize-images-from-asp-net-and-it-s-more-supported-ish

Currently i use the following code to create a Bitmap that honors EXIF Orientation tag

  static Bitmap FixImageOrientation(Bitmap srce)
        {
            const int ExifOrientationId = 0x112;
            // Read orientation tag
            if (!srce.PropertyIdList.Contains(ExifOrientationId)) return srce;
            var prop = srce.GetPropertyItem(ExifOrientationId);
            var orient = BitConverter.ToInt16(prop.Value, 0);
            // Force value to 1
            prop.Value = BitConverter.GetBytes((short)1);
            srce.SetPropertyItem(prop);

            // Rotate/flip image according to <orient>
            switch (orient)
            {
                case 1:
                    srce.RotateFlip(RotateFlipType.RotateNoneFlipNone);
                    return srce;


                case 2:
                    srce.RotateFlip(RotateFlipType.RotateNoneFlipX);
                    return srce;

                case 3:
                    srce.RotateFlip(RotateFlipType.Rotate180FlipNone);
                    return srce;

                case 4:
                    srce.RotateFlip(RotateFlipType.Rotate180FlipX);
                    return srce;

                case 5:
                    srce.RotateFlip(RotateFlipType.Rotate90FlipX);
                    return srce;

                case 6:
                    srce.RotateFlip(RotateFlipType.Rotate90FlipNone);
                    return srce;

                case 7:
                    srce.RotateFlip(RotateFlipType.Rotate270FlipX);
                    return srce;

                case 8:
                    srce.RotateFlip(RotateFlipType.Rotate270FlipNone);
                    return srce;

                default:
                    srce.RotateFlip(RotateFlipType.RotateNoneFlipNone);
                    return srce;
            }
        }

I'm first creating a orientation fixed image ,then resizing it (preserving aspect ratio) for fast processing.

  public static Bitmap UpdatedResizeImage(Bitmap source, Size size)
        {
            var scale = Math.Min(size.Width / (double)source.Width, size.Height / (double)source.Height);
            var bmp = new Bitmap((int)(source.Width * scale), (int)(source.Height * scale));

            using (var graph = Graphics.FromImage(bmp))
            {
                graph.InterpolationMode = InterpolationMode.High;
                graph.CompositingQuality = CompositingQuality.HighQuality;
                graph.SmoothingMode = SmoothingMode.AntiAlias;
                graph.DrawImage(source, 0, 0, bmp.Width, bmp.Height);
            }
            return bmp;
        }

Now WIC allows much faster image manipulation.Ref:https://stackoverflow.com/a/57987315/848968

How can i create a Scaled Down BitmapImage that honours the EXIF tag

Update:

if ((bitmapMetadata != null) && (bitmapMetadata.ContainsQuery("System.Photo.Orientation")))
            {
                object o = bitmapMetadata.GetQuery("System.Photo.Orientation");

                if (o != null)
                {
                    switch ((ushort)o)
                    {
                        case 3:
                            rotatedImage = new TransformedBitmap(resized, new RotateTransform(180));
                            break;
                        case 6:
                            rotatedImage = new TransformedBitmap(resized, new RotateTransform(90));
                            break;
                        case 8:
                            rotatedImage = new TransformedBitmap(resized, new RotateTransform(270));
                            break;

                    }

                }
            }
techno
  • 6,100
  • 16
  • 86
  • 192
  • 1
    See this: https://stackoverflow.com/questions/688990/reading-metadata-from-images-in-wpf and this https://stackoverflow.com/questions/33914140/wpf-how-to-rotate-a-bitmapsource-by-any-angle – Simon Mourier Sep 24 '19 at 14:13
  • 1
    The problem you face are actually three problems: reading metadata in WPF, rotating in WPF, and scaling in WPF. If you just look for these three independently you should get results quite easily. – Nyerguds Sep 27 '19 at 07:12
  • @SimonMourier The code you refereed to uses `Canvas` class in `System.Windows.Controls` to perform the rotation.Is this necessary? – techno Oct 03 '19 at 08:35
  • No, just use Metadata from first link and TransformedBitmap from second. You can also save Metadata. See here for another example: https://www.markbetz.net/2011/05/31/creating-image-thumbs-with-transformedbitmap-and-scaletransform/ – Simon Mourier Oct 03 '19 at 16:13
  • @SimonMourier Thanks... The example uses `Encoder` to save image to disk.I don't need to save the image to disk,i need to get the property oriented image,if its huge,resize it using your old code and then convert it to a `System.Drawing.Bitmap` for further processing. I hope this won't cause performance deterioration. I need for this approach is to speed up processing. – techno Oct 04 '19 at 04:59
  • Encoder saves to disk but you can save to stream I suppose. You shouldn't convert from WPF to GDI+ (Bitmap). This is inefficient (unless it proves to be good enough in your context) – Simon Mourier Oct 04 '19 at 06:37
  • @SimonMourier I need GDI+ as the entire code is based on GDI+ – techno Oct 04 '19 at 06:39
  • @SimonMourier I'm trying to wire up the things .. please see https://www.paste.org/100655 .Do i need a separate `BitmapDecoder` ? As your code example uses it.. is there a way to create the `TransformedBitmap` from `BitmapFrame` – techno Oct 04 '19 at 06:55
  • Your code seems ok, answer yourself if you're happy – Simon Mourier Oct 08 '19 at 13:31
  • @SimonMourier I have note tested the code. – techno Oct 08 '19 at 17:29
  • @SimonMourier I'm still stuck with this issue.Could you please answer it... based on my code snippet.I'm not confident enough to use the same. – techno Feb 05 '20 at 08:04
  • Do you have sample image(s)? – Simon Mourier Feb 06 '20 at 17:38
  • @SimonMourier https://drive.google.com/file/d/1ktUJgP7p8ywVjGsi_VmqqzXsKSZAm62Y/view?usp=sharing – techno Feb 06 '20 at 18:00
  • @SimonMourier I have managed to implement the code for RotatedImage.But i'm confused when there is flipping involved.It seems i need to use `ScaledTransform` to achieve flipping.But Scaled Transform cannot be directly applied to `BitmapFrame` – techno Feb 06 '20 at 18:03
  • @SimonMourier Please see my update and this question https://stackoverflow.com/questions/60087992/system-drawing-rotateflip-equivalent-in-wic-bitmapframe – techno Feb 06 '20 at 18:05
  • You can pass a unique Transform to TransformedBitmap which would be a TransformGroup with all the transforms (Rotate, Scale, etc.). For flipping: https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/how-to-flip-a-uielement-horizontally-or-vertically – Simon Mourier Feb 06 '20 at 18:10
  • @SimonMourier Can you provide an example on creating a TransformGroup with Scale and Rotate. – techno Feb 06 '20 at 18:19
  • @techno Create a TransformGroup, add a RotateTransform and a ScaleTransform to its Children collection, then pass it to the TransformedBitmap constructor. – Clemens Feb 07 '20 at 10:50

1 Answers1

1

Here is some sample code that saves a thumbnail while preserving the image orientation, based on WPF classes (and a small WIC interop fun to determine the proper encoder for a given file extension, but this is optional):

  static void Main()
  {
      SaveThumbnail("new.jpg", 64); // auto jpg
      SaveThumbnail("new.jpg", 64, "new.png"); // explicit png output
  }

  public static void SaveThumbnail(string inputFilePath, int thumbnailSize, string outputFilePath = null)
  {
      if (inputFilePath == null)
          throw new ArgumentNullException(inputFilePath);

      // decode frame
      var frame = BitmapDecoder.Create(new Uri(inputFilePath, UriKind.RelativeOrAbsolute), BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None).Frames[0];

      // read input transformations
      var transformations = new Transformations(frame.Metadata as BitmapMetadata);

      int width;
      int height;
      if (frame.Width > frame.Height)
      {
          width = thumbnailSize;
          height = (int)(frame.Height * thumbnailSize / frame.Width);
      }
      else
      {
          width = (int)(frame.Width * thumbnailSize / frame.Height);
          height = thumbnailSize;
      }

      Guid containerFormat;
      if (outputFilePath == null)
      {
          // use input format same as decode.
          containerFormat = frame.Decoder.CodecInfo.ContainerFormat;
          outputFilePath = Path.ChangeExtension(inputFilePath, thumbnailSize + Path.GetExtension(inputFilePath));
      }
      else
      {
          // icing on the cake..., we determine the format from the output file extension, using some WIC voodoo (code below)
          // you could make it simpler and harcode things out but this way you can use other 3rd parties codecs
          containerFormat = WicUtilities.EnumerateDecoderFormatsForExtension(Path.GetExtension(outputFilePath)).FirstOrDefault();
          if (containerFormat == Guid.Empty) // this extension is not supported on this system
              throw new ArgumentNullException(outputFilePath);
      }

      var encoder = BitmapEncoder.Create(containerFormat);
      Transform transform = new ScaleTransform(width / frame.Width * 96 / frame.DpiX, height / frame.Height * 96 / frame.DpiY, 0, 0);

      // the jpeg encoder has a built-in flip & rotate system
      if (encoder is JpegBitmapEncoder jpeg)
      {
          // exif is counter clockwise
          switch (transformations.Rotation)
          {
              case Rotation.Rotate90:
                  jpeg.Rotation = Rotation.Rotate270;
                  break;

              case Rotation.Rotate180:
                  jpeg.Rotation = Rotation.Rotate180;
                  break;

              case Rotation.Rotate270:
                  jpeg.Rotation = Rotation.Rotate90;
                  break;
          }

          jpeg.FlipVertical = transformations.FlipVertical;
          jpeg.FlipHorizontal = transformations.FlipHorizontal;

          // option: change quality level here
          // jpeg.QualityLevel = xx
      }
      else
      {
          // other codecs need transform
          var group = new TransformGroup();

          // we must flip before rotate
          // https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/how-to-flip-a-uielement-horizontally-or-vertically
          if (transformations.FlipHorizontal)
          {
              group.Children.Add(new ScaleTransform(-1, 1, 0.5, 0.5));
          }

          if (transformations.FlipVertical)
          {
              group.Children.Add(new ScaleTransform(1, -1, 0.5, 0.5));
          }

          // exif is counter clockwise
          switch (transformations.Rotation)
          {
              case Rotation.Rotate90:
                  group.Children.Add(new RotateTransform(270));
                  break;

              case Rotation.Rotate180:
                  group.Children.Add(new RotateTransform(180));
                  break;

              case Rotation.Rotate270:
                  group.Children.Add(new RotateTransform(90));
                  break;
          }

          // I scale *after* rotate/flip, but it's up to you, not sure it changes anything in perf or quality...
          group.Children.Add(transform);
          transform = group;
      }

      var resized = BitmapFrame.Create(new TransformedBitmap(frame, transform));
      encoder.Frames.Add(resized);
      using (var stream = File.OpenWrite(outputFilePath))
      {
          encoder.Save(stream);
      }
  }

// helper class that exposes supported transformations (rotate/flip)
public class Transformations
{
    public Transformations(BitmapMetadata md)
    {
        // https://learn.microsoft.com/en-us/uwp/api/windows.storage.fileproperties.photoorientation
        // https://learn.microsoft.com/en-us/windows/win32/wic/-wic-photoprop-system-photo-orientation
        // https://learn.microsoft.com/en-us/windows/win32/properties/props-system-photo-orientation
        const string orientationProperty = "System.Photo.Orientation";
        if (md != null && md.ContainsQuery(orientationProperty))
        {
            var orientation = (Orientation)md.GetQuery(orientationProperty);
            switch (orientation)
            {
                case Orientation.FlipHorizontal:
                    FlipHorizontal = true;
                    break;

                case Orientation.FlipVertical:
                    FlipVertical = true;
                    break;

                case Orientation.Rotate90:
                    Rotation = Rotation.Rotate90;
                    break;

                case Orientation.Rotate180:
                    Rotation = Rotation.Rotate180;
                    break;

                case Orientation.Rotate270:
                    Rotation = Rotation.Rotate270;
                    break;

                case Orientation.Transpose:
                    Rotation = Rotation.Rotate90;
                    FlipHorizontal = true;
                    break;

                case Orientation.Transverse:
                    Rotation = Rotation.Rotate270;
                    FlipHorizontal = true;
                    break;
            }
        }
    }

    public Rotation Rotation { get; set; }
    public bool FlipHorizontal { get; set; }
    public bool FlipVertical { get; set; }
}

public enum Orientation : ushort
{
    Undefined,
    Normal,
    FlipHorizontal,
    Rotate180,
    FlipVertical,
    Transpose,
    Rotate270,
    Transverse,
    Rotate90
}

// some WIC tool, need System.Runtime.InteropServices namespace
public static class WicUtilities
{
    public static IEnumerable<Guid> EnumerateEncoderFormatsForExtension(string extension) => EnumerateFormatsForExtension(WICComponentType.WICEncoder, extension);
    public static IEnumerable<Guid> EnumerateDecoderFormatsForExtension(string extension) => EnumerateFormatsForExtension(WICComponentType.WICDecoder, extension);

    private static IEnumerable<Guid> EnumerateFormatsForExtension(WICComponentType type, string extension)
    {
        if (extension == null)
            throw new ArgumentNullException(nameof(extension));

        foreach (var info in EnumerateCodecs(type))
        {
            info.GetFileExtensions(0, null, out var len);
            if (len >= 0)
            {
                var sb = new StringBuilder(len);
                info.GetFileExtensions(len + 1, sb, out _);
                var supportedExtensions = sb.ToString().Split(',');
                if (supportedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
                {
                    if (info.GetContainerFormat(out var format) == 0)
                        yield return format;
                }
            }
        }
    }

    private static IEnumerable<IWICBitmapCodecInfo> EnumerateCodecs(WICComponentType type)
    {
        var wfac = (IWICImagingFactory)new WICImagingFactory();
        wfac.CreateComponentEnumerator(type, 0, out var unks);
        if (unks != null)
        {
            var array = new object[1];
            do
            {
                if (unks.Next(1, array, out var _) != 0)
                    break;

                yield return (IWICBitmapCodecInfo)array[0];
            }
            while (true);
        }
    }

    [Guid("CACAF262-9370-4615-A13B-9F5539DA4C0A"), ComImport]
    private class WICImagingFactory { }

    [Guid("00000100-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IEnumUnknown
    {
        [PreserveSig]
        int Next(int celt, [Out, MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.IUnknown)] object[] rgelt, out int celtFetched);

        // we don't need the rest
    }

    [Flags]
    private enum WICComponentType
    {
        WICDecoder = 0x1,
        WICEncoder = 0x2,
        // we don't need the rest
    }

    [Guid("ec5ec8a9-c395-4314-9c77-54d7a935ff70"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IWICImagingFactory
    {
        void _VtblGap1_20(); // skip 20 methods we don't need

        [PreserveSig]
        int CreateComponentEnumerator(WICComponentType componentTypes, int options, out IEnumUnknown ppIEnumUnknown);

        // we don't need the rest
    }

    [Guid("E87A44C4-B76E-4c47-8B09-298EB12A2714"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IWICBitmapCodecInfo
    {
        void _VtblGap1_8(); // skip 8 methods we don't need

        [PreserveSig]
        int GetContainerFormat(out Guid pguidContainerFormat);

        void _VtblGap2_5(); // skip 5 methods we don't need

        [PreserveSig]
        int GetFileExtensions(int cchFileExtensions, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder wzFileExtensions, out int pcchActual);

        // we don't need the rest
    }
}
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • I have implemented this .. https://www.paste.org/102872 but it was producing single lined image when flipping was involved.From your answer i get that ScaleTransform requires 4 Values.Please take a look.. Also when is Vertical flipping involved? – techno Feb 07 '20 at 17:05
  • Your code has some issues.The generated orientations are not correct for non JPEG encoder .. I hardcoded PNG Encoder.Please download this zip file,It contains sample images with orientation tags,exiftool and the batch script used for generating those images using exiftool.Please compare windows explorer preview with your output https://drive.google.com/file/d/1aEMWmMgqBPgzHB5YsGA0dC93Q8FqzMb0/view?usp=sharing – techno Feb 07 '20 at 17:32
  • @techno - That's why I asked for sample images. I believe issues are fixed now. – Simon Mourier Feb 07 '20 at 21:48
  • Thanks.. but the issue still exists for Newfile4 and 6.Please cross check with explorer preview images. – techno Feb 08 '20 at 04:29
  • @techno - you probably didn't copy my whole code properly. – Simon Mourier Feb 08 '20 at 06:14
  • I did not update the `Transformations` . Now its working fine.I hope the extra computations won't effect the performance. – techno Feb 08 '20 at 09:33
  • @techno - It will eat some perf, but only tests will tell you. It would be interesting to compare the builtin-jpeg encoder perf vs encoder + WPF transforms perf. – Simon Mourier Feb 08 '20 at 09:44
  • Won't it be faster than `System.Drawing` . Its performance issue forced me to look for an alternative in the first case. – techno Feb 08 '20 at 09:49
  • Just an update.This code is not honoring the embedded color scheme of the image.In case of Bitmap, i can set `useEmbeddedColorManagement=true` , how to make this code honor the color scheme of the image? Please advice – techno Dec 02 '21 at 17:17