//-------------------------------------------------------------------------------------- // File: AnimatedGif.cpp // // Code for converting an animated GIF to a series of texture frames. // // References: // https://github.com/microsoft/Windows-classic-samples/tree/main/Samples/Win7Samples/multimedia/wic/wicanimatedgif // http://www.imagemagick.org/Usage/anim_basics/#dispose // // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. // // http://go.microsoft.com/fwlink/?LinkId=248926 //-------------------------------------------------------------------------------------- #pragma warning(push) #pragma warning(disable : 4005) #define WIN32_LEAN_AND_MEAN #define NOMINMAX #define NODRAWTEXT #define NOGDI #define NOMCX #define NOSERVICE #define NOHELP #pragma warning(pop) #include #include #include #include #include #include #include #include #pragma warning(disable : 4619 4616 26812) #include "DirectXTex.h" using namespace DirectX; using Microsoft::WRL::ComPtr; namespace { enum { DM_UNDEFINED = 0, DM_NONE = 1, DM_BACKGROUND = 2, DM_PREVIOUS = 3 }; void FillRectangle(const Image& img, const RECT& destRect, uint32_t color) { RECT clipped = { (destRect.left < 0) ? 0 : destRect.left, (destRect.top < 0) ? 0 : destRect.top, (destRect.right > static_cast(img.width)) ? static_cast(img.width) : destRect.right, (destRect.bottom > static_cast(img.height)) ? static_cast(img.height) : destRect.bottom }; auto ptr = reinterpret_cast(img.pixels + size_t(clipped.top) * img.rowPitch + size_t(clipped.left) * sizeof(uint32_t)); for (long y = clipped.top; y < clipped.bottom; ++y) { auto pixelPtr = reinterpret_cast(ptr); for (long x = clipped.left; x < clipped.right; ++x) { *pixelPtr++ = color; } ptr += img.rowPitch; } } void BlendRectangle(const Image& composed, const Image& raw, const RECT& destRect, uint32_t transparent) { RECT clipped = { (destRect.left < 0) ? 0 : destRect.left, (destRect.top < 0) ? 0 : destRect.top, (destRect.right > static_cast(composed.width)) ? static_cast(composed.width) : destRect.right, (destRect.bottom > static_cast(composed.height)) ? static_cast(composed.height) : destRect.bottom }; auto rawPtr = reinterpret_cast(raw.pixels); auto composedPtr = reinterpret_cast( composed.pixels + size_t(clipped.top) * composed.rowPitch + size_t(clipped.left) * sizeof(uint32_t)); for (long y = clipped.top; y < clipped.bottom; ++y) { auto srcPtr = reinterpret_cast(rawPtr); auto destPtr = reinterpret_cast(composedPtr); for (long x = clipped.left; x < clipped.right; ++x, ++srcPtr, ++destPtr) { if (transparent == *srcPtr) continue; *destPtr = *srcPtr; } rawPtr += raw.rowPitch; composedPtr += composed.rowPitch; } } } HRESULT LoadAnimatedGif(const wchar_t* szFile, std::vector>& loadedImages, bool usebgcolor) { bool iswic2; auto pWIC = GetWICFactory(iswic2); if (!pWIC) return E_NOINTERFACE; ComPtr decoder; HRESULT hr = pWIC->CreateDecoderFromFilename(szFile, nullptr, GENERIC_READ, WICDecodeMetadataCacheOnDemand, decoder.GetAddressOf()); if (FAILED(hr)) return hr; { GUID containerFormat; hr = decoder->GetContainerFormat(&containerFormat); if (FAILED(hr)) return hr; if (memcmp(&containerFormat, &GUID_ContainerFormatGif, sizeof(GUID)) != 0) { // This function only works for GIF return HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED); } } ComPtr metareader; hr = decoder->GetMetadataQueryReader(metareader.GetAddressOf()); if (FAILED(hr)) return hr; PROPVARIANT propValue; PropVariantInit(&propValue); // Get palette WICColor rgbColors[256] = {}; UINT actualColors = 0; { ComPtr palette; hr = pWIC->CreatePalette(palette.GetAddressOf()); if (FAILED(hr)) return hr; hr = decoder->CopyPalette(palette.Get()); if (FAILED(hr)) return hr; hr = palette->GetColors(static_cast(std::size(rgbColors)), rgbColors, &actualColors); if (FAILED(hr)) return hr; } // Get background color UINT bgColor = 0; if (usebgcolor) { // Most browsers just ignore the background color metadata and always use transparency hr = metareader->GetMetadataByName(L"/logscrdesc/GlobalColorTableFlag", &propValue); if (SUCCEEDED(hr)) { const bool hasTable = (propValue.vt == VT_BOOL && propValue.boolVal); PropVariantClear(&propValue); if (hasTable) { hr = metareader->GetMetadataByName(L"/logscrdesc/BackgroundColorIndex", &propValue); if (SUCCEEDED(hr)) { if (propValue.vt == VT_UI1) { const uint8_t index = propValue.bVal; if (index < actualColors) { bgColor = rgbColors[index]; } } PropVariantClear(&propValue); } } } } // Get global frame size UINT width = 0; UINT height = 0; hr = metareader->GetMetadataByName(L"/logscrdesc/Width", &propValue); if (FAILED(hr)) return hr; if (propValue.vt != VT_UI2) return E_FAIL; width = propValue.uiVal; PropVariantClear(&propValue); hr = metareader->GetMetadataByName(L"/logscrdesc/Height", &propValue); if (FAILED(hr)) return hr; if (propValue.vt != VT_UI2) return E_FAIL; height = propValue.uiVal; PropVariantClear(&propValue); UINT fcount; hr = decoder->GetFrameCount(&fcount); if (FAILED(hr)) return hr; UINT disposal = DM_UNDEFINED; RECT rct = {}; UINT previousFrame = 0; for (UINT iframe = 0; iframe < fcount; ++iframe) { int transparentIndex = -1; std::unique_ptr frameImage(new (std::nothrow) ScratchImage); if (!frameImage) return E_OUTOFMEMORY; if (disposal == DM_PREVIOUS) { hr = frameImage->InitializeFromImage(*loadedImages[previousFrame]->GetImage(0, 0, 0)); } else if (iframe > 0) { hr = frameImage->InitializeFromImage(*loadedImages[iframe - 1]->GetImage(0, 0, 0)); } else { hr = frameImage->Initialize2D(DXGI_FORMAT_B8G8R8A8_UNORM, width, height, 1, 1); } if (FAILED(hr)) return hr; auto composedImage = frameImage->GetImage(0, 0, 0); if (!iframe) { RECT fullRct = { 0, 0, static_cast(width), static_cast(height) }; FillRectangle(*composedImage, fullRct, bgColor); } else if (disposal == DM_BACKGROUND) { FillRectangle(*composedImage, rct, bgColor); } ComPtr frame; hr = decoder->GetFrame(iframe, frame.GetAddressOf()); if (FAILED(hr)) return hr; WICPixelFormatGUID pixelFormat; hr = frame->GetPixelFormat(&pixelFormat); if (FAILED(hr)) return hr; if (memcmp(&pixelFormat, &GUID_WICPixelFormat8bppIndexed, sizeof(GUID)) != 0) { // GIF is always loaded as this format return E_UNEXPECTED; } ComPtr frameMeta; hr = frame->GetMetadataQueryReader(frameMeta.GetAddressOf()); if (SUCCEEDED(hr)) { hr = frameMeta->GetMetadataByName(L"/imgdesc/Left", &propValue); if (SUCCEEDED(hr)) { hr = (propValue.vt == VT_UI2 ? S_OK : E_FAIL); if (SUCCEEDED(hr)) { rct.left = static_cast(propValue.uiVal); } PropVariantClear(&propValue); } hr = frameMeta->GetMetadataByName(L"/imgdesc/Top", &propValue); if (SUCCEEDED(hr)) { hr = (propValue.vt == VT_UI2 ? S_OK : E_FAIL); if (SUCCEEDED(hr)) { rct.top = static_cast(propValue.uiVal); } PropVariantClear(&propValue); } hr = frameMeta->GetMetadataByName(L"/imgdesc/Width", &propValue); if (SUCCEEDED(hr)) { hr = (propValue.vt == VT_UI2 ? S_OK : E_FAIL); if (SUCCEEDED(hr)) { rct.right = static_cast(propValue.uiVal) + rct.left; } PropVariantClear(&propValue); } hr = frameMeta->GetMetadataByName(L"/imgdesc/Height", &propValue); if (SUCCEEDED(hr)) { hr = (propValue.vt == VT_UI2 ? S_OK : E_FAIL); if (SUCCEEDED(hr)) { rct.bottom = static_cast(propValue.uiVal) + rct.top; } PropVariantClear(&propValue); } disposal = DM_UNDEFINED; hr = frameMeta->GetMetadataByName(L"/grctlext/Disposal", &propValue); if (SUCCEEDED(hr)) { hr = (propValue.vt == VT_UI1 ? S_OK : E_FAIL); if (SUCCEEDED(hr)) { disposal = propValue.bVal; } PropVariantClear(&propValue); } hr = frameMeta->GetMetadataByName(L"/grctlext/TransparencyFlag", &propValue); if (SUCCEEDED(hr)) { hr = (propValue.vt == VT_BOOL ? S_OK : E_FAIL); if (SUCCEEDED(hr) && propValue.boolVal) { PropVariantClear(&propValue); hr = frameMeta->GetMetadataByName(L"/grctlext/TransparentColorIndex", &propValue); if (SUCCEEDED(hr)) { hr = (propValue.vt == VT_UI1 ? S_OK : E_FAIL); if (SUCCEEDED(hr) && propValue.uiVal < actualColors) { transparentIndex = static_cast(propValue.uiVal); } } } PropVariantClear(&propValue); } } UINT w, h; hr = frame->GetSize(&w, &h); if (FAILED(hr)) return hr; ScratchImage rawFrame; hr = rawFrame.Initialize2D(DXGI_FORMAT_B8G8R8A8_UNORM, w, h, 1, 1); if (FAILED(hr)) return hr; ComPtr FC; hr = pWIC->CreateFormatConverter(FC.GetAddressOf()); if (FAILED(hr)) return hr; hr = FC->Initialize(frame.Get(), GUID_WICPixelFormat32bppBGRA, WICBitmapDitherTypeNone, nullptr, 0, WICBitmapPaletteTypeMedianCut); if (FAILED(hr)) return hr; auto img = rawFrame.GetImage(0, 0, 0); hr = FC->CopyPixels(nullptr, static_cast(img->rowPitch), static_cast(img->slicePitch), img->pixels); if (FAILED(hr)) return hr; if (!iframe || transparentIndex == -1) { const Rect fullRect(0, 0, img->width, img->height); hr = CopyRectangle(*img, fullRect, *composedImage, TEX_FILTER_DEFAULT, size_t(rct.left), size_t(rct.top)); if (FAILED(hr)) return hr; } else { BlendRectangle(*composedImage, *img, rct, rgbColors[transparentIndex]); } if (disposal == DM_UNDEFINED || disposal == DM_NONE) { previousFrame = iframe; } loadedImages.emplace_back(std::move(frameImage)); } PropVariantClear(&propValue); return S_OK; }