using System; using System.ComponentModel; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Permissions; using System.Windows; using System.Windows.Controls; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Resources; using HandyControl.Data; using HandyControl.Tools; using HandyControl.Tools.Interop; namespace HandyControl.Controls { public class GifImage : Image, IDisposable { private static readonly Guid GifGuid = new Guid("{b96b3caa-0728-11d3-9d7b-0000f81ef32e}"); private static readonly Guid GifSingleFrameGuid = new Guid("{b96b3cb0-0728-11d3-9d7b-0000f81ef32e}"); internal IntPtr NativeImage; private byte[] _rawData; private bool _isStart; private bool _isLoaded; static GifImage() { VisibilityProperty.OverrideMetadata(typeof(GifImage), new PropertyMetadata(default(Visibility), OnVisibilityChanged)); } public static readonly DependencyProperty UriProperty = DependencyProperty.Register( "Uri", typeof(Uri), typeof(GifImage), new PropertyMetadata(default(Uri), OnUriChanged)); private static void OnUriChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var ctl = (GifImage) d; ctl.StopAnimate(); if (e.NewValue != null) { var v = (Uri) e.NewValue; ctl.GetGifStreamFromPack(v); ctl.StartAnimate(); } else { ctl.Source = null; } } public Uri Uri { get => (Uri) GetValue(UriProperty); set => SetValue(UriProperty, value); } private static void OnVisibilityChanged(DependencyObject s, DependencyPropertyChangedEventArgs e) { var ctl = (GifImage) s; if (ctl.NativeImage == IntPtr.Zero) return; var v = (Visibility) e.NewValue; if (v != Visibility.Visible) { ctl.StopAnimate(); } else if (!ctl._isStart) { ctl.StartAnimate(); } } ~GifImage() => Dispose(false); public GifImage() { Loaded += (s, e) => { if (DesignerProperties.GetIsInDesignMode(this)) return; if (_isLoaded) return; _isLoaded = true; if (Uri != null) { GetGifStreamFromPack(Uri); StartAnimate(); } else if (Source is BitmapImage image) { GetGifStreamFromPack(image.UriSource); StartAnimate(); } }; Unloaded += (s, e) => Dispose(); } public GifImage(string filename) { Source = null; CreateSourceFromFile(filename); StartAnimate(); } public GifImage(Stream stream) { Source = null; CreateSourceFromStream(stream); StartAnimate(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (NativeImage == IntPtr.Zero) return; try { StopAnimate(); InteropMethods.Gdip.GdipDisposeImage(new HandleRef(this, NativeImage)); } catch { // ignored } finally { NativeImage = IntPtr.Zero; } } private void CreateSourceFromFile(string filename) { filename = Path.GetFullPath(filename); var status = InteropMethods.Gdip.GdipCreateBitmapFromFile(filename, out var bitmap); if (status != InteropMethods.Gdip.Ok) throw InteropMethods.Gdip.StatusException(status); status = InteropMethods.Gdip.GdipImageForceValidation(new HandleRef(null, bitmap)); if (status != InteropMethods.Gdip.Ok) { InteropMethods.Gdip.GdipDisposeImage(new HandleRef(null, bitmap)); throw InteropMethods.Gdip.StatusException(status); } SetNativeImage(bitmap); EnsureSave(this, filename, null); } private void CreateSourceFromStream(Stream stream) { if (stream == null) throw new ArgumentException("stream null"); var status = InteropMethods.Gdip.GdipCreateBitmapFromStream(new GPStream(stream), out var bitmap); if (status != InteropMethods.Gdip.Ok) throw InteropMethods.Gdip.StatusException(status); status = InteropMethods.Gdip.GdipImageForceValidation(new HandleRef(null, bitmap)); if (status != InteropMethods.Gdip.Ok) { InteropMethods.Gdip.GdipDisposeImage(new HandleRef(null, bitmap)); throw InteropMethods.Gdip.StatusException(status); } SetNativeImage(bitmap); EnsureSave(this, null, stream); } private void GetGifStreamFromPack(Uri uri) { try { StreamResourceInfo streamInfo; if (!uri.IsAbsoluteUri) { streamInfo = Application.GetContentStream(uri) ?? Application.GetResourceStream(uri); } else { if (uri.GetLeftPart(UriPartial.Authority).Contains("siteoforigin")) { streamInfo = Application.GetRemoteStream(uri); } else { streamInfo = Application.GetContentStream(uri) ?? Application.GetResourceStream(uri); } } if (streamInfo == null) { throw new FileNotFoundException("Resource not found.", uri.ToString()); } CreateSourceFromStream(streamInfo.Stream); } catch { // ignored } } private void SwitchToCommonImage() { if (Source == null && Uri != null) { SetCurrentValue(SourceProperty, new BitmapImage(Uri)); } } [ResourceExposure(ResourceScope.None)] [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)] internal static void EnsureSave(GifImage image, string filename, Stream dataStream) { if (!image.RawGuid.Equals(GifGuid) && !image.RawGuid.Equals(GifSingleFrameGuid)) { image.SwitchToCommonImage(); return; } var animatedGif = false; var dimensions = image.FrameDimensionsList; if (dimensions.Select(guid => new GifFrameDimension(guid)).Contains(GifFrameDimension.Time)) { animatedGif = image.GetFrameCount(GifFrameDimension.Time) > 1; } if (!animatedGif) { image.SwitchToCommonImage(); return; } try { Stream created = null; long lastPos = 0; if (dataStream != null) { lastPos = dataStream.Position; dataStream.Position = 0; } try { if (dataStream == null) { created = dataStream = File.OpenRead(filename); } image._rawData = new byte[(int) dataStream.Length]; dataStream.Read(image._rawData, 0, (int) dataStream.Length); } finally { if (created != null) { created.Close(); } else { // ReSharper disable once PossibleNullReferenceException dataStream.Position = lastPos; } } } // possible exceptions for reading the filename catch (UnauthorizedAccessException) { } catch (DirectoryNotFoundException) { } catch (IOException) { } // possible exceptions for setting/getting the position inside dataStream catch (NotSupportedException) { } catch (ObjectDisposedException) { } // possible exception when reading stuff into dataStream catch (ArgumentException) { } } internal void SetNativeImage(IntPtr handle) { if (handle == IntPtr.Zero) throw new ArgumentException("NativeHandle0"); NativeImage = handle; } internal Guid RawGuid { get { var guid = new Guid(); var status = InteropMethods.Gdip.GdipGetImageRawFormat(new HandleRef(this, NativeImage), ref guid); if (status != InteropMethods.Gdip.Ok) throw InteropMethods.Gdip.StatusException(status); return guid; } } internal void StartAnimate() { ImageAnimator.Animate(this, OnFrameChanged); _isStart = true; } internal void StopAnimate() { ImageAnimator.StopAnimate(this, OnFrameChanged); _isStart = false; } internal Guid[] FrameDimensionsList { get { var status = InteropMethods.Gdip.GdipImageGetFrameDimensionsCount(new HandleRef(this, NativeImage), out var count); if (status != InteropMethods.Gdip.Ok) { throw InteropMethods.Gdip.StatusException(status); } if (count <= 0) { return new Guid[0]; } var size = Marshal.SizeOf(typeof(Guid)); var buffer = Marshal.AllocHGlobal(checked(size * count)); if (buffer == IntPtr.Zero) { throw InteropMethods.Gdip.StatusException(InteropMethods.Gdip.OutOfMemory); } status = InteropMethods.Gdip.GdipImageGetFrameDimensionsList(new HandleRef(this, NativeImage), buffer, count); if (status != InteropMethods.Gdip.Ok) { Marshal.FreeHGlobal(buffer); throw InteropMethods.Gdip.StatusException(status); } var guids = new Guid[count]; try { for (var i = 0; i < count; i++) { guids[i] = (Guid) InteropMethods.PtrToStructure((IntPtr) ((long) buffer + size * i), typeof(Guid)); } } finally { Marshal.FreeHGlobal(buffer); } return guids; } } internal int GetFrameCount(GifFrameDimension dimension) { var count = new[] { 0 }; var dimensionId = dimension.Guid; var status = InteropMethods.Gdip.GdipImageGetFrameCount(new HandleRef(this, NativeImage), ref dimensionId, count); if (status != InteropMethods.Gdip.Ok) throw InteropMethods.Gdip.StatusException(status); return count[0]; } internal GifPropertyItem GetPropertyItem(int propid) { GifPropertyItem propitem; var status = InteropMethods.Gdip.GdipGetPropertyItemSize(new HandleRef(this, NativeImage), propid, out var size); if (status != InteropMethods.Gdip.Ok) throw InteropMethods.Gdip.StatusException(status); if (size == 0) return null; var propdata = Marshal.AllocHGlobal(size); if (propdata == IntPtr.Zero) throw InteropMethods.Gdip.StatusException(InteropMethods.Gdip.OutOfMemory); status = InteropMethods.Gdip.GdipGetPropertyItem(new HandleRef(this, NativeImage), propid, size, propdata); try { if (status != InteropMethods.Gdip.Ok) { throw InteropMethods.Gdip.StatusException(status); } propitem = GifPropertyItemInternal.ConvertFromMemory(propdata, 1)[0]; } finally { Marshal.FreeHGlobal(propdata); } return propitem; } internal int SelectActiveFrame(GifFrameDimension dimension, int frameIndex) { var count = new[] { 0 }; var dimensionId = dimension.Guid; var status = InteropMethods.Gdip.GdipImageSelectActiveFrame(new HandleRef(this, NativeImage), ref dimensionId, frameIndex); if (status != InteropMethods.Gdip.Ok) throw InteropMethods.Gdip.StatusException(status); return count[0]; } [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] [EditorBrowsable(EditorBrowsableState.Advanced)] [ResourceExposure(ResourceScope.Machine)] [ResourceConsumption(ResourceScope.Machine)] internal IntPtr GetHbitmap() => GetHbitmap(Colors.LightGray); [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] [EditorBrowsable(EditorBrowsableState.Advanced)] [ResourceExposure(ResourceScope.Machine)] [ResourceConsumption(ResourceScope.Machine)] internal IntPtr GetHbitmap(Color background) { var status = InteropMethods.Gdip.GdipCreateHBITMAPFromBitmap(new HandleRef(this, NativeImage), out var hBitmap, ColorHelper.ToWin32(background)); if (status == 2 && (Width >= short.MaxValue || Height >= short.MaxValue)) { throw new ArgumentException("GdiplusInvalidSize"); } if (status != InteropMethods.Gdip.Ok && NativeImage != IntPtr.Zero) { throw InteropMethods.Gdip.StatusException(status); } return hBitmap; } private void GetBitmapSource() { var handle = IntPtr.Zero; try { handle = GetHbitmap(); Source = Imaging.CreateBitmapSourceFromHBitmap(handle, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } catch { // ignored } finally { if (handle != IntPtr.Zero) { InteropMethods.DeleteObject(handle); } } } private void OnFrameChanged(object sender, EventArgs e) { Dispatcher.BeginInvoke(new Action(() => { ImageAnimator.UpdateFrames(); Source?.Freeze(); GetBitmapSource(); InvalidateVisual(); })); } } }