diff --git a/Definitions/Reference.cs b/Definitions/Reference.cs
new file mode 100644
index 0000000..694150b
--- /dev/null
+++ b/Definitions/Reference.cs
@@ -0,0 +1,219 @@
+using System;
+using System.Drawing;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+
+namespace PhotoThumbnail.Definitions
+{
+ internal static class HandlerNativeMethods
+ {
+ internal static readonly Guid IThumbnailProviderGuid = new Guid("e357fccd-a995-4576-b01f-234630154e96");
+
+ //internal static readonly Guid IInitializeWithFileGuid = new Guid("b7d14566-0509-4cce-a71f-0a554233bd9b");
+ internal static readonly Guid IInitializeWithStreamGuid = new Guid("b824b49d-22ac-4161-ac8a-9916e8fa3f7f");
+ //internal static readonly Guid IInitializeWithItemGuid = new Guid("7f73be3f-fb79-493c-a6c7-7ee14e245841");
+
+ internal static readonly Guid IMarshalGuid = new Guid("00000003-0000-0000-C000-000000000046");
+ }
+
+ ///
+ /// This interface exposes the function for initializing the
+ /// Thumbnail Provider with a .
+ /// If this interfaces is not used, then the handler must opt out of process isolation.
+ /// This interface can be used in conjunction with the other intialization interfaces,
+ /// but only 1 will be accessed according to the priorities preset by the Windows Shell:
+ ///
+ ///
+ ///
+ ///
+ public interface IThumbnailFromStream
+ {
+ ///
+ /// Provides the to the item from which a thumbnail should be created.
+ /// Only 32bpp bitmaps support adornments.
+ /// While 24bpp bitmaps will be displayed they will not display adornments.
+ /// Additional guidelines for developing thumbnails can be found at http://msdn.microsoft.com/en-us/library/cc144115(v=VS.85).aspx
+ ///
+ ///
+ /// Stream to initialize the thumbnail
+ /// Square side dimension in which the thumbnail should fit; the thumbnail will be scaled otherwise.
+ ///
+ Bitmap ConstructBitmap(Stream stream, int sideSize);
+ }
+
+ ///
+ /// The STGM constants are flags that indicate
+ /// conditions for creating and deleting the object and access modes
+ /// for the object.
+ ///
+ /// You can combine these flags, but you can only choose one flag
+ /// from each group of related flags. Typically one flag from each
+ /// of the access and sharing groups must be specified for all
+ /// functions and methods which use these constants.
+ ///
+ [Flags]
+ public enum AccessModes
+ {
+ ///
+ /// Indicates that, in direct mode, each change to a storage
+ /// or stream element is written as it occurs.
+ ///
+ Direct = 0x00000000,
+
+ ///
+ /// Indicates that, in transacted mode, changes are buffered
+ /// and written only if an explicit commit operation is called.
+ ///
+ Transacted = 0x00010000,
+
+ ///
+ /// Provides a faster implementation of a compound file
+ /// in a limited, but frequently used, case.
+ ///
+ Simple = 0x08000000,
+
+ ///
+ /// Indicates that the object is read-only,
+ /// meaning that modifications cannot be made.
+ ///
+ Read = 0x00000000,
+
+ ///
+ /// Enables you to save changes to the object,
+ /// but does not permit access to its data.
+ ///
+ Write = 0x00000001,
+
+ ///
+ /// Enables access and modification of object data.
+ ///
+ ReadWrite = 0x00000002,
+
+ ///
+ /// Specifies that subsequent openings of the object are
+ /// not denied read or write access.
+ ///
+ ShareDenyNone = 0x00000040,
+
+ ///
+ /// Prevents others from subsequently opening the object in Read mode.
+ ///
+ ShareDenyRead = 0x00000030,
+
+ ///
+ /// Prevents others from subsequently opening the object
+ /// for Write or ReadWrite access.
+ ///
+ ShareDenyWrite = 0x00000020,
+
+ ///
+ /// Prevents others from subsequently opening the object in any mode.
+ ///
+ ShareExclusive = 0x00000010,
+
+ ///
+ /// Opens the storage object with exclusive access to the most
+ /// recently committed version.
+ ///
+ Priority = 0x00040000,
+
+ ///
+ /// Indicates that the underlying file is to be automatically destroyed when the root
+ /// storage object is released. This feature is most useful for creating temporary files.
+ ///
+ DeleteOnRelease = 0x04000000,
+
+ ///
+ /// Indicates that, in transacted mode, a temporary scratch file is usually used
+ /// to save modifications until the Commit method is called.
+ /// Specifying NoScratch permits the unused portion of the original file
+ /// to be used as work space instead of creating a new file for that purpose.
+ ///
+ NoScratch = 0x00100000,
+
+ ///
+ /// Indicates that an existing storage object
+ /// or stream should be removed before the new object replaces it.
+ ///
+ Create = 0x00001000,
+
+ ///
+ /// Creates the new object while preserving existing data in a stream named "Contents".
+ ///
+ Convert = 0x00020000,
+
+ ///
+ /// Causes the create operation to fail if an existing object with the specified name exists.
+ ///
+ FailIfThere = 0x00000000,
+
+ ///
+ /// This flag is used when opening a storage object with Transacted
+ /// and without ShareExclusive or ShareDenyWrite.
+ /// In this case, specifying NoSnapshot prevents the system-provided
+ /// implementation from creating a snapshot copy of the file.
+ /// Instead, changes to the file are written to the end of the file.
+ ///
+ NoSnapshot = 0x00200000,
+
+ ///
+ /// Supports direct mode for single-writer, multireader file operations.
+ ///
+ DirectSingleWriterMultipleReader = 0x00400000
+ }
+
+ //
+ /// Thumbnail Alpha Types
+ ///
+ public enum ThumbnailAlphaType
+ {
+ ///
+ /// Let the system decide.
+ ///
+ Unknown = 0,
+
+ ///
+ /// No transparency
+ ///
+ NoAlphaChannel = 1,
+
+ ///
+ /// Has transparency
+ ///
+ HasAlphaChannel = 2,
+ }
+
+ ///
+ /// ComVisible interface for native IThumbnailProvider
+ ///
+ [ComImport]
+ [Guid("e357fccd-a995-4576-b01f-234630154e96")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ interface IThumbnailProvider
+ {
+ ///
+ /// Gets a pointer to a bitmap to display as a thumbnail
+ ///
+ ///
+ ///
+ ///
+ void GetThumbnail(uint squareLength, [Out] out IntPtr bitmapHandle, [Out] out uint bitmapType);
+ }
+
+ ///
+ /// Provides means by which to initialize with a stream.
+ ///
+ [ComImport]
+ [Guid("b824b49d-22ac-4161-ac8a-9916e8fa3f7f")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ interface IInitializeWithStream
+ {
+ ///
+ /// Initializes with a stream.
+ ///
+ ///
+ ///
+ void Initialize(IStream stream, AccessModes fileMode);
+ }
+}
diff --git a/Definitions/StorageStream.cs b/Definitions/StorageStream.cs
new file mode 100644
index 0000000..b266d32
--- /dev/null
+++ b/Definitions/StorageStream.cs
@@ -0,0 +1,265 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+
+namespace PhotoThumbnail.Definitions
+{
+ ///
+ /// A wrapper for the native IStream object.
+ ///
+ public class StorageStream : Stream, IDisposable
+ {
+ private IStream _stream;
+ private readonly bool _isReadOnly;
+
+ internal StorageStream(IStream stream, bool readOnly)
+ {
+ _stream = stream ?? throw new ArgumentNullException("stream");
+ _isReadOnly = readOnly;
+ }
+
+ ///
+ /// Reads a single byte from the stream, moving the current position ahead by 1.
+ ///
+ /// A single byte from the stream, -1 if end of stream.
+ public override int ReadByte()
+ {
+ ThrowIfDisposed();
+
+ byte[] buffer = new byte[1];
+ if (Read(buffer, 0, 1) > 0)
+ {
+ return buffer[0];
+ }
+ return -1;
+ }
+
+ ///
+ /// Writes a single byte to the stream
+ ///
+ /// Byte to write to stream
+ public override void WriteByte(byte value)
+ {
+ ThrowIfDisposed();
+ byte[] buffer = new byte[] { value };
+ Write(buffer, 0, 1);
+ }
+
+ ///
+ /// Gets whether the stream can be read from.
+ ///
+ public override bool CanRead => _stream != null;
+
+ ///
+ /// Gets whether seeking is supported by the stream.
+ ///
+ public override bool CanSeek => _stream != null;
+
+ ///
+ /// Gets whether the stream can be written to.
+ /// Always false.
+ ///
+ public override bool CanWrite => _stream != null && !_isReadOnly;
+
+ ///
+ /// Reads a buffer worth of bytes from the stream.
+ ///
+ /// Buffer to fill
+ /// Offset to start filling in the buffer
+ /// Number of bytes to read from the stream
+ ///
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ ThrowIfDisposed();
+
+ if (buffer == null) { throw new ArgumentNullException("buffer"); }
+ if (offset < 0) { throw new ArgumentOutOfRangeException("offset", "StorageStreamOffsetLessThanZero"); }
+ if (count < 0) { throw new ArgumentOutOfRangeException("count", "StorageStreamCountLessThanZero"); }
+ if (offset + count > buffer.Length) { throw new ArgumentException("StorageStreamBufferOverflow", "count"); }
+
+ int bytesRead = 0;
+ if (count > 0)
+ {
+ IntPtr ptr = Marshal.AllocCoTaskMem(sizeof(ulong));
+ try
+ {
+ if (offset == 0)
+ {
+ _stream.Read(buffer, count, ptr);
+ bytesRead = (int)Marshal.ReadInt64(ptr);
+ }
+ else
+ {
+ byte[] tempBuffer = new byte[count];
+ _stream.Read(tempBuffer, count, ptr);
+
+ bytesRead = (int)Marshal.ReadInt64(ptr);
+ if (bytesRead > 0)
+ {
+ Array.Copy(tempBuffer, 0, buffer, offset, bytesRead);
+ }
+ }
+ }
+ finally
+ {
+ Marshal.FreeCoTaskMem(ptr);
+ }
+ }
+ return bytesRead;
+ }
+
+ ///
+ /// Writes a buffer to the stream if able to do so.
+ ///
+ /// Buffer to write
+ /// Offset in buffer to start writing
+ /// Number of bytes to write to the stream
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ ThrowIfDisposed();
+
+ if (_isReadOnly) { throw new InvalidOperationException("StorageStreamIsReadonly"); }
+ if (buffer == null) { throw new ArgumentNullException("buffer"); }
+ if (offset < 0) { throw new ArgumentOutOfRangeException("offset", "StorageStreamOffsetLessThanZero"); }
+ if (count < 0) { throw new ArgumentOutOfRangeException("count", "StorageStreamCountLessThanZero"); }
+ if (offset + count > buffer.Length) { throw new ArgumentException("StorageStreamBufferOverflow", "count"); }
+
+ if (count > 0)
+ {
+ IntPtr ptr = Marshal.AllocCoTaskMem(sizeof(ulong));
+ try
+ {
+ if (offset == 0)
+ {
+ _stream.Write(buffer, count, ptr);
+ }
+ else
+ {
+ byte[] tempBuffer = new byte[count];
+ Array.Copy(buffer, offset, tempBuffer, 0, count);
+ _stream.Write(tempBuffer, count, ptr);
+ }
+ }
+ finally
+ {
+ Marshal.FreeCoTaskMem(ptr);
+ }
+ }
+ }
+
+ ///
+ /// Gets the length of the IStream
+ ///
+ public override long Length
+ {
+ get
+ {
+ ThrowIfDisposed();
+ const int STATFLAG_NONAME = 1;
+ _stream.Stat(out System.Runtime.InteropServices.ComTypes.STATSTG stats, STATFLAG_NONAME);
+ return stats.cbSize;
+ }
+ }
+
+ ///
+ /// Gets or sets the current position within the underlying IStream.
+ ///
+ public override long Position
+ {
+ get
+ {
+ ThrowIfDisposed();
+ return Seek(0, SeekOrigin.Current);
+ }
+ set
+ {
+ ThrowIfDisposed();
+ Seek(value, SeekOrigin.Begin);
+ }
+ }
+
+ ///
+ /// Seeks within the underlying IStream.
+ ///
+ /// Offset
+ /// Where to start seeking
+ ///
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ ThrowIfDisposed();
+ IntPtr ptr = Marshal.AllocCoTaskMem(sizeof(long));
+ try
+ {
+ _stream.Seek(offset, (int)origin, ptr);
+ return Marshal.ReadInt64(ptr);
+ }
+ finally
+ {
+ Marshal.FreeCoTaskMem(ptr);
+ }
+ }
+
+ ///
+ /// Sets the length of the stream
+ ///
+ ///
+ public override void SetLength(long value)
+ {
+ ThrowIfDisposed();
+ _stream.SetSize(value);
+ }
+
+ ///
+ /// Commits data to be written to the stream if it is being cached.
+ ///
+ public override void Flush()
+ {
+ _stream.Commit((int)StorageStreamCommitOptions.None);
+ }
+
+ ///
+ /// Disposes the stream.
+ ///
+ /// True if called from Dispose(), false if called from finalizer.
+ protected override void Dispose(bool disposing)
+ {
+ _stream = null;
+ base.Dispose(disposing);
+ }
+
+ private void ThrowIfDisposed() { if (_stream == null) throw new ObjectDisposedException(GetType().Name); }
+ }
+
+ ///
+ /// Options for commiting (flushing) an IStream storage stream
+ ///
+ [Flags]
+ internal enum StorageStreamCommitOptions
+ {
+ ///
+ /// Uses default options
+ ///
+ None = 0,
+
+ ///
+ /// Overwrite option
+ ///
+ Overwrite = 1,
+
+ ///
+ /// Only if current
+ ///
+ OnlyIfCurrent = 2,
+
+ ///
+ /// Commits to disk cache dangerously
+ ///
+ DangerouslyCommitMerelyToDiskCache = 4,
+
+ ///
+ /// Consolidate
+ ///
+ Consolidate = 8
+ }
+}
diff --git a/Definitions/ThumbnailProvider.cs b/Definitions/ThumbnailProvider.cs
new file mode 100644
index 0000000..f343533
--- /dev/null
+++ b/Definitions/ThumbnailProvider.cs
@@ -0,0 +1,256 @@
+using Microsoft.Win32;
+using System;
+using System.Drawing;
+using System.Linq;
+using System.Runtime.InteropServices;
+
+namespace PhotoThumbnail.Definitions
+{
+ public abstract class ThumbnailProvider : IThumbnailProvider, ICustomQueryInterface, IDisposable, IInitializeWithStream
+ {
+ private Bitmap GetBitmap(int sideLength)
+ {
+ if (_stream != null && this is IThumbnailFromStream stream)
+ {
+ return stream.ConstructBitmap(_stream, sideLength);
+ }
+
+ throw new InvalidOperationException("ThumbnailProviderInterfaceNotImplemented: " + GetType().Name);
+ }
+
+ public virtual ThumbnailAlphaType ThumbnailAlphaType => ThumbnailAlphaType.Unknown;
+
+ private StorageStream _stream;
+
+ #region IThumbnailProvider Members
+
+ void IThumbnailProvider.GetThumbnail(uint sideLength, out IntPtr hBitmap, out uint alphaType)
+ {
+ using (Bitmap map = GetBitmap((int)sideLength))
+ {
+ hBitmap = map.GetHbitmap();
+ }
+ alphaType = (uint)ThumbnailAlphaType;
+ }
+
+ #endregion
+
+ #region ICustomQueryInterface Members
+
+ CustomQueryInterfaceResult ICustomQueryInterface.GetInterface(ref Guid iid, out IntPtr ppv)
+ {
+ ppv = IntPtr.Zero;
+
+ // Forces COM to not use the managed (free threaded) marshaler
+ if (iid == HandlerNativeMethods.IMarshalGuid)
+ {
+ return CustomQueryInterfaceResult.Failed;
+ }
+
+ if (iid == HandlerNativeMethods.IInitializeWithStreamGuid && !(this is IThumbnailFromStream))
+ {
+ return CustomQueryInterfaceResult.Failed;
+ }
+
+ return CustomQueryInterfaceResult.NotHandled;
+ }
+
+ #endregion
+
+ #region COM Registration
+
+ ///
+ /// Called when the assembly is registered via RegAsm.
+ ///
+ /// Type to be registered.
+ [ComRegisterFunction]
+ private static void Register(Type registerType)
+ {
+ if (registerType != null && registerType.IsSubclassOf(typeof(ThumbnailProvider)))
+ {
+ object[] attributes = registerType.GetCustomAttributes(typeof(ThumbnailProviderAttribute), true);
+ if (attributes != null && attributes.Length == 1)
+ {
+ ThumbnailProviderAttribute attribute = attributes[0] as ThumbnailProviderAttribute;
+ ThrowIfInvalid(registerType, attribute);
+ RegisterThumbnailHandler(registerType.GUID.ToString("B"), attribute);
+ }
+ }
+ }
+
+ private static void RegisterThumbnailHandler(string guid, ThumbnailProviderAttribute attribute)
+ {
+ // set process isolation
+ using (RegistryKey clsidKey = Registry.ClassesRoot.OpenSubKey("CLSID"))
+ using (RegistryKey guidKey = clsidKey.OpenSubKey(guid, true))
+ {
+ guidKey.SetValue("DisableProcessIsolation", attribute.DisableProcessIsolation ? 1 : 0, RegistryValueKind.DWord);
+
+ using (RegistryKey inproc = guidKey.OpenSubKey("InprocServer32", true))
+ {
+ inproc.SetValue("ThreadingModel", "Apartment", RegistryValueKind.String);
+ }
+ }
+
+ // register file as an approved extension
+ using (RegistryKey approvedShellExtensions = Registry.LocalMachine.OpenSubKey(
+ @"SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved", true))
+ {
+ approvedShellExtensions.SetValue(guid, attribute.Name, RegistryValueKind.String);
+ }
+
+ // register extension with each extension in the list
+ string[] extensions = attribute.Extensions.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (string extension in extensions)
+ {
+ using (RegistryKey extensionKey = Registry.ClassesRoot.CreateSubKey(extension)) // Create makes it writable
+ using (RegistryKey shellExKey = extensionKey.CreateSubKey("shellex"))
+ using (RegistryKey providerKey = shellExKey.CreateSubKey(HandlerNativeMethods.IThumbnailProviderGuid.ToString("B")))
+ {
+ providerKey.SetValue(null, guid, RegistryValueKind.String);
+
+ if (attribute.ThumbnailCutoff == ThumbnailCutoffSize.Square20)
+ {
+ extensionKey.DeleteValue("ThumbnailCutoff", false);
+ }
+ else
+ {
+ extensionKey.SetValue("ThumbnailCutoff", (int)attribute.ThumbnailCutoff, RegistryValueKind.DWord);
+ }
+
+
+ if (attribute.TypeOverlay != null)
+ {
+ extensionKey.SetValue("TypeOverlay", attribute.TypeOverlay, RegistryValueKind.String);
+ }
+
+ if (attribute.ThumbnailAdornment == ThumbnailAdornment.Default)
+ {
+ extensionKey.DeleteValue("Treatment", false);
+ }
+ else
+ {
+ extensionKey.SetValue("Treatment", (int)attribute.ThumbnailAdornment, RegistryValueKind.DWord);
+ }
+ }
+ }
+ }
+
+
+ ///
+ /// Called when the assembly is registered via RegAsm.
+ ///
+ /// Type to register.
+ [ComUnregisterFunction]
+ private static void Unregister(Type registerType)
+ {
+ if (registerType != null && registerType.IsSubclassOf(typeof(ThumbnailProvider)))
+ {
+ object[] attributes = registerType.GetCustomAttributes(typeof(ThumbnailProviderAttribute), true);
+ if (attributes != null && attributes.Length == 1)
+ {
+ ThumbnailProviderAttribute attribute = attributes[0] as ThumbnailProviderAttribute;
+ UnregisterThumbnailHandler(registerType.GUID.ToString("B"), attribute);
+ }
+ }
+ }
+
+ private static void UnregisterThumbnailHandler(string guid, ThumbnailProviderAttribute attribute)
+ {
+ string[] extensions = attribute.Extensions.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (string extension in extensions)
+ {
+ using (RegistryKey extKey = Registry.ClassesRoot.OpenSubKey(extension, true))
+ using (RegistryKey shellexKey = extKey.OpenSubKey("shellex", true))
+ {
+ shellexKey.DeleteSubKey(HandlerNativeMethods.IThumbnailProviderGuid.ToString("B"), false);
+
+ extKey.DeleteValue("ThumbnailCutoff", false);
+ extKey.DeleteValue("TypeOverlay", false);
+ extKey.DeleteValue("Treatment", false); // Thumbnail adornment
+ }
+ }
+
+ using (RegistryKey approvedShellExtensions = Registry.LocalMachine.OpenSubKey(
+ @"SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved", true))
+ {
+ approvedShellExtensions.DeleteValue(guid, false);
+ }
+ }
+
+ private static void ThrowIfInvalid(Type type, ThumbnailProviderAttribute attribute)
+ {
+ if (attribute is null)
+ {
+ throw new ArgumentNullException(nameof(attribute));
+ }
+
+ var interfaces = type.GetInterfaces();
+ bool interfaced = interfaces.Any(x => x == typeof(IThumbnailFromStream));
+
+ /*
+ if (interfaces.Any(x => x == typeof(IThumbnailFromShellObject) || x == typeof(IThumbnailFromFile)))
+ {
+ // According to MSDN (http://msdn.microsoft.com/en-us/library/cc144114(v=VS.85).aspx)
+ // A thumbnail provider that does not implement IInitializeWithStream must opt out of
+ // running in the isolated process. The default behavior of the indexer opts in
+ // to process isolation regardless of which interfaces are implemented.
+ if (!interfaced && !attribute.DisableProcessIsolation)
+ {
+ throw new InvalidOperationException("ThumbnailProviderDisabledProcessIsolation: " + type.Name);
+ }
+ interfaced = true;
+ }
+ */
+
+ if (!interfaced)
+ {
+ throw new InvalidOperationException("ThumbnailProviderInterfaceNotImplemented: " + type.Name);
+ }
+ }
+
+ #endregion
+
+ #region IInitializeWithStream Members
+
+ void IInitializeWithStream.Initialize(System.Runtime.InteropServices.ComTypes.IStream stream, AccessModes fileMode)
+ {
+ _stream = new StorageStream(stream, fileMode != AccessModes.ReadWrite);
+ }
+
+ #endregion
+
+ #region IDisposable Members
+
+ ///
+ /// Finalizer for the thumbnail provider.
+ ///
+ ~ThumbnailProvider()
+ {
+ Dispose(false);
+ }
+
+ ///
+ /// Disposes the thumbnail provider.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disploses the thumbnail provider.
+ ///
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing && _stream != null)
+ {
+ _stream.Dispose();
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Definitions/ThumbnailProviderAttribute.cs b/Definitions/ThumbnailProviderAttribute.cs
new file mode 100644
index 0000000..4c52317
--- /dev/null
+++ b/Definitions/ThumbnailProviderAttribute.cs
@@ -0,0 +1,136 @@
+using System;
+
+namespace PhotoThumbnail.Definitions
+{
+ ///
+ /// This class attribute is applied to a Thumbnail Provider to specify registration parameters
+ /// and aesthetic attributes.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+ public sealed class ThumbnailProviderAttribute : Attribute
+ {
+ ///
+ /// Creates a new instance of the attribute.
+ ///
+ /// Name of the provider
+ /// Semi-colon-separated list of extensions supported by this provider.
+ public ThumbnailProviderAttribute(string name, string extensions)
+ {
+ Name = name ?? throw new ArgumentNullException("name");
+ Extensions = extensions ?? throw new ArgumentNullException("extensions");
+
+ DisableProcessIsolation = false;
+ ThumbnailCutoff = ThumbnailCutoffSize.Square20;
+ TypeOverlay = null;
+ ThumbnailAdornment = ThumbnailAdornment.Default;
+ }
+
+ ///
+ /// Gets the name of the provider
+ ///
+ public string Name { get; private set; }
+
+ ///
+ /// Gets the semi-colon-separated list of extensions supported by the provider.
+ ///
+ public string Extensions { get; private set; }
+
+ // optional parameters below.
+
+ ///
+ /// Opts-out of running within the surrogate process DllHost.exe.
+ /// This will reduce robustness and security.
+ /// This value should be true if the provider does not implement .
+ ///
+ // Note: The msdn documentation and property name are contradicting.
+ // http://msdn.microsoft.com/en-us/library/cc144118(VS.85).aspx
+ public bool DisableProcessIsolation { get; set; } // If true: Makes it run IN PROCESS.
+
+
+ ///
+ /// Below this size thumbnail images will not be generated - file icons will be used instead.
+ ///
+ public ThumbnailCutoffSize ThumbnailCutoff { get; set; }
+
+ ///
+ /// A resource reference string pointing to the icon to be used as an overlay on the bottom right of the thumbnail.
+ /// ex. ISVComponent.dll@,-155
+ /// ex. C:\Windows\System32\SampleIcon.ico
+ /// If an empty string is provided, no overlay will be used.
+ /// If the property is set to null, the default icon for the associated icon will be used as an overlay.
+ ///
+ public string TypeOverlay { get; set; }
+
+ ///
+ /// Specifies the for the thumbnail.
+ ///
+ /// Only 32bpp bitmaps support adornments.
+ /// While 24bpp bitmaps will be displayed, their adornments will not.
+ /// If an adornment is specified by the file-type's associated application,
+ /// the applications adornment will override the value specified in this registration.
+ ///
+ public ThumbnailAdornment ThumbnailAdornment { get; set; }
+ }
+
+ ///
+ /// Defines the minimum thumbnail size for which thumbnails will be generated.
+ ///
+ public enum ThumbnailCutoffSize
+ {
+ ///
+ /// Default size of 20x20
+ ///
+ Square20 = -1, //For 20x20, you do not add any key in the registry
+
+ ///
+ /// Size of 32x32
+ ///
+ Square32 = 0,
+
+ ///
+ /// Size of 16x16
+ ///
+ Square16 = 1,
+
+ ///
+ /// Size of 48x48
+ ///
+ Square48 = 2,
+
+ ///
+ /// Size of 16x16. An alternative to Square16.
+ ///
+ Square16B = 3
+ }
+
+ ///
+ /// Adornment applied to thumbnails.
+ ///
+ public enum ThumbnailAdornment
+ {
+ ///
+ /// This will use the associated application's default icon as the adornment.
+ ///
+ Default = -1, // Default behaviour for no value added in registry
+
+ ///
+ /// No adornment
+ ///
+ None = 0,
+
+ ///
+ /// Drop shadow adornment
+ ///
+ DropShadow = 1,
+
+ ///
+ /// Photo border adornment
+ ///
+ PhotoBorder = 2,
+
+ ///
+ /// Video sprocket adornment
+ ///
+ VideoSprockets = 3
+ }
+}
diff --git a/PhotoThumbnail.sln b/PhotoThumbnail.sln
new file mode 100644
index 0000000..519958d
--- /dev/null
+++ b/PhotoThumbnail.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.32014.148
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhotoThumbnail", "PhotoThumbnail\PhotoThumbnail.csproj", "{08C34C77-B778-46AA-A48B-BEA6B9FE07FE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsoleApp", "TestConsoleApp\TestConsoleApp.csproj", "{C2573376-C9CF-41DE-B9A6-22790C048062}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {08C34C77-B778-46AA-A48B-BEA6B9FE07FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {08C34C77-B778-46AA-A48B-BEA6B9FE07FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {08C34C77-B778-46AA-A48B-BEA6B9FE07FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {08C34C77-B778-46AA-A48B-BEA6B9FE07FE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C2573376-C9CF-41DE-B9A6-22790C048062}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C2573376-C9CF-41DE-B9A6-22790C048062}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C2573376-C9CF-41DE-B9A6-22790C048062}.Release|Any CPU.ActiveCfg = Debug|Any CPU
+ {C2573376-C9CF-41DE-B9A6-22790C048062}.Release|Any CPU.Build.0 = Debug|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {40333A22-52A4-4082-B351-CD5B9550653E}
+ EndGlobalSection
+EndGlobal
diff --git a/PhotoThumbnail/PhotoThumbnail.csproj b/PhotoThumbnail/PhotoThumbnail.csproj
new file mode 100644
index 0000000..316fbb4
--- /dev/null
+++ b/PhotoThumbnail/PhotoThumbnail.csproj
@@ -0,0 +1,73 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {08C34C77-B778-46AA-A48B-BEA6B9FE07FE}
+ Library
+ Properties
+ PhotoThumbnail
+ PhotoThumbnail
+ v4.7.2
+ 512
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+ true
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+ true
+
+
+ true
+
+
+ PhotoThumbnail.pfx
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Definitions\Reference.cs
+
+
+ Definitions\StorageStream.cs
+
+
+ Definitions\ThumbnailProvider.cs
+
+
+ Definitions\ThumbnailProviderAttribute.cs
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PhotoThumbnail/Properties/AssemblyInfo.cs b/PhotoThumbnail/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..e246c0c
--- /dev/null
+++ b/PhotoThumbnail/Properties/AssemblyInfo.cs
@@ -0,0 +1,41 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.InteropServices;
+using System.Security;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("PhotoThumbnail")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("PhotoThumbnail")]
+[assembly: AssemblyCopyright("Copyright © 2021")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+[assembly: SecurityRules(SecurityRuleSet.Level1)]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("08c34c77-b778-46aa-a48b-bea6b9fe07fe")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+
+[assembly: NeutralResourcesLanguage("en")]
diff --git a/PhotoThumbnail/TgaDecoder.cs b/PhotoThumbnail/TgaDecoder.cs
new file mode 100644
index 0000000..90a8dc0
--- /dev/null
+++ b/PhotoThumbnail/TgaDecoder.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Drawing;
+using System.IO;
+
+namespace PhotoThumbnail
+{
+ internal class TgaDecoder
+ {
+ private class TgaData
+ {
+ private const int TgaHeaderSize = 18;
+
+ //private int idFieldLength;
+ private readonly int colorMapType;
+ private readonly int imageType;
+ //private int colorMapIndex;
+ //private int colorMapLength;
+ //private int colorMapDepth;
+ //private int imageOriginX;
+ //private int imageOriginY;
+ private readonly int imageWidth;
+ private readonly int imageHeight;
+ private readonly int bitPerPixel;
+ private readonly int descriptor;
+ private readonly byte[] colorData;
+
+ public TgaData(byte[] image)
+ {
+ //idFieldLength = image[0];
+ colorMapType = image[1];
+ imageType = image[2];
+ //colorMapIndex = image[4] << 8 | image[3];
+ //colorMapLength = image[6] << 8 | image[5];
+ //colorMapDepth = image[7];
+ //imageOriginX = image[9] << 8 | image[8];
+ //imageOriginY = image[11] << 8 | image[10];
+ imageWidth = image[13] << 8 | image[12];
+ imageHeight = image[15] << 8 | image[14];
+ bitPerPixel = image[16];
+ descriptor = image[17];
+ colorData = new byte[image.Length - TgaHeaderSize];
+ Array.Copy(image, TgaHeaderSize, colorData, 0, colorData.Length);
+ // Index color RLE or Full color RLE or Gray RLE
+ if (imageType == 9 || imageType == 10 || imageType == 11)
+ colorData = DecodeRLE();
+ }
+
+ public int Width => imageWidth;
+
+ public int Height => imageHeight;
+
+ public int GetPixel(int x, int y)
+ {
+ if (colorMapType == 0)
+ {
+ switch (imageType)
+ {
+ // Index color
+ case 1:
+ case 9:
+ // not implemented
+ return 0;
+
+ // Full color
+ case 2:
+ case 10:
+ int elementCount = bitPerPixel / 8;
+ int dy = ((descriptor & 0x20) == 0 ? (imageHeight - 1 - y) : y) * (imageWidth * elementCount);
+ int dx = ((descriptor & 0x10) == 0 ? x : (imageWidth - 1 - x)) * elementCount;
+ int index = dy + dx;
+
+ int b = colorData[index + 0] & 0xFF;
+ int g = colorData[index + 1] & 0xFF;
+ int r = colorData[index + 2] & 0xFF;
+
+ if (elementCount == 4) // bitPerPixel == 32
+ {
+ int a = colorData[index + 3] & 0xFF;
+ return (a << 24) | (r << 16) | (g << 8) | b;
+ }
+ else if (elementCount == 3) // bitPerPixel == 24
+ {
+ return (r << 16) | (g << 8) | b;
+ }
+ break;
+
+ // Gray
+ case 3:
+ case 11:
+ // not implemented
+ return 0;
+ }
+ return 0;
+ }
+ else
+ {
+ // not implemented
+ return 0;
+ }
+ }
+
+ protected byte[] DecodeRLE()
+ {
+ int elementCount = bitPerPixel / 8;
+ byte[] elements = new byte[elementCount];
+ int decodeBufferLength = elementCount * imageWidth * imageHeight;
+ byte[] decodeBuffer = new byte[decodeBufferLength];
+ int decoded = 0;
+ int offset = 0;
+ while (decoded < decodeBufferLength)
+ {
+ int packet = colorData[offset++] & 0xFF;
+ if ((packet & 0x80) != 0)
+ {
+ for (int i = 0; i < elementCount; i++)
+ {
+ elements[i] = colorData[offset++];
+ }
+ int count = (packet & 0x7F) + 1;
+ for (int i = 0; i < count; i++)
+ {
+ for (int j = 0; j < elementCount; j++)
+ {
+ decodeBuffer[decoded++] = elements[j];
+ }
+ }
+ }
+ else
+ {
+ int count = (packet + 1) * elementCount;
+ for (int i = 0; i < count; i++)
+ {
+ decodeBuffer[decoded++] = colorData[offset++];
+ }
+ }
+ }
+ return decodeBuffer;
+ }
+ }
+
+ public static Bitmap FromStream(Stream stream)
+ {
+ using (var mstream = new MemoryStream())
+ {
+ stream.CopyTo(mstream);
+
+ return Decode(mstream.ToArray());
+ }
+ }
+
+ private unsafe static Bitmap Decode(byte[] image)
+ {
+ TgaData tga = new TgaData(image);
+ Rectangle rect = new Rectangle(0, 0, tga.Width, tga.Height);
+ Bitmap bitmap = new Bitmap(rect.Width, rect.Height);
+
+ var data = bitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
+ byte* p = (byte*)data.Scan0;
+ int offset = data.Stride - rect.Width * 4;
+
+ for (var y = 0; y < tga.Height; y++)
+ {
+ for (var x = 0; x < tga.Width; x++)
+ {
+ var c = BitConverter.GetBytes(tga.GetPixel(x, y));
+ if (c[3] == 0)
+ {
+ p += 4;
+ }
+ else
+ {
+ *p++ = c[0];
+ *p++ = c[1];
+ *p++ = c[2];
+ *p++ = c[3];
+ }
+ }
+ p += offset;
+ }
+
+ bitmap.UnlockBits(data);
+ return bitmap;
+ }
+ }
+}
diff --git a/PhotoThumbnail/TgaThumbnailer.cs b/PhotoThumbnail/TgaThumbnailer.cs
new file mode 100644
index 0000000..e06ef16
--- /dev/null
+++ b/PhotoThumbnail/TgaThumbnailer.cs
@@ -0,0 +1,44 @@
+using PhotoThumbnail.Definitions;
+using System;
+using System.Drawing;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace PhotoThumbnail
+{
+ [Guid("6f36dc76-d390-4042-b5d7-89e96e5dddc2")]
+ [ComVisible(true)]
+ [ClassInterface(ClassInterfaceType.None)]
+ [ProgId("PhotoThumbnail.TgaThumbnailer")]
+ [ThumbnailProvider("TgaThumbnailer", ".tga", ThumbnailAdornment = ThumbnailAdornment.PhotoBorder)]
+ public class TgaThumbnailer : ThumbnailProvider, IThumbnailFromStream
+ {
+ #region IThumbnailFromStream Members
+
+ public Bitmap ConstructBitmap(Stream stream, int sideSize)
+ {
+ Bitmap bitmap = TgaDecoder.FromStream(stream);
+
+ if (bitmap.Width > sideSize || bitmap.Height > sideSize)
+ {
+ int w = bitmap.Width;
+ int h = bitmap.Height;
+ if (w > sideSize)
+ {
+ h = sideSize * h / w;
+ w = sideSize;
+ }
+ else
+ {
+ w = w * sideSize / h;
+ h = sideSize;
+ }
+ bitmap = new Bitmap(bitmap, w, h);
+ }
+
+ return bitmap;
+ }
+
+ #endregion
+ }
+}
diff --git a/TestConsoleApp/App.config b/TestConsoleApp/App.config
new file mode 100644
index 0000000..56efbc7
--- /dev/null
+++ b/TestConsoleApp/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/TestConsoleApp/Program.cs b/TestConsoleApp/Program.cs
new file mode 100644
index 0000000..efb3c1d
--- /dev/null
+++ b/TestConsoleApp/Program.cs
@@ -0,0 +1,15 @@
+using PhotoThumbnail;
+using System.Drawing.Imaging;
+using System.IO;
+
+namespace TestConsoleApp
+{
+ internal class Program
+ {
+ static void Main(string[] args)
+ {
+ var bitmap = TgaDecoder.FromStream(File.OpenRead(@"C:\Program Files (x86)\Steam\steamapps\common\wallpaper_engine\assets\presets\fern\materials\presets\fern1.tga"));
+ bitmap.Save(@"C:\Users\tsanie\Downloads\test.png", ImageFormat.Png);
+ }
+ }
+}
diff --git a/TestConsoleApp/Properties/AssemblyInfo.cs b/TestConsoleApp/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..9807101
--- /dev/null
+++ b/TestConsoleApp/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("TestConsoleApp")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("TestConsoleApp")]
+[assembly: AssemblyCopyright("Copyright © 2021")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("c2573376-c9cf-41de-b9a6-22790c048062")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/TestConsoleApp/TestConsoleApp.csproj b/TestConsoleApp/TestConsoleApp.csproj
new file mode 100644
index 0000000..79ab3a3
--- /dev/null
+++ b/TestConsoleApp/TestConsoleApp.csproj
@@ -0,0 +1,59 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {C2573376-C9CF-41DE-B9A6-22790C048062}
+ Exe
+ TestConsoleApp
+ TestConsoleApp
+ v4.7.2
+ 512
+ true
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+ true
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TgaDecoder.cs
+
+
+
+
+
+
+
+
+
\ No newline at end of file