Création d’un lecteur GIF avec WIC et Direct2D – Partie 2
Dans la première partie de cet article, qui a pour but d’expliquer comment créer un composant pour afficher des images/animations au format GIF dans une application universelle, nous avions vu comment initialiser les composants DirectX(DXGI et Direct2D dans notre cas) et WIC.
Puis nous avions vu comment lire les métadonnées du GIF en créant une classe GIFDecoder. Nous allons maintenant voir dans cette seconde et dernière partie, comment rendre les frames fournis par le GIFDecoder à l’écran, grâce au GIFRenderer, puis comment l’encapsuler dans un composant XAML sobrement nommé GIF.
Avant de commencer je vous rappelle que le code de ce composant est disponible sur codeplex ici. N’hésitez pas à aller voir, puisque je vous le rappelle, certaines parties ne sont pas abordées dans ces articles.
Rendu du GIF
Commençons donc cette seconde partie, avec la création de la classe GIFRenderer. Cette classe aura pour but d’afficher les frames qui sont décodés par le GIFDecoder. Pour éviter d’aller chercher et recalculer les frames à chaque boucle de l’animation, j’ai ajouté une mise en cache. Je ne détaillerai pas cette partie ici, mais c’est un simple tableau de bitmap de la taille de l’animation. A chaque fois qu’un frame est demandé à l’affichage, s’il n’est pas présent dans le tableau, je l’y ajoute.
Avant de rendre un frame, il faut d’abord savoir où on va vouloir l’afficher. Pour garder une grande souplesse pour la suite, j’ai décidé de choisir une SurfaceImageSource comme destination du rendu. Comme SurfaceImageSource hérite de ImageSource, il sera facile ensuite de l’intégrer dans le rendu XAML. Il nous faut donc écrire une méthode pour créer cette surface :
SurfaceImageSource^ GIFRenderer::CreateSurfaceImageSource()
{
if (m_surfaceImageSource == nullptr)
{
// Création de la surface de rendu avec la bonne taille et le support de la transparence
m_surfaceImageSource = ref new SurfaceImageSource(static_cast<UINT>(m_surfaceSize.Width), static_cast<UINT>(m_surfaceSize.Height), false);
// Récupération de l'objet SurfaceImageSourceNativeWithD2D qui pointe sur cette surface
IInspectable* inspectable = (IInspectable*) reinterpret_cast<IInspectable*>(m_surfaceImageSource);
inspectable->QueryInterface(__uuidof(ISurfaceImageSourceNativeWithD2D),(void **)&m_nativeImageSource);
// Créer un lien entre notre SurfaceImageSourceNativeWIthD2D et notre D2DDevice, pour pouvoir écrire dedans ultérieurement
m_nativeImageSource->SetDevice(D2DManager::GetInstance()->GetD2DDevice());
}
return m_surfaceImageSource;
}
Cette méthode est relativement simple, on crée d’abord la SurfaceImageSource avec la bonne taille, et le support de la transparence. Ensuite de cette SurfaceImageSource, on récupère une ISurfaceImageSourceNativeWithD2D, qui sera utilisée plus tard pour écrire dans la surface avec Direct2D. Et pour cela justement il faut définir le D2DDevice de celle-ci.
Nous avons maintenant la destination de notre rendu. Avant de passer au traitement, il faut récupérer une source. Pour simplifier cette tâche j’ai décidé d’ajouter directement une référence à GIFDecoder dans le GIFRenderer, qui est initialisé dans le constructeur.
Il est temps de passer dans la méthode principale qui effectue le rendu. Elle prend en paramètre l’index du frame à afficher et renvoi le délai à attendre avant le prochain frame :
UINT GIFRenderer::DrawFrame(UINT frameIndex)
Pour rappel, dans le composant final il y a un mécanisme de mise en cache que je ne détaillerai pas ici, mais je vous invite à aller voir sur le codeplex.
On commence par récupérer notre frame et ces informations grâce au GIFDecoder :
FrameMetadata current_frame_data = FrameMetadata();
ID2D1Bitmap1* current_frame = nullptr;
ThrowIfFailed(
m_gifDecoder->GetFrameAndMetadata(frameIndex,
static_cast<UINT>(m_surfaceSize.Width),
static_cast<UINT>(m_surfaceSize.Height),
current_frame_data, current_frame)
);
Pour effectuer le rendu on initialise la mise à jour sur notre SurfaceImageSource:
RECT drawRect = { 0, 0, static_cast<LONG>(m_surfaceSize.Width), static_cast<LONG>(m_surfaceSize.Height) };
POINT offset = { 0, 0 };
ComPtr<IDXGISurface> surface;
ThrowIfFailed(
m_nativeImageSource->BeginDraw(drawRect, IID_IDXGISurface, &surface, &offset)
);
On définit la taille de la surface à rendre. Et la méthode BeginDraw nous renvoi un pointeur sur la surface DXGI, et également le décalage qu’il faudra appliquer à notre rendu.
Ensuite nous pouvons définir la taille de la surface avec ce décalage et créer un bitmap correspondant à la surface, pour pouvoir écrire dedans :
D2D1_RECT_F destRect = D2D1::RectF(offset.x, offset.y, m_surfaceSize.Width + offset.x, m_surfaceSize.Height + offset.y);
ThrowIfFailed(
d2dContext->CreateBitmapFromDxgiSurface(
surface.Get(),
nullptr,
&m_targetBitmap
)
);
On utilise donc ce bitmap comme rendertarget :
d2dContext->SetTarget(m_targetBitmap.Get());
Puis vient l’initialisation du contexte D2D. On restreint l’accès de la surface pour limiter uniquement à la zone à laquelle on va accéder. Et on remplit avec une couleur transparente.
d2dContext->BeginDraw();
d2dContext->PushAxisAlignedClip(&destRect, D2D1_ANTIALIAS_MODE_ALIASED);
d2dContext->Clear(D2D1::ColorF(0, 0, 0, 0));
Nous arrivons à la partie la plus importante de cette méthode, qui est le rendu du frame. Dans cette partie il y a deux cas pour l’instant qui sont gérés par le composant. En fait, le format GIF implémente des « méthodes de dispositions » qui permet de dire globalement comment un frame doit se superposer aux anciens. Pour l’instant je ne gère que les cas suivants (les autres seront gérés dans une version ultérieure) :
• Le frame est indépendant (on n’affiche seulement celui-là)
• On garde tout l’historique de l’animation, si le frame est transparent (le frame 3 s’affiche par-dessus le 2 qui s’affiche par-dessus le 1). Un peu comme un système de calque.
Pour le premier cas j’ai simplement fait un DrawBitmap, et pour le deuxième j’utilise l’effet intégré Composite, en lui ajoutant chaque frame en entrée :
if (current_frame_data.transparent && current_frame_data.disposal <= 1)
{
// On ajoute les frames en entrée de l'effet Composite
for (UINT i = 0; i <= m_current_index; i++)
{
// On va chercher les anciens frames dans le cache
m_compositeEffect->SetInput(i, m_cached_frames[i]);
m_compositeEffect->SetInputCount(m_current_index + 1);
}
// On affiche le résultat de l'effet Composite
D2D1_POINT_2F targetOffset = D2D1::Point2F(static_cast<float>(offset.x), static_cast<float>(offset.y));
d2dContext->DrawImage(m_compositeEffect.Get(), targetOffset, D2D1_INTERPOLATION_MODE_LINEAR);
}
else
{
// On affiche tout simplement le frame
D2D1_RECT_F sourceRect = D2D1::RectF(0, 0, m_surfaceSize.Width, m_surfaceSize.Height);
d2dContext->DrawBitmap(
current_frame,
&destRect, 1.0f, D2D1_INTERPOLATION_MODE_LINEAR, &sourceRect);
}
On termine le rendu :
d2dContext->PopAxisAlignedClip();
d2dContext->EndDraw();
m_nativeImageSource->EndDraw();
Et on finit la méthode en renvoyant le délai du frame courant :
return current_frame_data.delay;
J’ai également fait le choix de rendre le GIFRenderer capable de naviguer entre les frames. Il expose donc les méthodes suivantes :
UINT DrawCurrentFrameAndGoNext();
UINT DrawCurrentFrameAndGoPrevious();
UINT DrawNextFrame();
UINT DrawPreviousFrame();
Ces méthodes permettent de déplacer le curseur de l’index courant, et d’appeler ensuite (ou avant) la méthode DrawFrame.
Le GIFRenderer est maintenant fin prêt. Nous pouvons passer au contrôle XAML.
Contrôle xaml GIF
Nous voilà maintenant dans la dernière partie de ces deux articles sur la création du composant qui permet d’afficher une image/animation au format GIF dans une application universelle. Juste avec les parties précédentes on pourrait déjà le faire. Pour cela il suffirait par exemple de mettre un contrôle Image, dans l’arbre XAML et de définir l’ImageSource de ce contrôle avec le SurfaceImageSource d’un GIFRenderer. Il faudrait par contre gérer l’animation à la main, car notre GIFRenderer n’affiche qu’un frame à la fois.
Le rôle de la dernière brique, le contrôle XAML, est donc de simplifier son intégration justement. Il va permettre de ne plus avoir à gérer ça (mais de garder la main dessus quand même si nécessaire).
Il nous faut donc commencer par déclarer notre classe en héritant de Control :
public ref class GIF sealed : public Windows::UI::Xaml::Controls::Control
Dans le constructeur nous allons définir la clé de ressource de notre contrôle :
GIF::GIF()
{
DefaultStyleKey = "GIFPlayer.GIF";
}
Ce contrôle personnalisé aura besoin d’un template, il faudra donc placer un fichier Generic.xaml dans le projet qui consomme le contrôle (le package nuget le fait pour vous). J’ai choisi d’utiliser un ImageBrush comme ceci :
<ResourceDictionary
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:GIFPlayer">
<Style TargetType="local:GIF">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:GIF">
<Border Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}">
<Border.Background>
<ImageBrush x:Name="imgBrush" />
</Border.Background>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
On vient de définir le visuel du composant, maintenant nous allons pouvoir définir son comportement. Nous allons donc commencer par définir deux dependency properties : Source, IsAnimating.
Pour rappel voici un exemple de déclaration d’une depency property (sans callback dans notre cas) en C++ :
Dans le header :
static Windows::UI::Xaml::DependencyProperty^ m_sourceProperty;
static property Windows::UI::Xaml::DependencyProperty^ SourceProperty
{
Windows::UI::Xaml::DependencyProperty^ get() { return m_sourceProperty; }
}
property Platform::String^ Source{
Platform::String^ get() { return (Platform::String^)GetValue(SourceProperty); }
void set(Platform::String^ value){ SetValue(SourceProperty, value); }
};
Et dans le .cpp :
DependencyProperty^ GIF::m_sourceProperty = DependencyProperty::Register("Source",
String::typeid,
GIF::typeid,
ref new PropertyMetadata(nullptr, nullptr)
);
Je vous invite à aller voir le code également dans la méthode OnLoaded du contrôle.
C’est ici que le composant charge la source. Je ne détaillerai pas cette partie, ce qu’il faut retenir dedans, c’est :
// Initialisation du Décodeur
m_gifDecoder.reset(new GIFDecoder(fileName->Data()));
// Création du renderer et initialisation de sa surface
m_gifRenderer = ref new GIFRenderer(m_gifDecoder.get());
m_gifRenderer->SetSurfaceSize(static_cast<UINT>(Width * scaleFactor), static_cast<UINT>(Height * scaleFactor));
// On récupère l'ImageBrush et on affecte se source avec la surface de notre renderer
m_imgBrush = (ImageBrush^)GetTemplateChild(L"imgBrush");
m_imgBrush->ImageSource = m_gifRenderer->GetSurfaceImageSource();
Enfin pour gérer l’animation, le contrôle s’abonne à :
CompositionTarget::Rendering += ref new Windows::Foundation::EventHandler<Platform::Object ^>(this, &GIFPlayer::GIF::OnRendering);
Dans la méthode OnRendering correspondante on retrouve :
// Si l'animation est activé
if (IsAnimating)
{
// Update du timer haute performance
m_timer->Tick([this](){
// Mise à jour du temps depuis le dernier frame
m_currentTime += m_timer->GetElapsedSeconds() * 1000.0f;
// Si le délai du dernier frame est écoulé on reset les temps
// Et on affiche le frame suivant
if (m_currentTime >= m_nextFrameRemainingTime)
{
m_currentTime = 0.0f;
m_nextFrameRemainingTime = m_gifRenderer->DrawCurrentFrameAndGoNext();
m_timer->ResetElapsedTime();
}
});
}
Les principales parties de ce composant ont maintenant été traités.
Pour l’utiliser maintenant, rien de plus simple, il suffit d’ajouter le package nuget du même nom à votre projet. Ensuite dans votre page ajouter le namespace suivant :
xmlns:gif="using:GIFPlayer"
Et enfin, là où vous le souhaitez dans votre XAML :
<gif:GIF IsAnimating="True"
Source="https://media.giphy.com/media/yv8iBjSfZs9IA/giphy.gif" />
Facile non ? De plus, maintenant que le contrôle est intégré à l’arbre visuel, il est possible de faire des chose comme jouer sur son opacité, ou lui appliquer une projection, comme par exemple :
<gif:GIF x:Name="myGifPlayer" Opacity="0.5" Source="https://media.giphy.com/media/yv8iBjSfZs9IA/giphy.gif">
<gif:GIF.Projection>
<PlaneProjection RotationY="45" />
</gif:GIF.Projection>
</gif:GIF>
Conclusion
J’espère que ces deux articles vous auront aidés à mieux comprendre comment ce composant fonctionne. Je vous le rappelle n’hésitez pas à aller fouiller dans les sources pour en voir plus.
J’ajouterai des fonctionnalités, comme la création de GIF, dans une version ultérieure. Si vous avez des retours sur son utilisation, n’hésitez pas à me les faire parvenir via le projet codeplex : https://gifplayer.codeplex.com/
A bientôt.