From 13f765fc0d221df3b22b2a6216f3f55b8b618895 Mon Sep 17 00:00:00 2001 From: SirLynix Date: Mon, 25 Apr 2022 19:26:37 +0200 Subject: [PATCH] Utility: Fix GIF loading --- include/Nazara/Utility/ImageStream.hpp | 5 +- src/Nazara/Utility/Formats/GIFLoader.cpp | 633 ++++++++++++++--------- 2 files changed, 380 insertions(+), 258 deletions(-) diff --git a/include/Nazara/Utility/ImageStream.hpp b/include/Nazara/Utility/ImageStream.hpp index ef5e48eca..87e6aeef8 100644 --- a/include/Nazara/Utility/ImageStream.hpp +++ b/include/Nazara/Utility/ImageStream.hpp @@ -34,15 +34,12 @@ namespace Nz ImageStream() = default; virtual ~ImageStream(); - virtual bool DecodeNextFrame(void* buffer) = 0; + virtual bool DecodeNextFrame(void* frameBuffer, UInt64* frameTime) = 0; virtual UInt64 GetFrameCount() const = 0; - virtual UInt64 GetFrameDelay(std::size_t frameIndex) const = 0; virtual PixelFormat GetPixelFormat() const = 0; virtual Vector2ui GetSize() const = 0; - virtual bool HasConstantRate() const = 0; - virtual void Seek(UInt64 frameIndex) = 0; virtual UInt64 Tell() = 0; diff --git a/src/Nazara/Utility/Formats/GIFLoader.cpp b/src/Nazara/Utility/Formats/GIFLoader.cpp index 28a5f05c0..63e26e91f 100644 --- a/src/Nazara/Utility/Formats/GIFLoader.cpp +++ b/src/Nazara/Utility/Formats/GIFLoader.cpp @@ -21,6 +21,9 @@ namespace Nz { namespace { + constexpr UInt8 DisposeToBackground = 2; + constexpr UInt8 DisposeToPrevious = 3; + class GIFImageStream : public ImageStream { public: @@ -48,17 +51,29 @@ namespace Nz return true; } - bool DecodeNextFrame(void* buffer) override + bool DecodeNextFrame(void* frameBuffer, UInt64* frameTime) override { - UInt8* outputImage = static_cast(buffer); + if (m_currentFrame >= m_frames.size()) + { + if (frameTime) + *frameTime = m_endFrameTime; + + return false; + } + + UInt8* outputImage = static_cast(frameBuffer); auto& frameData = m_frames[m_currentFrame]; + if (frameTime) + *frameTime = frameData.time; + UInt16 left; UInt16 top; UInt16 width; UInt16 height; UInt8 flag; + m_byteStream.GetStream()->SetCursorPos(frameData.streamOffset); m_byteStream >> left >> top >> width >> height >> flag; ImageDecodingData decodingData; @@ -69,10 +84,60 @@ namespace Nz decodingData.maxY = decodingData.startY + decodingData.lineSize * height; decodingData.currentX = decodingData.startX; decodingData.currentY = decodingData.startY; - decodingData.outputImage = outputImage; - //if (m_currentFrame == 0) - std::memset(decodingData.outputImage, 0, m_header.width * m_header.height * 4); + // Render to previous frame if frame history is required + if (m_requiresFrameHistory) + decodingData.outputImage = m_previousFrame.get(); + else + decodingData.outputImage = outputImage; + + std::size_t pixelCount = m_header.width * m_header.height; + + if (m_currentFrame == 0) + { + if (m_requiresFrameHistory) + std::memset(m_previousFrame.get(), 0, pixelCount * 4); + else if (outputImage) + std::memset(outputImage, 0, pixelCount * 4); + + if (m_disposedRendering) + std::memset(m_disposedRendering.get(), 0, pixelCount * 4); + } + else if (m_requiresFrameHistory) + { + if (m_frames[m_currentFrame - 1].disposalMethod == DisposeToBackground) + { + // FIXME: Is background color something else than transparent? + std::array backgroundColor; + backgroundColor.fill(0); + + // restore affected pixels to background + for (std::size_t i = 0; i < pixelCount; ++i) + { + if (m_affectedPixels[i]) + std::memcpy(&m_previousFrame[i * 4], &backgroundColor[0], 4); + } + } + else if (m_frames[m_currentFrame - 1].disposalMethod == DisposeToPrevious) + { + // restore affected pixels to frame N - 2 + for (std::size_t i = 0; i < pixelCount; ++i) + { + if (m_affectedPixels[i]) + std::memcpy(&m_previousFrame[i * 4], &m_disposedRendering[i * 4], 4); + } + } + + if (m_disposedRendering) + std::memcpy(&m_disposedRendering[0], &m_previousFrame[0], pixelCount * 4); + } + else if (m_frames[m_currentFrame - 1].disposalMethod == DisposeToBackground) + { + // Special case where each frame dispose to background but does full rendering + // simply clear to transparent + if (outputImage) + std::memset(outputImage, 0, pixelCount * 4); + } // if the width of the specified rectangle is 0, that means // we may not see *any* pixels or the image is malformed; @@ -101,13 +166,17 @@ namespace Nz for (std::size_t i = 0; i < numEntries; ++i) { m_byteStream >> m_localColorTable[i].r >> m_localColorTable[i].g >> m_localColorTable[i].b; - m_localColorTable[i].a = (i == frameData.transparentIndex) ? 0 : 0xFF; + m_localColorTable[i].a = 0xFF; } decodingData.colorTable = &m_localColorTable[0]; + decodingData.transparentColorIndex = frameData.transparentIndex; } else if (!m_globalColorTable.empty()) + { decodingData.colorTable = &m_globalColorTable[0]; + decodingData.transparentColorIndex = frameData.transparentIndex; + } else { // this error should have been caught already when loading @@ -123,6 +192,292 @@ namespace Nz return false; } + if (decodingData.outputImage) + { + if (!DecodeImageDescriptor(minimumCodeSize, decodingData)) + return false; + } + else + SkipUntilTerminationBlock(); + + if (m_currentFrame == 0) + { + // if first frame, any pixel not drawn to gets the background color + if (!m_globalColorTable.empty()) + { + for (std::size_t i = 0; i < pixelCount; ++i) + { + if (!m_affectedPixels[i]) + { + UInt8* outputPixel = &outputImage[i * 4]; + outputPixel[0] = m_globalColorTable[m_header.backgroundPaletteIndex].r; + outputPixel[1] = m_globalColorTable[m_header.backgroundPaletteIndex].g; + outputPixel[2] = m_globalColorTable[m_header.backgroundPaletteIndex].b; + outputPixel[3] = m_globalColorTable[m_header.backgroundPaletteIndex].a; + } + } + } + } + + if (outputImage && decodingData.outputImage != outputImage) + std::memcpy(outputImage, decodingData.outputImage, pixelCount * 4); + + m_currentFrame++; + return true; + } + + UInt64 GetFrameCount() const override + { + return m_frames.size(); + } + + PixelFormat GetPixelFormat() const override + { + return PixelFormat::RGBA8; //< TODO: Set SRGB + } + + Vector2ui GetSize() const override + { + return Vector2ui(m_header.width, m_header.height); + } + + void Seek(UInt64 frameIndex) override + { + assert(frameIndex < m_frames.size()); + + if (m_requiresFrameHistory) + { + if (m_currentFrame > frameIndex) + m_currentFrame = 0; + + while (m_currentFrame < frameIndex) + DecodeNextFrame(nullptr, nullptr); + } + else + m_currentFrame = frameIndex; + } + + UInt64 Tell() override + { + return m_currentFrame; + } + + bool Open() + { + if (!Check()) + { + NazaraError("stream has invalid GIF header"); + return false; + } + + m_byteStream >> m_header.width >> m_header.height; + m_byteStream >> m_header.flags >> m_header.backgroundPaletteIndex >> m_header.ratio; + + bool hasGlobalColorTable = (m_header.flags & 0b1000'0000); + if (hasGlobalColorTable) + { + std::size_t numEntries = 2ULL << (m_header.flags & 0b0000'0111); + m_globalColorTable.resize(numEntries); + for (std::size_t i = 0; i < numEntries; ++i) + { + m_byteStream >> m_globalColorTable[i].r >> m_globalColorTable[i].g >> m_globalColorTable[i].b; + m_globalColorTable[i].a = 0xFF; + } + } + + m_frames.clear(); + m_requiresFrameHistory = false; + bool hasDisposeToPrevious = false; + bool hasPartialRendering = false; + bool terminated = false; + + UInt64 frameTime = 0; + + FrameMetadata nextFrame; + while (!terminated) + { + UInt8 tag; + m_byteStream >> tag; + + switch (tag) + { + case 0: //< empty block? + break; + + case 0x2C: //< image descriptor tag + { + nextFrame.streamOffset = m_byteStream.GetStream()->GetCursorPos(); + + m_frames.push_back(nextFrame); + nextFrame = {}; + + UInt16 left; + UInt16 top; + UInt16 width; + UInt16 height; + UInt8 flag; + + m_byteStream >> left >> top >> width >> height >> flag; + + if (left + width > m_header.width) + { + NazaraError("corrupt gif (out of range)"); + return false; + } + + if (top + height > m_header.height) + { + NazaraError("corrupt gif (out of range)"); + return false; + } + + if (left != 0 || top != 0 || width < m_header.width || height < m_header.height) + hasPartialRendering = true; + + if (flag & 0b1000'0000) + { + // has local color table + UInt16 colorTableSize = 2ULL << (flag & 0b0000'0111); + m_byteStream.Read(nullptr, colorTableSize * 3); + } + else if (!hasGlobalColorTable) + { + NazaraError("corrupt gif (no color table for image #" + std::to_string(m_frames.size() - 1) + ")"); + return false; + } + + UInt8 minimumCodeSize; + m_byteStream >> minimumCodeSize; + if (minimumCodeSize > 12) + { + NazaraError("unexpected LZW Minimum Code Size (" + std::to_string(minimumCodeSize) + ")"); + return false; + } + + SkipUntilTerminationBlock(); + break; + } + + case 0x3B: //< end of file + terminated = true; + break; + + case 0x21: //< extension tag + { + UInt8 label; + m_byteStream >> label; + switch (label) + { + case 0xF9: //< graphic control extension + { + UInt8 blockSize; + UInt8 flags; + UInt16 delay; + + m_byteStream >> blockSize >> flags >> delay; + + if (delay == 0) + delay = 10; + + if (blockSize != 4) + { + NazaraError("corrupt gif (invalid block size for graphic control extension)"); + return false; + } + + nextFrame.disposalMethod = (flags & 0b0001'1100) >> 2; + nextFrame.time = frameTime; + frameTime += delay * 10; + + if (flags & 0b0000'0001) + { + UInt8 transparentIndex; + m_byteStream >> transparentIndex; + + nextFrame.transparentIndex = transparentIndex; + } + + if (nextFrame.disposalMethod == DisposeToPrevious) + hasDisposeToPrevious = true; + + break; + } + + case 0xFE: //< comment extension + break; + + case 0x01: //< plain text extension + break; + + case 0xFF: //< application extension + break; + + default: + NazaraWarning("unrecognized extension label (unknown tag 0x" + NumberToString(label, 16) + ")"); + break; + } + + SkipUntilTerminationBlock(); + break; + } + + default: + NazaraError("corrupt gif (unknown tag 0x" + NumberToString(tag, 16) + ")"); + return false; + } + } + + if (hasDisposeToPrevious || hasPartialRendering) + m_requiresFrameHistory = true; + + m_endFrameTime = frameTime; + + m_affectedPixels.Resize(m_header.width * m_header.height); + if (m_requiresFrameHistory) + m_previousFrame = std::make_unique(m_header.width * m_header.height * 4); + else + m_previousFrame.reset(); + + if (hasDisposeToPrevious) + m_disposedRendering = std::make_unique(m_header.width * m_header.height * 4); + else + m_disposedRendering.reset(); + + m_currentFrame = 0; + + return true; + } + + bool SetFile(const std::filesystem::path& filePath) + { + std::unique_ptr file = std::make_unique(); + if (!file->Open(filePath, OpenMode::ReadOnly)) + { + NazaraError("Failed to open stream from file: " + Error::GetLastError()); + return false; + } + m_ownedStream = std::move(file); + + SetStream(*m_ownedStream); + return true; + } + + void SetMemory(const void* data, std::size_t size) + { + m_ownedStream = std::make_unique(data, size); + SetStream(*m_ownedStream); + } + + void SetStream(Stream& stream) + { + m_byteStream.SetStream(&stream); + } + + private: + struct ImageDecodingData; + + bool DecodeImageDescriptor(UInt8 minimumCodeSize, ImageDecodingData& decodingData) + { Int32 clear = 1 << minimumCodeSize; UInt32 first = 1; Int32 codeSize = minimumCodeSize + 1; @@ -172,7 +527,8 @@ namespace Nz validBits -= codeSize; // @OPTIMIZE: is there some way we can accelerate the non-clear path? if (code == clear) - { // clear code + { + // clear code codeSize = minimumCodeSize + 1; codeMask = (1 << codeSize) - 1; avail = clear + 2; @@ -230,248 +586,9 @@ namespace Nz } } - // if this was the first frame - if (m_currentFrame == 0 && m_header.backgroundPaletteIndex > 0) - { - std::size_t pixelCount = m_header.width * m_header.height; - // if first frame, any pixel not drawn to gets the background color - for (std::size_t i = 0; i < pixelCount; ++i) - { - if (!m_affectedPixels[i]) - { - UInt8* outputPixel = &outputImage[i * 4]; - outputPixel[0] = m_globalColorTable[m_header.backgroundPaletteIndex].r; - outputPixel[1] = m_globalColorTable[m_header.backgroundPaletteIndex].g; - outputPixel[2] = m_globalColorTable[m_header.backgroundPaletteIndex].b; - outputPixel[3] = m_globalColorTable[m_header.backgroundPaletteIndex].a; - } - } - } - - if (m_currentFrame + 1 < m_frames.size()) - Seek(m_currentFrame + 1); - return true; } - UInt64 GetFrameCount() const override - { - return m_frames.size(); - } - - UInt64 GetFrameDelay(std::size_t frameIndex) const override - { - assert(frameIndex < m_frames.size()); - return m_frames[frameIndex].delay; - } - - PixelFormat GetPixelFormat() const override - { - return PixelFormat::RGBA8; //< TODO: Set SRGB - } - - Vector2ui GetSize() const override - { - return Vector2ui(m_header.width, m_header.height); - } - - bool HasConstantRate() const override - { - return m_hasConstantRate; - } - - void Seek(UInt64 frameIndex) override - { - assert(frameIndex < m_frames.size()); - - m_currentFrame = frameIndex; - m_byteStream.GetStream()->SetCursorPos(m_frames[m_currentFrame].streamOffset); - } - - UInt64 Tell() override - { - return m_currentFrame; - } - - bool Open() - { - if (!Check()) - { - NazaraError("stream has invalid GIF header"); - return false; - } - - m_byteStream >> m_header.width >> m_header.height; - m_byteStream >> m_header.flags >> m_header.backgroundPaletteIndex >> m_header.ratio; - - bool hasGlobalColorTable = (m_header.flags & 0b1000'0000); - if (hasGlobalColorTable) - { - std::size_t numEntries = 2ULL << (m_header.flags & 0b0000'0111); - m_globalColorTable.resize(numEntries); - for (std::size_t i = 0; i < numEntries; ++i) - { - m_byteStream >> m_globalColorTable[i].r >> m_globalColorTable[i].g >> m_globalColorTable[i].b; - m_globalColorTable[i].a = 0xFF; - } - } - - m_frames.clear(); - bool terminated = false; - UInt16 nextFrameDelay = 0; - UInt8 nextFrameTransparentIndex = 0xFF; - while (!terminated) - { - UInt8 tag; - m_byteStream >> tag; - - switch (tag) - { - case 0x2C: //< image descriptor tag - { - auto& frame = m_frames.emplace_back(); - frame.delay = nextFrameDelay; - frame.streamOffset = m_byteStream.GetStream()->GetCursorPos(); - frame.transparentIndex = nextFrameTransparentIndex; - - UInt16 left; - UInt16 top; - UInt16 width; - UInt16 height; - UInt8 flag; - - m_byteStream >> left >> top >> width >> height >> flag; - - if (left + width > m_header.width) - { - NazaraError("corrupt gif (out of range)"); - return false; - } - - if (top + height > m_header.height) - { - NazaraError("corrupt gif (out of range)"); - return false; - } - - if (flag & 0b1000'0000) - { - // has local color table - UInt16 colorTableSize = 2ULL << (flag & 0b0000'0111); - m_byteStream.Read(nullptr, colorTableSize * 3); - } - else if (!hasGlobalColorTable) - { - NazaraError("corrupt gif (no color table for image #" + std::to_string(m_frames.size() - 1) + ")"); - return false; - } - - UInt8 minimumCodeSize; - m_byteStream >> minimumCodeSize; - if (minimumCodeSize > 12) - { - NazaraError("unexpected LZW Minimum Code Size (" + std::to_string(minimumCodeSize) + ")"); - return false; - } - - SkipUntilTerminationBlock(); - break; - } - - case 0x3B: //< end of file - terminated = true; - break; - - case 0x21: //< extension tag - { - UInt8 label; - m_byteStream >> label; - switch (label) - { - case 0xF9: //< graphic control extension - { - UInt8 blockSize; - UInt8 flags; - UInt16 delay; - - m_byteStream >> blockSize >> flags >> delay; - - if (blockSize != 4) - { - NazaraError("corrupt gif (invalid block size for graphic control extension)"); - return false; - } - - UInt8 disposalMethod = (flags & 0b0001'1100) >> 2; - nextFrameDelay = delay * 10; - - if (flags & 0b0000'0001) - m_byteStream >> nextFrameTransparentIndex; - else - nextFrameTransparentIndex = 0xFFFF; - - break; - } - - case 0xFE: //< comment extension - break; - - case 0x01: //< plain text extension - break; - - case 0xFF: //< application extension - break; - - default: - NazaraWarning("unrecognized extension label (unknown tag 0x" + NumberToString(label, 16) + ")"); - break; - } - - SkipUntilTerminationBlock(); - break; - } - - default: - NazaraError("corrupt gif (unknown tag 0x" + NumberToString(tag, 16) + ")"); - return false; - } - } - - m_affectedPixels.Resize(m_header.width* m_header.height); - - Seek(0); - - return true; - } - - bool SetFile(const std::filesystem::path& filePath) - { - std::unique_ptr file = std::make_unique(); - if (!file->Open(filePath, OpenMode::ReadOnly)) - { - NazaraError("Failed to open stream from file: " + Error::GetLastError()); - return false; - } - m_ownedStream = std::move(file); - - SetStream(*m_ownedStream); - return true; - } - - void SetMemory(const void* data, std::size_t size) - { - m_ownedStream = std::make_unique(data, size); - SetStream(*m_ownedStream); - } - - void SetStream(Stream& stream) - { - m_byteStream.SetStream(&stream); - } - - private: - struct ImageDecodingData; - void DecodeGIF(UInt16 code, ImageDecodingData& decodingData) { // recurse to decode the prefixes, since the linked-list is backwards, @@ -486,10 +603,13 @@ namespace Nz UInt8* p = &decodingData.outputImage[idx]; m_affectedPixels[idx / 4] = true; - const Color* c = &decodingData.colorTable[m_lzwEntries[code].suffix]; - if (c->a > 128) + std::size_t colorIndex = m_lzwEntries[code].suffix; + + const Color* c = &decodingData.colorTable[colorIndex]; + + // don't render transparent pixels + if (colorIndex != decodingData.transparentColorIndex) { - // don't render transparent pixels; p[0] = c->r; p[1] = c->g; p[2] = c->b; @@ -531,11 +651,12 @@ namespace Nz UInt8 r, g, b, a; }; - struct FrameData + struct FrameMetadata { - UInt64 delay; + std::size_t transparentIndex = std::numeric_limits::max(); + UInt64 time; UInt64 streamOffset; - UInt8 transparentIndex; + UInt8 disposalMethod = 0; }; struct ImageDecodingData @@ -549,6 +670,7 @@ namespace Nz std::size_t startX; std::size_t startY; std::size_t step; + std::size_t transparentColorIndex; Color* colorTable; UInt8* outputImage; }; @@ -573,13 +695,16 @@ namespace Nz std::size_t m_currentFrame; std::vector m_globalColorTable; std::vector m_localColorTable; - std::vector m_frames; + std::vector m_frames; std::vector m_lzwEntries; std::unique_ptr m_ownedStream; + std::unique_ptr m_disposedRendering; + std::unique_ptr m_previousFrame; Bitset m_affectedPixels; ByteStream m_byteStream; LogicalScreenDescriptor m_header; - bool m_hasConstantRate; + UInt64 m_endFrameTime; + bool m_requiresFrameHistory; }; bool CheckGIFExtension(const std::string_view& extension)