Using WIC to Quickly Create Scaled Down Bitmap that honors EXIF Orientation Tag
Asked Answered
S

1

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://mcmap.net/q/1136827/-displaying-thumbnails-of-very-high-resolution-images-fast-with-minimal-delay

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;

                    }

                }
            }
Schou answered 23/9, 2019 at 11:54 Comment(18)
See this: #689490 and this #33914640Iloilo
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.Malemute
@SimonMourier The code you refereed to uses Canvas class in System.Windows.Controls to perform the rotation.Is this necessary?Schou
No, just use Metadata from first link and TransformedBitmap from second. You can also save Metadata. See here for another example: markbetz.net/2011/05/31/…Iloilo
@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.Schou
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)Iloilo
@SimonMourier I need GDI+ as the entire code is based on GDI+Schou
@SimonMourier I'm trying to wire up the things .. please see paste.org/100655 .Do i need a separate BitmapDecoder ? As your code example uses it.. is there a way to create the TransformedBitmap from BitmapFrameSchou
Your code seems ok, answer yourself if you're happyIloilo
@SimonMourier I have note tested the code.Schou
@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.Schou
Do you have sample image(s)?Iloilo
@SimonMourier drive.google.com/file/d/1ktUJgP7p8ywVjGsi_VmqqzXsKSZAm62Y/…Schou
@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 BitmapFrameSchou
@SimonMourier Please see my update and this question #60088492Schou
You can pass a unique Transform to TransformedBitmap which would be a TransformGroup with all the transforms (Rotate, Scale, etc.). For flipping: learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/…Iloilo
@SimonMourier Can you provide an example on creating a TransformGroup with Scale and Rotate.Schou
@Schou Create a TransformGroup, add a RotateTransform and a ScaleTransform to its Children collection, then pass it to the TransformedBitmap constructor.Unstring
I
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
    }
}
Iloilo answered 7/2, 2020 at 10:58 Comment(9)
I have implemented this .. 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?Schou
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 drive.google.com/file/d/1aEMWmMgqBPgzHB5YsGA0dC93Q8FqzMb0/…Schou
@Schou - That's why I asked for sample images. I believe issues are fixed now.Iloilo
Thanks.. but the issue still exists for Newfile4 and 6.Please cross check with explorer preview images.Schou
@Schou - you probably didn't copy my whole code properly.Iloilo
I did not update the Transformations . Now its working fine.I hope the extra computations won't effect the performance.Schou
@Schou - 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.Iloilo
Won't it be faster than System.Drawing . Its performance issue forced me to look for an alternative in the first case.Schou
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 adviceSchou

© 2022 - 2024 — McMap. All rights reserved.