Skip to content

Commit

Permalink
Add Support for (animated) WebP image format
Browse files Browse the repository at this point in the history
  • Loading branch information
thomas694 committed Nov 5, 2022
1 parent badcec1 commit cf416c4
Show file tree
Hide file tree
Showing 16 changed files with 2,820 additions and 17 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,7 @@ FodyWeavers.xsd
# JetBrains Rider
.idea/
*.sln.iml

# project exceptions
!src/x86
!src/x64
7 changes: 4 additions & 3 deletions LICENSE 3RD PARTY
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
-----------------------------------------------------------------------------
MIT License
applies to:
- Microsoft.Xaml.Behaviors.Wpf, Copyright � Microsoft
MIT License
applies to:
- Microsoft.Xaml.Behaviors.Wpf, Copyright � Microsoft
- Wrapper for WebP format in C#, Copyright � Jose M. Pi�eiro and others
-----------------------------------------------------------------------------

Permission is hereby granted, free of charge, to any person obtaining a copy
Expand Down
313 changes: 313 additions & 0 deletions src/AnimatedWebpElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using WebPWrapper;
using static WebPWrapper.WebP;

namespace WpfImageViewer
{
public class AnimatedWebpElement : Canvas
{
// Create a collection of child visual objects.
private readonly VisualCollection _children;

private Bitmap _bitmap;
private BitmapSource _bitmapSource;
private bool _started;
private static System.Timers.Timer _timer = new System.Timers.Timer();

private WebP _webp = new WebP();
private List<FrameData> _frames;
private int _currentIndex = 0;

public delegate void FrameUpdatedEventHandler();

public AnimatedWebpElement()
{
_children = new VisualCollection(this)
{
CreateDrawingVisualImage()
};
_timer.Elapsed += OnFrameChanged;
}

// Register a custom routed event using the Bubble routing strategy.
public static readonly RoutedEvent MediaLoadedEvent = EventManager.RegisterRoutedEvent(
name: "MediaLoaded",
routingStrategy: RoutingStrategy.Bubble,
handlerType: typeof(RoutedEventHandler),
ownerType: typeof(AnimatedWebpElement));

// Provide CLR accessors for assigning an event handler.
public event RoutedEventHandler MediaLoaded
{
add { AddHandler(MediaLoadedEvent, value); }
remove { RemoveHandler(MediaLoadedEvent, value); }
}

private void RaiseMediaLoadedRoutedEvent()
{
// Create a RoutedEventArgs instance.
RoutedEventArgs routedEventArgs = new RoutedEventArgs(routedEvent: MediaLoadedEvent);

// Raise the event, which will bubble up through the element tree.
RaiseEvent(routedEventArgs);
}

public int NaturalImageWidth => (_frames.Count > 0) ? _frames[0].Bitmap.Width : 0;
public int NaturalImageHeight => (_frames.Count > 0) ? _frames[0].Bitmap.Height : 0;

public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register("Source", typeof(Uri), typeof(AnimatedWebpElement), new PropertyMetadata(OnSourcePropertyChanged));

public Uri Source
{
get => (Uri)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}

private static void OnSourcePropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
if ((bool)DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue) { return; }

AnimatedWebpElement control = source as AnimatedWebpElement;
Uri url = (Uri)e.NewValue;
control.StopAnimate();
control.AnimatedWebpElement_Loaded(control, null);
}

protected override void OnRender(DrawingContext drawingContext)
{
if (_bitmapSource != null)
{
drawingContext.DrawImage(_bitmapSource, new Rect(0, 0, ActualWidth, ActualHeight));
}
}

[DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeleteObject([In] IntPtr hObject);

protected override void OnInitialized(EventArgs e)
{
if (DesignerProperties.GetIsInDesignMode(this)) { return; }

base.OnInitialized(e);
Loaded += new RoutedEventHandler(AnimatedWebpElement_Loaded);
Unloaded += new RoutedEventHandler(AnimatedWebpElement_Unloaded);

Window wnd = GetWindow();
wnd.StateChanged += AnimatedWebpElement_WindowStateChanged;
wnd.SizeChanged += AnimatedWebpElement_WindowSizeChanged;
}

private Window GetWindow()
{
FrameworkElement el = this;
do
{
el = el.Parent as FrameworkElement;

} while (!(el is Window) && !el.GetType().Name.EndsWith("WindowInstance"));

return el as Window;
}

private void AnimatedWebpElement_WindowStateChanged(object sender, EventArgs e)
{
if (DesignerProperties.GetIsInDesignMode(this)) { return; }

if (GetWindow().WindowState == WindowState.Minimized)
{
StopAnimate();
}
else
{
StartAnimate();
}
}

private void AnimatedWebpElement_WindowSizeChanged(object sender, EventArgs e)
{
if (DesignerProperties.GetIsInDesignMode(this)) { return; }

if (_bitmapSource != null)
{
_bitmapSource = GetBitmapSource();

double factor = _bitmapSource.Width / _bitmapSource.Height;

double parentWidth;
double parentHeight;
if (sender.Equals(GetWindow()))
{
parentWidth = ((FrameworkElement)sender).ActualWidth;
parentHeight = ((FrameworkElement)sender).ActualHeight;
}
else
{
parentWidth = ((FrameworkElement)((FrameworkElement)sender).Parent).ActualWidth;
parentHeight = ((FrameworkElement)((FrameworkElement)sender).Parent).ActualHeight;
}

if (factor > 1)
{
Width = Math.Min(parentWidth, _bitmapSource.Width);
Height = Width / factor;
}
else
{
Height = Math.Min(parentHeight, _bitmapSource.Height);
Width = Height * factor;
}
Thickness tn = Margin;
if (parentHeight - Height > 0)
{
tn.Top = (parentHeight - Height) / 2;
}
if (parentWidth - Width > 0)
{
tn.Left = (parentWidth - Width) / 2;
}
//Margin = tn;
}
}

/// <summary>
/// Load the in the property Source specified image and start animation.
/// </summary>
void AnimatedWebpElement_Loaded(object sender, RoutedEventArgs e)
{
if (DesignerProperties.GetIsInDesignMode(this)) { return; }

if (!string.IsNullOrEmpty(Source?.ToString()))
{
_frames = new List<FrameData>(_webp.AnimLoad(Source.LocalPath));
_currentIndex = 0;
_bitmap = _frames[0].Bitmap;
_bitmapSource = GetBitmapSource();
if (_bitmapSource != null)
{
_bitmapSource.Freeze();
}

RaiseMediaLoadedRoutedEvent();

AnimatedWebpElement_WindowSizeChanged(this, EventArgs.Empty);

InvalidateVisual();

StartAnimate();
}
}

private void AnimatedWebpElement_Unloaded(object sender, RoutedEventArgs e)
{
StopAnimate();
}

private void OnFrameChanged(Object source, System.Timers.ElapsedEventArgs e)
{
_timer.Stop();

Dispatcher.BeginInvoke(DispatcherPriority.Normal, new FrameUpdatedEventHandler(FrameUpdatedCallback));
}

private void FrameUpdatedCallback()
{
_currentIndex = (_currentIndex + 1 < _frames.Count) ? _currentIndex + 1 : 0;

_bitmap = _frames[_currentIndex].Bitmap;
_bitmapSource = GetBitmapSource();
if (_bitmapSource != null)
{
_bitmapSource.Freeze();
}

InvalidateVisual();

SetTimer(_frames[_currentIndex].Duration);
}

private void SetTimer(int duration)
{
if (duration == 0) { return; }

_timer.Interval = duration;
_timer.Start();
}

private void StartAnimate()
{
if (_started) { return; }
_started = true;

SetTimer(_frames[_currentIndex].Duration);
}

private void StopAnimate()
{
if (!_started) { return; }
_started = false;

_timer.Stop();
}

private BitmapSource GetBitmapSource()
{
IntPtr handle = IntPtr.Zero;

try
{
handle = _bitmap.GetHbitmap();
_bitmapSource = Imaging.CreateBitmapSourceFromHBitmap(
handle, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
}
finally
{
if (handle != IntPtr.Zero)
{
DeleteObject(handle);
}
}

return _bitmapSource;
}

// Create a DrawingVisual that contains a rectangle.
private DrawingVisual CreateDrawingVisualImage()
{
DrawingVisual drawingVisual = new DrawingVisual();

// Retrieve the DrawingContext in order to create new drawing content.
DrawingContext drawingContext = drawingVisual.RenderOpen();

// Persist the drawing content.
drawingContext.Close();

return drawingVisual;
}

// Provide a required override for the VisualChildrenCount property.
protected override int VisualChildrenCount => _children.Count;

// Provide a required override for the GetVisualChild method.
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= _children.Count)
{
throw new ArgumentOutOfRangeException();
}

return _children[index];
}
}
}
2 changes: 1 addition & 1 deletion src/App.config
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<value>2</value>
</setting>
<setting name="IncludedFileExtensions" serializeAs="String">
<value>.bmp,.gif,.gifv,.jpeg,.jpg,.png,.tif,.tiff</value>
<value>.bmp,.gif,.gifv,.jpeg,.jpg,.png,.tif,.tiff,.webp</value>
</setting>
<setting name="MsgColor" serializeAs="String">
<value>Green</value>
Expand Down
36 changes: 36 additions & 0 deletions src/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:ei="http://schemas.microsoft.com/xaml/behaviors"
xmlns:local="clr-namespace:WpfImageViewer"
Title="MainWindow" WindowStyle="None" WindowState="Maximized" Loaded="Window1_Loaded">
<Grid x:Name="Grid1" MouseDown="Grid1_MouseDown" MouseWheel="Grid1_HandleMouseWheel">
<Border x:Name="Border1" ClipToBounds="True">
Expand All @@ -29,6 +30,38 @@
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchableAnim" Storyboard.TargetProperty="Visibility" Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="ShowAnim">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchableImage" Storyboard.TargetProperty="Visibility" Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchableMediaElement" Storyboard.TargetProperty="Visibility" Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchableAnim" Storyboard.TargetProperty="Visibility" Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
Expand All @@ -40,6 +73,9 @@
<Image Visibility="Visible" x:Name="SwitchableImage"
Source="{Binding FilenameImage, Converter={StaticResource NullImageConverter}, IsAsync=True}"
Stretch="Uniform" StretchDirection="DownOnly" />
<local:AnimatedWebpElement Visibility="Collapsed" x:Name="SwitchableAnim"
Source="{Binding FilenameAnim, Converter={StaticResource NullImageConverter}, IsAsync=True}"
MediaLoaded="SwitchableAnim_MediaLoaded" />
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding CurrentVisualState}" Value="{Binding CurrentVisualState}">
<ei:GoToStateAction StateName="{Binding CurrentVisualState}" />
Expand Down
Loading

0 comments on commit cf416c4

Please sign in to comment.