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