1

If I drag an image from Chrome (or Firefox) onto the Desktop, Windows Explorer (not IE) is able to save the file.

However, if I attempt to get the Bitmap from the System.Windows.Forms.IDataObject, there is no Bitmap data available for Chrome (and only DIB from Firefox).

So riddle me this, how is Explorer able to get the actual image from Chrome/Firefox, when it's not available in IDataObject? Does Microsoft have a undocumented feature the only Windows can use to extract the image data? I know how to get the image via the URI, or the FileDrop tempfile (see below). The question is "how to get the served file", like Explorer does.

Some things to note:

  1. The JPEG dropped onto the Desktop is identical (md5sum) to the one served, plus EXIF data is preserved, so it can't be converting the DIB (if present) to a JPEG.
  2. The Image is behind an authenticated session, so Explorer can't be fetching the file by the URL.
  3. Both browsers provide DragImageBits, but this isn't the original image (it's too small), plus it's only supposed to be used as an preview when dragging (Explorer does this).
  4. Firefox provides a FileDrop array which contains a tempfile, but this is a BMP, not a JPEG.

I actually wrote a small app to display IDataObject data from Paste/DragDrop events in order to solve this, but to no avail.

Source & binary available on GitHub

  • The dragged image is treates a file drop (goes in your temp folder). If it's a mixed drop (text and images, it could, but its more often treated as a file (http source) download. See this [DragDrop text from browser](https://stackoverflow.com/questions/48978257/dragdrop-text-from-browser-to-textbox?answertab=active#tab-top). There's also a small project that let's you test the results. The `DragImageBits` is the smaller iconic grayed image used to "animate" the drop. – Jimi Apr 06 '18 at 15:25

1 Answers1

2

After reading some comments about the System.Windows.Forms.IDataObject class not exposing IStream (which I had no idea about), I came across a CodeProject article Outlook Drag and Drop in C#.

Mail messages are an issue because the OS call returns an IStorage which is a compound file type, and again, the C# implementation of the IDataObject lets us down by not handling this type of return, so you get a null.

So, lifting liberally from that example (I haven't implement IStorage, as what I was mainly interested about turned out to be HGlobal) I created a set of extension methods that allow me to retrieve the FileContent.

Usage:

var fileNames = e.Data.GetFileContentNames();
for (int i = 0; i < files.Length; i++) {
  using (var ms = e.Data.GetFileContent(i)) {
    // Do something with your unadulterated content!
  }
}

Code:

using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Windows.Forms;

namespace DragDropViewer.ExtensionMethods {
  /// <summary>Helper methods for getting FileContents from DragDrop data.</summary>
  /// <see cref="!:https://www.codeproject.com/Articles/28209/Outlook-Drag-and-Drop-in-C"/>
  public static class IDataObjectExtensionMethods {
    /// <summary>Gets the array of FileNames from the FileGroupDescriptors format.</summary>
    public static string[] GetFileContentNames(this System.Windows.Forms.IDataObject data) {
      var names = new string[data.GetFileContentCount()];

      if (names.Length != 0) {
        var bytes = data.GetFileGroupDescriptor().ToArray();
        IntPtr fgdPtr = IntPtr.Zero;
        try {
          fgdPtr = Marshal.AllocHGlobal(bytes.Length);

          int offset = Marshal.SizeOf(typeof(UInt32));
          int size = Marshal.SizeOf(typeof(FILEDESCRIPTORW));

          for (int i = 0; i < names.Length; i++) {
            var fd = (FILEDESCRIPTORW)Marshal.PtrToStructure(fgdPtr + offset + (i * size), typeof(FILEDESCRIPTORW));
            names[i] = fd.cFileName;
          }

        } finally {
          if (fgdPtr != IntPtr.Zero) Marshal.FreeHGlobal(fgdPtr);

        }
      }

      return names;
    }

    /// <summary>Gets the number of files available in the FileGroupDescriptor format.</summary>
    public static int GetFileContentCount(this System.Windows.Forms.IDataObject data) {
      // File count is stored as an UInt32 in the FileGroupDescriptor format
      MemoryStream ms = data.GetFileGroupDescriptor();
      if (ms == null) return 0;

      using (var reader = new BinaryReader(ms)) {
        return (int)reader.ReadUInt32(); // Assume this won't overflow!
      }
    }

    /// <summary>Gets the file content for the specified FileDescriptor index.</summary>
    /// <param name="index">The index of the file content to retrieve.</param>
    public static MemoryStream GetFileContent(this System.Windows.Forms.IDataObject data, int index) {
      // As this is indexed, "FileContent" is most likely null, so the COM IDataObject needs to be used
      var comData = (System.Runtime.InteropServices.ComTypes.IDataObject)data;

      var formatetc = new FORMATETC() {
        cfFormat = (short)DataFormats.GetFormat("FileContents").Id,
        dwAspect = DVASPECT.DVASPECT_CONTENT,
        lindex = index,
        ptd = IntPtr.Zero,
        tymed = TYMED.TYMED_ISTREAM | TYMED.TYMED_HGLOBAL
      };

      var medium = new STGMEDIUM();
      comData.GetData(ref formatetc, out medium);

      switch (medium.tymed) {
        case TYMED.TYMED_HGLOBAL:
          return data.GetFileContentFromHGlobal(medium);

        case TYMED.TYMED_ISTREAM:
          return data.GetFileContentFromIStream(medium);

        default:
          throw new InvalidOperationException($"Cannot get FileContent for {medium.tymed} TYMED.");

      }
    }

    private static MemoryStream GetFileContentFromHGlobal(this System.Windows.Forms.IDataObject data, STGMEDIUM medium) {
      var innerDataField = data.GetType().GetField("innerData", BindingFlags.NonPublic | BindingFlags.Instance);
      var oldData = (System.Windows.Forms.IDataObject)innerDataField.GetValue(data);

      var getDataFromHGLOBLALMethod = oldData.GetType().GetMethod("GetDataFromHGLOBLAL", BindingFlags.NonPublic | BindingFlags.Instance);

      return (MemoryStream)getDataFromHGLOBLALMethod.Invoke(oldData, new object[] { "FileContents", medium.unionmember });
    }

    private static MemoryStream GetFileContentFromIStream(this System.Windows.Forms.IDataObject data, STGMEDIUM medium) {
      var iStream = (IStream)Marshal.GetObjectForIUnknown(medium.unionmember);
      Marshal.Release(medium.unionmember);

      var iStreamStat = new System.Runtime.InteropServices.ComTypes.STATSTG();
      iStream.Stat(out iStreamStat, 0);

      var content = new byte[(int)iStreamStat.cbSize];
      iStream.Read(content, content.Length, IntPtr.Zero);

      return new MemoryStream(content);
    }

    private static MemoryStream GetFileGroupDescriptor(this System.Windows.Forms.IDataObject data) {
      MemoryStream ms = null;
      if (data.GetDataPresent("FileGroupDescriptorW")) {
        ms = (MemoryStream)data.GetData("FileGroupDescriptorW", true);
      }

      return ms;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct FILEDESCRIPTORW {
      public UInt32 dwFlags;
      public Guid clsid;
      public System.Drawing.Size sizel;
      public System.Drawing.Point pointl;
      public UInt32 dwFileAttributes;
      public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
      public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
      public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
      public UInt32 nFileSizeHigh;
      public UInt32 nFileSizeLow;
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
      public String cFileName;
    }
  }
}