// Copyright (C) 2022 Jérôme "Lynix" Leclercq (lynix680@gmail.com) // This file is part of the "Nazara Engine - Utility module" // For conditions of distribution and use, see copyright notice in Config.hpp #include #include #include #include #include #include #include #include #include #include // based on https://www.w3.org/Graphics/GIF/spec-gif89a.txt, with help from the following public domain libraries source code: // - https://github.com/lecram/gifdec // - https://github.com/nothings/stb/blob/master/stb_image.h namespace Nz { namespace { constexpr UInt8 DisposeToBackground = 2; constexpr UInt8 DisposeToPrevious = 3; class GIFImageStream : public ImageStream { public: GIFImageStream() { m_byteStream.SetDataEndianness(Endianness::LittleEndian); } ~GIFImageStream() { } bool Check() { std::array header; //< 3 bytes for signature + 3 bytes for version (87a and 89a supported) if (m_byteStream.Read(header.data(), header.size()) != header.size()) return false; if (std::memcmp(&header[0], "GIF", 3) != 0) return false; if (std::memcmp(&header[3], "87a", 3) != 0 && std::memcmp(&header[3], "89a", 3) != 0) return false; return true; } bool DecodeNextFrame(void* frameBuffer, UInt64* frameTime) override { 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; decodingData.lineSize = m_header.width * 4; decodingData.startX = left * 4; decodingData.startY = top * decodingData.lineSize; decodingData.maxX = decodingData.startX + width * 4; decodingData.maxY = decodingData.startY + decodingData.lineSize * height; decodingData.currentX = decodingData.startX; decodingData.currentY = decodingData.startY; // 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; // to make sure this is caught, move the current y down to // max_y (which is what out_gif_code checks). if (width == 0) decodingData.currentY = decodingData.maxY; bool interlace = (flag & 0b0100'0000); if (interlace) { decodingData.step = 8 * decodingData.lineSize; decodingData.parseMode = 3; } else { decodingData.step = decodingData.lineSize; decodingData.parseMode = 0; } bool hasLocalColorTable = (flag & 0b1000'0000); if (hasLocalColorTable) { UInt16 numEntries = 2ULL << (flag & 0b0000'0111); m_localColorTable.resize(numEntries); 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 = 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 NazaraInternalError("expected color table"); return false; } UInt8 minimumCodeSize; m_byteStream >> minimumCodeSize; if (minimumCodeSize > 12) { NazaraInternalError("unexpected LZW Minimum Code Size (" + std::to_string(minimumCodeSize) + ")"); 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; } Result Open() { if (!Check()) return Err(ResourceLoadingError::Unrecognized); 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 Err(ResourceLoadingError::DecodingError); } if (top + height > m_header.height) { NazaraError("corrupt gif (out of range)"); return Err(ResourceLoadingError::DecodingError); } 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 Err(ResourceLoadingError::DecodingError); } UInt8 minimumCodeSize; m_byteStream >> minimumCodeSize; if (minimumCodeSize > 12) { NazaraError("unexpected LZW Minimum Code Size (" + std::to_string(minimumCodeSize) + ")"); return Err(ResourceLoadingError::DecodingError); } 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 Err(ResourceLoadingError::DecodingError); } 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 Err(ResourceLoadingError::DecodingError); } } 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 Ok(); } 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; Int32 codeMask = (1 << codeSize) - 1; Int32 bits = 0; Int32 validBits = 0; m_lzwEntries.clear(); m_lzwEntries.resize(8192); //< ?? for (Int32 i = 0; i < clear; ++i) { auto& entry = m_lzwEntries[i]; entry.prefix = -1; entry.first = UInt8(i); entry.suffix = UInt8(i); } // support no starting clear code Int32 avail = clear + 2; Int32 oldcode = -1; UInt8 len = 0; m_affectedPixels.Reset(); for (;;) { if (validBits < codeSize) { if (len == 0) { m_byteStream >> len; // start new block if (len == 0) break; } UInt8 data; m_byteStream >> data; --len; bits |= data << validBits; validBits += 8; } else { Int32 code = bits & codeMask; bits >>= codeSize; validBits -= codeSize; // @OPTIMIZE: is there some way we can accelerate the non-clear path? if (code == clear) { // clear code codeSize = minimumCodeSize + 1; codeMask = (1 << codeSize) - 1; avail = clear + 2; oldcode = -1; first = 0; } else if (code == clear + 1) { // end of stream code SkipUntilTerminationBlock(); break; } else if (code <= avail) { if (first) { NazaraError("corrupt gif (no clear code)"); return false; } if (oldcode >= 0) { auto& p = m_lzwEntries[avail++]; if (avail > 8192) { NazaraError("corrupt gif (too many codes)"); return false; } p.prefix = SafeCast(oldcode); p.first = m_lzwEntries[oldcode].first; p.suffix = (code == avail) ? p.first : m_lzwEntries[code].first; } else if (code == avail) { NazaraError("corrupt gif (illegal code in raster)"); return false; } DecodeGIF(SafeCast(code), decodingData); if ((avail & codeMask) == 0 && avail <= 0x0FFF) { codeSize++; codeMask = (1 << codeSize) - 1; } oldcode = code; } else { NazaraError("corrupt gif (illegal code in raster)"); return false; } } } return true; } void DecodeGIF(UInt16 code, ImageDecodingData& decodingData) { // recurse to decode the prefixes, since the linked-list is backwards, // and working backwards through an interleaved image would be nasty if (m_lzwEntries[code].prefix >= 0) DecodeGIF(m_lzwEntries[code].prefix, decodingData); if (decodingData.currentY >= decodingData.maxY) return; std::size_t idx = decodingData.currentX + decodingData.currentY; UInt8* p = &decodingData.outputImage[idx]; m_affectedPixels[idx / 4] = true; std::size_t colorIndex = m_lzwEntries[code].suffix; const Color* c = &decodingData.colorTable[colorIndex]; // don't render transparent pixels if (colorIndex != decodingData.transparentColorIndex) { p[0] = c->r; p[1] = c->g; p[2] = c->b; p[3] = c->a; } decodingData.currentX += 4; if (decodingData.currentX >= decodingData.maxX) { decodingData.currentX = decodingData.startX; decodingData.currentY += decodingData.step; while (decodingData.currentY >= decodingData.maxY && decodingData.parseMode > 0) { decodingData.step = (1ULL << decodingData.parseMode) * decodingData.lineSize; decodingData.currentY = decodingData.startY + (decodingData.step >> 1); --decodingData.parseMode; } } } void SkipUntilTerminationBlock() { for (;;) { UInt8 blockSize; m_byteStream >> blockSize; if (blockSize == 0) return; m_byteStream.Read(nullptr, blockSize); } } struct Color { UInt8 r, g, b, a; }; struct FrameMetadata { std::size_t transparentIndex = std::numeric_limits::max(); UInt64 time; UInt64 streamOffset; UInt8 disposalMethod = 0; }; struct ImageDecodingData { std::size_t currentX; std::size_t currentY; std::size_t lineSize; std::size_t maxX; std::size_t maxY; std::size_t parseMode; std::size_t startX; std::size_t startY; std::size_t step; std::size_t transparentColorIndex; Color* colorTable; UInt8* outputImage; }; struct LogicalScreenDescriptor { UInt16 height; UInt16 width; UInt8 backgroundPaletteIndex; UInt8 flags; UInt8 packedFields; UInt8 ratio; }; struct LZWEntry { Int16 prefix = 0; UInt8 first = 0; UInt8 suffix = 0; }; std::size_t m_currentFrame; std::vector m_globalColorTable; std::vector m_localColorTable; 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; UInt64 m_endFrameTime; bool m_requiresFrameHistory; }; bool CheckGIFExtension(const std::string_view& extension) { return extension == ".gif"; } Result, ResourceLoadingError> LoadGIFFile(const std::filesystem::path& filePath, const ImageStreamParams& /*parameters*/) { std::shared_ptr gifStream = std::make_shared(); if (!gifStream->SetFile(filePath)) return Err(ResourceLoadingError::FailedToOpenFile); Result status = gifStream->Open(); return status.Map([&] { return std::move(gifStream); }); } Result, ResourceLoadingError> LoadGIFMemory(const void* ptr, std::size_t size, const ImageStreamParams& /*parameters*/) { std::shared_ptr gifStream = std::make_shared(); gifStream->SetMemory(ptr, size); Result status = gifStream->Open(); return status.Map([&] { return std::move(gifStream); }); } Result, ResourceLoadingError> LoadGIFStream(Stream& stream, const ImageStreamParams& /*parameters*/) { std::shared_ptr gifStream = std::make_shared(); gifStream->SetStream(stream); Result status = gifStream->Open(); return status.Map([&] { return std::move(gifStream); }); } } namespace Loaders { ImageStreamLoader::Entry GetImageStreamLoader_GIF() { ImageStreamLoader::Entry loaderEntry; loaderEntry.extensionSupport = CheckGIFExtension; loaderEntry.fileLoader = LoadGIFFile; loaderEntry.memoryLoader = LoadGIFMemory; loaderEntry.streamLoader = LoadGIFStream; loaderEntry.parameterFilter = [](const ImageStreamParams& parameters) { bool skip; if (parameters.custom.GetBooleanParameter("SkipBuiltinGIFLoader", &skip) && skip) return false; return true; }; return loaderEntry; } } }