Création d’un lecteur GIF avec WIC et Direct2D – Partie 1

directX
Dans WinRT il n’a rien de fournit « nativement » pour pouvoir afficher des images au format GIF. Il y a une bidouille avec le webbrowser, mais je trouve que cette solution a beaucoup d’inconvénients.

Les deux principaux problèmes de la solution avec le webbrowser sont, premièrement, que ce dernier est un contrôle particulier qui ne fait pas parti de l’arbre visuel XAML, donc impossible de lui appliquer des transformations. Ensuite il est du coup plus difficile de positionner l’image exactement comme on le souhaite.

Pour répondre à ces problématiques, j’ai donc décidé de faire un composant basé sur WIC pour décoder l’image, et Direct2D pour le rendu. Le tout est englobé dans un contrôle XAML pour faciliter son usage.

Ce composant est disponible pour les projets Windows Phone Store app 8.1 et Windows 8.1. Pour Windows Phone, il n’était pas possible de le faire avant puisque c’est la version 8.1 qui apporte le support de WIC et Direct2D.

Cet article ne parlera que de l’aspect rendu et décodage du composant, mais vous pouvez consulter les sources de l’ensemble de celui-ci sur codeplex ici. Pour utiliser le composant dans l’une de vos applications vous pouvez l’installer depuis NuGet.

Architecture de GIFPlayer

Voici le schéma d’architecture de GIFPlayer.
GIFPlayer

Comme on le voit sur ce schéma, le composant se décompose en trois grandes parties :

  • Le décodeur : basé sur WIC il permet de lire les différentes informations portées par le format de l’image. Il utilise également Direct2D pour créer le bitmap du frame.
  • Le moteur de rendu : Grâce aux informations fournis par le décodeur, le moteur de rendu a la tâche d’effectuer le rendu des différents frames. Il réalise cette tâche grâce à Direct2D.
  • Le contrôle XAML : Il englobe les appels au moteur de rendu, pour faciliter l’intégration dans une page XAML.

Création du contexte Direct2D

Avant de rentrer dans le vif du sujet il est important de commencer par l’initialisation des différents objets DirectX que l’on aura besoin d’utiliser avec notre composant.

Voici ce que ça donne :

D2DManager^ D2DManager::m_instance;

D2DManager::D2DManager()
{
	D2D1_FACTORY_OPTIONS options;
	ZeroMemory(&options, sizeof(D2D1_FACTORY_OPTIONS));

#if defined(_DEBUG)
	options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
#endif

	ThrowIfFailed(
		D2D1CreateFactory(
		D2D1_FACTORY_TYPE_MULTI_THREADED,
		__uuidof(ID2D1Factory2),
		&options,
		&m_d2dFactory
		)
		);

	ThrowIfFailed(
		CoCreateInstance(
		CLSID_WICImagingFactory2,
		nullptr,
		CLSCTX_INPROC_SERVER,
		IID_PPV_ARGS(&m_wicFactory)
		)
		);

	UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;

#if defined(_DEBUG)
	creationFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

	D3D_FEATURE_LEVEL featureLevels[] =
	{
		D3D_FEATURE_LEVEL_11_1,
		D3D_FEATURE_LEVEL_11_0,
		D3D_FEATURE_LEVEL_10_1,
		D3D_FEATURE_LEVEL_10_0,
		D3D_FEATURE_LEVEL_9_3,
		D3D_FEATURE_LEVEL_9_2,
		D3D_FEATURE_LEVEL_9_1
	};

	ComPtr<ID3D11Device> device;
	ComPtr<ID3D11DeviceContext> context;
	D3D_FEATURE_LEVEL featureLevel;

	ThrowIfFailed(
		D3D11CreateDevice(
		nullptr,
		D3D_DRIVER_TYPE_HARDWARE,
		0,
		creationFlags,
		featureLevels,
		ARRAYSIZE(featureLevels),
		D3D11_SDK_VERSION,
		&device,
		&featureLevel,
		&context
		)
		);

	ThrowIfFailed(
		device.As(&m_dxgiDevice)
		);

	m_dxgiDevice->SetMaximumFrameLatency(1);

	ThrowIfFailed(
		m_d2dFactory->CreateDevice(m_dxgiDevice.Get(), &m_d2dDevice)
		);

	ThrowIfFailed(
		m_d2dDevice->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_ENABLE_MULTITHREADED_OPTIMIZATIONS, &m_d2dDeviceContext)
		);

	if (!Windows::ApplicationModel::DesignMode::DesignModeEnabled)
	Application::Current->Suspending +=	ref new SuspendingEventHandler(this, &D2DManager::App_Suspending);
}

void D2DManager::App_Suspending(Object^ sender, SuspendingEventArgs^ e)
{
	m_dxgiDevice->Trim();
}

D2DManager^ D2DManager::GetInstance()
{
	if (m_instance == nullptr)
		m_instance = ref new D2DManager();

	return m_instance;
}

Rien de compliquer ici, c’est du classique Direct2D.

Le décodeur

Le rôle de cet objet est simple :

  • Lire les différentes meta données du fichier et les stocker.
  • Créer un bitmap pour le frame correspondant à l’index demandé.

Puisque le décodeur est spécifique pour chaque fichier, il prend en paramètre de constructeur le nom de fichier.

Dans l’article on en restera aux fichiers locaux, mais le composant lui est capable de récupérer un fichier en http.

Voici à quoi ressemble donc notre constructeur :

GIFDecoder::GIFDecoder(const wchar_t* fileName) : m_background_color(0, 0, 0)
{
	ThrowIfFailed(
		D2DManager::GetInstance()->GetWICImagingFactory()->CreateDecoderFromFilename(
		fileName,
		NULL,
		GENERIC_READ,
		WICDecodeMetadataCacheOnLoad,
		m_decoder.GetAddressOf()
		));

	ReadMetadata();
}

La fonction helper ThrowIfFailed permet d’envoyer une exception dans le cas où le HRESULT n’est pas SUCCEEDED.

On crée ensuite notre décodeur WIC grâce à l’objet IWICImagingFactory2 fournit par notre D2DManager. Les paramètres à fournir à cette méthode parlent d’eux même.

Pour finir on appelle la méthode ReadMetadata qui permet de lire les métadonnées globales du fichier.

Lecture des métadonnées globales

Dans cette méthode ReadMetadata, on commence par lire le nombre de frames grâce à :

ThrowIfFailed(m_decoder->GetFrameCount(&m_frames_count));

Pour pouvoir lire les métadonnées, il faut récupérer un objet IWICMetadataQueryReader fournit par le décodeur :

ThrowIfFailed(m_decoder->GetMetadataQueryReader(&pMetadataQueryReader));

Pour lire les métadonnées, j’ai fait une méthode helper pour chaque type de données qui ressemble à ça :

HRESULT ReadUIntegerMetadata(IWICMetadataQueryReader* reader, LPCWSTR metadataName, UINT &value)
{
	PROPVARIANT propValue;
	PropVariantInit(&propValue);

	ThrowIfFailed(reader->GetMetadataByName(metadataName, &propValue));

	ThrowIfFailed(propValue.vt == VT_UI2 ? S_OK : E_FAIL);

	value = static_cast<UINT>(propValue.uiVal);

	PropVariantClear(&propValue);
	return S_OK;
}

L’objet IWICMetadataQueryReader nous renvoi les données grâce à un objet PROPVARIANT, on en initialise donc un. Puis on récupère la métadonnée grâce à son nom.

On vérifie ensuite que le type de la donnée lue est bien celle souhaitée, et on met à jour la référence passée en paramètre avec cette valeur.

Maintenant dans la méthode ReadMetadata on peut lire les métadonnées comme ceci :

ThrowIfFailed(ReadUIntegerMetadata(pMetadataQueryReader.Get(), L"/logscrdesc/Width", m_width));
ThrowIfFailed(ReadUIntegerMetadata(pMetadataQueryReader.Get(), L"/logscrdesc/Height", m_height));

J’ai mi la taille de l’image en exemple, mais vous pourrez retrouver toutes les métadonnées fournit par le format GIF ici : http://www.pixcl.com/GIF_MetaData_Fields.htm

Lecture des métadonnées par frame

Pour demander les métadonnées d’un frame au décodeur j’ai décidé d’exposer la méthode suivante :

HRESULT GetFrameAndMetadata(UINT uFrameIndex, UINT width, UINT height, FrameMetadata &data, ID2D1Bitmap1* &frame);

On fournit à la méthode le numéro du frame pour lequel on souhaite récupérer les informations, ainsi que la taille du frame que l’on souhaite récupérer. Elle nous renvoie les métadonnées et le frame à la taille demandée.

FrameMetadata est une structure où sont stockées les données du frame :

struct FrameMetadata{
	UINT frameIndex  = UINT_MAX;
	D2D1_RECT_F rect = D2D1::RectF();
	UINT delay = 0;
	UINT disposal = 0;
	bool transparent = false;
};

Dans la méthode GetFrameAndMetadata la partie pour lire les métadonnées ressemble beaucoup à la méthode ReadMetadata, mais le IWICMetadataQueryReader ne sera pas créé directement avec le IWICBitmapDecoder. Il sera créé à partir d’un IWICBitmapFrameDecode lui-même créé à partir du IWICBitmapDecoder.

On a donc :

data = FrameMetadata();
IWICBitmapFrameDecode* pWicFrame = nullptr;
IWICMetadataQueryReader* pFrameMetadataQueryReader = nullptr;

// Retrieve the current frame
ThrowIfFailed(m_decoder->GetFrame(uFrameIndex, &pWicFrame));

data.frameIndex = uFrameIndex;

// Get Metadata Query Reader from the frame
ThrowIfFailed(pWicFrame->GetMetadataQueryReader(&pFrameMetadataQueryReader));

// Get the Metadata for the current frame

// Transparency
ThrowIfFailed(ReadBoolMetadata(pFrameMetadataQueryReader, L"/grctlext/TransparencyFlag", data.transparent));

// Left
ThrowIfFailed(ReadFloatMetadata(pFrameMetadataQueryReader, L"/imgdesc/Left", data.rect.left));

// Top
ThrowIfFailed(ReadFloatMetadata(pFrameMetadataQueryReader, L"/imgdesc/Top", data.rect.top));

// Width
ThrowIfFailed(ReadFloatMetadata(pFrameMetadataQueryReader, L"/imgdesc/Width", data.rect.right));

// Height
ThrowIfFailed(ReadFloatMetadata(pFrameMetadataQueryReader, L"/imgdesc/Height", data.rect.bottom));

// Frame delay
UINT baseDelay;
data.delay = 0;

ThrowIfFailed(ReadUIntegerMetadata(pFrameMetadataQueryReader, L"/grctlext/Delay", baseDelay));
ThrowIfFailed(UIntMult(baseDelay, 10, &data.delay));

if (data.delay <= 0)
	data.delay = 90;

// Dispose method
ThrowIfFailed(ReadUCharMetadata(pFrameMetadataQueryReader, L"/grctlext/Disposal", data.disposal));

Récupération du frame

Toujours dans la méthode GetFrameAndMetadata voyons maintenant comment récupérer le frame.

Il faut tout d’abord créer un IWICFormatConverter qui va nous permettre de convertir le bitmap au format WIC à celui qui sera utilisé par Direct2D pour le rendu après :

IWICFormatConverter* pConverter = nullptr;

ThrowIfFailed(D2DManager::GetInstance()->GetWICImagingFactory()->CreateFormatConverter(&pConverter));

ThrowIfFailed(pConverter->Initialize(
								pWicFrame,
								GUID_WICPixelFormat32bppPBGRA,
								WICBitmapDitherTypeNone,
								nullptr,
								1.f,
								WICBitmapPaletteTypeFixedWebPalette));

Ensuite on a plus qu’à récupérer temporairement le bitmap au ratio 1:1 pour le redessiner à la taille demandée :

ID2D1Bitmap1 *tempBitmap;

// Read the frame into temporary bitmap to save in cache scaled one later
ThrowIfFailed(D2DManager::GetInstance()->GetD2DDeviceContext()->CreateBitmapFromWicBitmap(
																	pConverter,
																	NULL,
																	&tempBitmap));

// Get current dpi
float dpiX, dpiY;
tempBitmap->GetDpi(&dpiX, &dpiY);

// Set properties of the scaled bitmap
D2D1_BITMAP_PROPERTIES1 properties;
ZeroMemory(&properties, sizeof(D2D1_BITMAP_PROPERTIES));
properties.pixelFormat = tempBitmap->GetPixelFormat();
properties.dpiX = dpiX;
properties.dpiY = dpiY;
tempBitmap->GetColorContext(&properties.colorContext);
properties.bitmapOptions = D2D1_BITMAP_OPTIONS_TARGET;

// Get the D2D Context
ID2D1DeviceContext1* context = D2DManager::GetInstance()->GetD2DDeviceContext();

// Create the bitmap that will be store into the cache
D2D1_SIZE_U size = D2D1::SizeU(width, height);
ThrowIfFailed(context->CreateBitmap(size, (const void*)0, 0, properties, &frame));

// Define this bitmap as the render target
context->SetTarget(frame);

// Draw the scaled version into the bitmap that will be store into the cache
context->BeginDraw();

// Compute ratio between actual bitmap and scaled one
double widthRatio = (double)width / (double)m_width;
double heightRatio = (double)height / (double)m_height;

// Get the good rectangle of the frame and compute where to draw it in the final bitmap
D2D1_RECT_F destinationRect = D2D1::RectF(static_cast<float>(data.rect.left * widthRatio),
											static_cast<float>(data.rect.top * heightRatio),
											static_cast<float>(data.rect.right * widthRatio),
											static_cast<float>(data.rect.bottom * heightRatio));

D2D1_RECT_F sourceRect = D2D1::RectF(0.0f, 0.0f, static_cast<float>(data.rect.right - data.rect.left), static_cast<float>(data.rect.bottom - data.rect.top));

context->DrawBitmap(tempBitmap, &destinationRect, 1, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, &sourceRect);
ThrowIfFailed(context->EndDraw());

// Release the temporary bitmap
if (tempBitmap)
{
	tempBitmap->Release();
	tempBitmap = nullptr;
}

Nous avons fini avec le décodeur, il remplit maintenant le rôle que l’on souhaitait.

Conclusion

Pour cette première partie nous en resterons au décodeur. Nous avons donc vu comment lire les métadonnées du fichier et créer les frames en mémoire, pour en préparer la l’affichage. Je vous donne rendez-vous dans quelques jours pour la deuxième partie dans laquelle on verra comment gérer l’affichage, et l’englober dans un contrôle XAML.

A bientôt !

Nombre de vue : 117

COMMENTAIRES 1 commentaire

  1. […] la première partie de cet article, qui a pour but d’expliquer comment créer un composant pour afficher des […]

AJOUTER UN COMMENTAIRE