From ccc98bdf6288cd055edb570e601496205827b8a6 Mon Sep 17 00:00:00 2001 From: Atdunbg <979541498@qq.com> Date: Sat, 27 Sep 2025 14:06:57 +0800 Subject: [PATCH] Implement GIF conversion --- expkg/CMakeLists.txt | 12 +- expkg/src/BinaryOPT/BinaryReader.cpp | 9 +- expkg/src/BinaryOPT/BinaryReader.h | 1 + expkg/src/BinaryOPT/BinaryWriter.cpp | 13 +- expkg/src/BinaryOPT/BinaryWriter.h | 2 +- expkg/src/DXT/DXT.cpp | 230 +++++++ expkg/src/DXT/DXT.h | 38 ++ expkg/src/EXPKG/EXPKG.cpp | 393 ++++++++--- expkg/src/EXPKG/EXPKG.h | 4 +- expkg/src/Tex/Tex.h | 3 +- expkg/src/Tex/TexFrameInfoContainer.cpp | 5 + expkg/src/Tex/TexFrameInfoContainer.h | 39 ++ expkg/vendor/gif-h/LICENSE | 24 + expkg/vendor/gif-h/gif.h | 864 ++++++++++++++++++++++++ 14 files changed, 1519 insertions(+), 118 deletions(-) create mode 100644 expkg/src/DXT/DXT.cpp create mode 100644 expkg/src/DXT/DXT.h create mode 100644 expkg/src/Tex/TexFrameInfoContainer.cpp create mode 100644 expkg/src/Tex/TexFrameInfoContainer.h create mode 100644 expkg/vendor/gif-h/LICENSE create mode 100644 expkg/vendor/gif-h/gif.h diff --git a/expkg/CMakeLists.txt b/expkg/CMakeLists.txt index 4587740..ae67d01 100644 --- a/expkg/CMakeLists.txt +++ b/expkg/CMakeLists.txt @@ -3,16 +3,18 @@ project(${TARGET}) add_subdirectory(vendor/lz4/build/cmake) -file(GLOB_RECURSE SRC_SOURCE src/**.cpp) +file(GLOB_RECURSE SRC_SOURCE src/**.cpp vendor/gif-h/gif.h) + +file(GLOB STB_SOURCE vendor/stb/*.cpp) # static add_library(expkg-static STATIC ${SRC_SOURCE} - vendor/stb/stb_image_write.cpp + ${STB_SOURCE} ) target_link_libraries(expkg-static PRIVATE lz4) -target_include_directories(expkg-static PRIVATE vendor/stb) +target_include_directories(expkg-static PRIVATE vendor/stb vendor/gif-h) target_include_directories(expkg-static PUBLIC src) @@ -20,7 +22,7 @@ target_include_directories(expkg-static PUBLIC src) # shared add_library(expkg-shared SHARED ${SRC_SOURCE} - vendor/stb/stb_image_write.cpp + ${STB_SOURCE} ) set_target_properties(expkg-shared PROPERTIES @@ -31,7 +33,7 @@ set_target_properties(expkg-shared PROPERTIES target_link_libraries(expkg-shared PRIVATE lz4) target_compile_definitions(expkg-shared PRIVATE -DPKG_SHARED -DPKG_BUILD_DLL) -target_include_directories(expkg-shared PRIVATE vendor/stb) +target_include_directories(expkg-shared PRIVATE vendor/stb vendor/gif-h) target_include_directories(expkg-shared PUBLIC src) diff --git a/expkg/src/BinaryOPT/BinaryReader.cpp b/expkg/src/BinaryOPT/BinaryReader.cpp index 1451a0f..68de088 100644 --- a/expkg/src/BinaryOPT/BinaryReader.cpp +++ b/expkg/src/BinaryOPT/BinaryReader.cpp @@ -43,6 +43,13 @@ namespace PKG return result; } + float_t BinaryReader::ReadSingle() + { + float_t result = 0; + m_File.read(reinterpret_cast(&result), sizeof(float_t)); + return result; + } + char BinaryReader::ReadChar() { char result; @@ -69,7 +76,7 @@ namespace PKG result.resize(length); m_File.read(reinterpret_cast(result.data()), length); - return std::filesystem::u8path(std::string(reinterpret_cast(result.data()), length)).string(); // TODO: fix me! chinese charactor bug + return std::filesystem::u8path(std::string(reinterpret_cast(result.data()), length)).string(); } std::string BinaryReader::ReadNString(const int32_t maxLength) diff --git a/expkg/src/BinaryOPT/BinaryReader.h b/expkg/src/BinaryOPT/BinaryReader.h index 0c9d560..c07b178 100644 --- a/expkg/src/BinaryOPT/BinaryReader.h +++ b/expkg/src/BinaryOPT/BinaryReader.h @@ -25,6 +25,7 @@ namespace PKG int32_t ReadInt32(); uint32_t ReadUInt32(); + float_t ReadSingle(); char ReadChar(); std::string ReadString(uint32_t length); std::string ReadNString(int32_t maxLength = -1); diff --git a/expkg/src/BinaryOPT/BinaryWriter.cpp b/expkg/src/BinaryOPT/BinaryWriter.cpp index 96ce3c3..b1e4dda 100644 --- a/expkg/src/BinaryOPT/BinaryWriter.cpp +++ b/expkg/src/BinaryOPT/BinaryWriter.cpp @@ -26,16 +26,12 @@ namespace PKG BinaryWriter::~BinaryWriter() { - if (!m_File.is_open()) - close(); + close(); } - void BinaryWriter::WriteBytes(const std::string& data, const uint32_t size) + void BinaryWriter::WriteBytes(const char* data, const uint32_t size) { - if (size == 0) - m_File.write(data.data(), data.size()); - else - m_File.write(data.data(), size); + m_File.write(data, size); } void BinaryWriter::WriteString(const std::string& str) @@ -45,6 +41,7 @@ namespace PKG void BinaryWriter::close() { - m_File.close(); + if (!m_File.is_open()) + m_File.close(); } } diff --git a/expkg/src/BinaryOPT/BinaryWriter.h b/expkg/src/BinaryOPT/BinaryWriter.h index 3e168aa..f6ddb8c 100644 --- a/expkg/src/BinaryOPT/BinaryWriter.h +++ b/expkg/src/BinaryOPT/BinaryWriter.h @@ -19,7 +19,7 @@ namespace PKG explicit BinaryWriter(const std::filesystem::path& fileName, std::ios_base::openmode optMode = std::ios::out); ~BinaryWriter(); - void WriteBytes(const std::string& data, uint32_t size = 0); + void WriteBytes(const char* data, uint32_t size); void WriteString(const std::string& str); std::string GetFilePath() const { return m_FilePath; } diff --git a/expkg/src/DXT/DXT.cpp b/expkg/src/DXT/DXT.cpp new file mode 100644 index 0000000..10fa2c6 --- /dev/null +++ b/expkg/src/DXT/DXT.cpp @@ -0,0 +1,230 @@ +// +// Created by sfd on 25-9-21. +// + +#include "DXT.h" + + +namespace PKG +{ + void DXT::DecompressImage(int width, int height, std::vector& data, const DXTFlags flags) + { + std::vector rgba(width * height * 4); + // uint8_t rgba[width * height * 4]; + + // init the block pos + int sourceBlockPos = 0; + int bytesPerBlock = flags == DXTFlags::DXT1 ? 8 : 16; + std::vector targetRGBA(4 * 16); + + // loop over blocks + for (int y = 0; y < height; y += 4) + { + for (int x = 0; x < width; x += 4) + { + // decompress the block + uint8_t targetRGBA_pos = 0; + + if (data.size() == sourceBlockPos) + continue; + + Decompress(targetRGBA, data, sourceBlockPos, flags); + + // Write the decompressed pixels to the correct image locations + for (int py = 0; py < 4; py++) + { + for (int px = 0; px < 4; px++) + { + const int sx = x + px; + const int sy = y + py; + if (sx < width && sy < height) + { + const int targetPixel = 4 * (width * sy + sx); + + rgba[targetPixel + 0] = targetRGBA[targetRGBA_pos + 0]; + rgba[targetPixel + 1] = targetRGBA[targetRGBA_pos + 1]; + rgba[targetPixel + 2] = targetRGBA[targetRGBA_pos + 2]; + rgba[targetPixel + 3] = targetRGBA[targetRGBA_pos + 3]; + + targetRGBA_pos += 4; + } + else + { + // Ignore that pixel + targetRGBA_pos += 4; + } + } + } + sourceBlockPos += bytesPerBlock; + } + } + + data = rgba; + } + + void DXT::Decompress(std::vector& rgba, std::vector& block, const int blockIndex, const DXTFlags flags) + { + // get block locations + int colorBlockIndex = blockIndex; + + if (flags == DXTFlags::DXT3 | flags == DXTFlags::DXT5) + colorBlockIndex += 8; + + DecompressColor(rgba, block, colorBlockIndex, flags == DXTFlags::DXT1); + + + // decompress alpha separately is necessary + if (flags == DXTFlags::DXT3) + DecompressAlphaDxt3(rgba, block, blockIndex); + else if (flags == DXTFlags::DXT5) + DecompressAlphaDxt5(rgba, block, blockIndex); + } + + void DXT::DecompressColor(std::vector& rgba, std::vector& block, const int blockIndex, const bool isDxt1) + { + // unpack end points + std::vector codes(16); + const int a = UnPack565(block, blockIndex, 0, codes, 0); + const int b = UnPack565(block, blockIndex, 2, codes, 4); + + + // generate Midpoints + for (int i = 0; i < 3; i++) + { + const int c = codes[i]; + const int d = codes[4 + i]; + + if (isDxt1 && a <= b) + { + codes[8 + i] = (uint8_t)((c + d) / 2); + codes[12 + i] = 0; + } + else + { + codes[8 + i] = (uint8_t)((2 * c + d) / 3); + codes[12 + i] = (uint8_t)((c + 2 * d) / 3); + } + } + + // Fill in alpha for intermediate values + codes[8 + 3] = 255; + codes[12 + 3] = (isDxt1 && a <= b) ? (uint8_t)0 : (uint8_t)255; + + + // unpack the indices + std::vector indices(16); + for (int i = 0; i < 4; i++) + { + const int packed = block[blockIndex + 4 + i]; + + indices[0 + i * 4] = (uint8_t)(packed & 0x3); + indices[1 + i * 4] = (uint8_t)((packed >> 2) & 0x3); + indices[2 + i * 4] = (uint8_t)((packed >> 4) & 0x3); + indices[3 + i * 4] = (uint8_t)((packed >> 6) & 0x3); + } + + // store out the colours + for (int i = 0; i < 16; i++) + { + const int offset = 4 * indices[i]; + + rgba[4 * i + 0] = codes[offset + 0]; + rgba[4 * i + 1] = codes[offset + 1]; + rgba[4 * i + 2] = codes[offset + 2]; + rgba[4 * i + 3] = codes[offset + 3]; + } + } + + void DXT::DecompressAlphaDxt3(std::vector& rgba, std::vector& block, const int blockIndex) + { + // Unpack the alpha values pairwise + for (int i = 0; i < 8; i++) + { + // Quantise down to 4 bits + int quant = block[blockIndex + i]; + + const uint8_t lo = (uint8_t)(quant & 0x0F); + const uint8_t hi = (uint8_t)(quant & 0xF0); + + // Convert back up to bytes + rgba[8 * i + 3] = (uint8_t)(lo | (lo << 4)); + rgba[8 * i + 7] = (uint8_t)(hi | (hi >> 4)); + } + } + + void DXT::DecompressAlphaDxt5(std::vector& rgba, std::vector& block, const int blockIndex) + { + // Get the two alpha values + uint8_t alpha0 = block[blockIndex + 0]; + uint8_t alpha1 = block[blockIndex + 1]; + + // compare the values to build the codebook + std::vector codes(8); + codes[0] = alpha0; + codes[1] = alpha1; + if (alpha0 <= alpha1) + { + // Use 5-Alpha Codebook + for (int i = 1; i < 5; i++) + codes[1 + i] = (uint8_t)(((5 - i) * alpha0 + i * alpha1) / 5); + codes[6] = 0; + codes[7] = 255; + } + else + { + // Use 7-Alpha Codebook + for (int i = 1; i < 7; i++) + { + codes[i + 1] = (uint8_t)(((7 - i) * alpha0 + i * alpha1) / 7); + } + } + + // decode indices + std::vector indices(16); + uint8_t blockSrc_pos = 2; + uint8_t indices_pos = 0; + for (int i = 0; i < 2; i++) + { + // grab 3 bytes + int value = 0; + for (int j = 0; j < 3; j++) + { + int _byte = block[blockIndex + blockSrc_pos++]; + value |= (_byte << 8 * j); + } + + // unpack 8 3-bit values from it + for (int j = 0; j < 8; j++) + { + int index = (value >> 3 * j) & 0x07; + indices[indices_pos++] = (uint8_t)index; + } + } + + // write out the indexed codebook values + for (int i = 0; i < 16; i++) + { + rgba[4 * i + 3] = codes[indices[i]]; + } + } + + int DXT::UnPack565(std::vector& block, const int blockIndex, const int packedOffset, std::vector& color, const int colorOffset) + { + // build packed value + const int value = block[blockIndex + packedOffset] | (block[blockIndex + packedOffset + 1] << 8); + + // get components in the stored range + const uint16_t red = ((value >> 11) & 0x1F); + const uint16_t green = ((value >> 5) & 0x3F); + const uint16_t blue = (value & 0x1F); + + + // Scale up to 8 Bit + color[0 + colorOffset] = (uint8_t)((red << 3) | (red >> 2)); + color[1 + colorOffset] = (uint8_t)((green << 2) | (green >> 4)); + color[2 + colorOffset] = (uint8_t)((blue << 3) | (blue >> 2)); + color[3 + colorOffset] = 255; + + return value; + } +} diff --git a/expkg/src/DXT/DXT.h b/expkg/src/DXT/DXT.h new file mode 100644 index 0000000..1e2c335 --- /dev/null +++ b/expkg/src/DXT/DXT.h @@ -0,0 +1,38 @@ +// +// Created by sfd on 25-9-21. +// + +#ifndef DXT_H +#define DXT_H +#include +#include + +namespace PKG +{ + enum class DXTFlags + { + DXT1 = 1, + DXT3 = 1 << 1, + DXT5 = 1 << 2, + }; + + + class DXT + { + public: + // public static byte[] DecompressImage(int width, int height, byte[] data, DXTFlags flags) + + static void DecompressImage(int width, int height, std::vector& data, DXTFlags flags); + + private: + static void Decompress(std::vector& rgba, std::vector& block, int blockIndex, DXTFlags flags); + + static int UnPack565(std::vector& block, int blockIndex, int packedOffset, std::vector& color, int colorOffset); + static void DecompressColor(std::vector& rgba, std::vector& block, int blockIndex, bool isDxt1); + static void DecompressAlphaDxt3(std::vector& rgba, std::vector& block, int blockIndex); + static void DecompressAlphaDxt5(std::vector& rgba, std::vector& block, int blockIndex); + }; +} + + +#endif //DXT_H diff --git a/expkg/src/EXPKG/EXPKG.cpp b/expkg/src/EXPKG/EXPKG.cpp index 5c565c3..f02fffd 100644 --- a/expkg/src/EXPKG/EXPKG.cpp +++ b/expkg/src/EXPKG/EXPKG.cpp @@ -6,8 +6,10 @@ #include +#include "gif.h" #include "BinaryOPT/BinaryWriter.h" #include "BinaryOPT/ImageReader.h" +#include "DXT/DXT.h" #include "Tex/Tex.h" extern "C" unsigned char* stbi_write_png_to_mem(const unsigned char* pixels, int stride_bytes, int x, int y, int n, @@ -53,14 +55,14 @@ example: Run(); } - EXPKG::EXPKG(const std::filesystem::path& filePath, const std::filesystem::path& outDir) + EXPKG::EXPKG(const std::string& filePath, const std::string& outDir) { m_Reader = std::make_shared(filePath); m_OutDir = outDir; m_OutDir = m_OutDir.make_preferred(); - - Run(); + if (!m_Reader) + Run(); } FILE_EXTENSION EXPKG::checkExtension(const std::filesystem::path& filePath) @@ -72,6 +74,7 @@ example: if (filePath.extension() == ".tex") return FILE_EXTENSION::TEX; + std::cerr << "not a pkg file or mpkg file or tex file" << std::endl; return FILE_EXTENSION::UNKNOWN; } @@ -149,7 +152,7 @@ example: std::string texdata; m_Reader->ReadData(texdata, entry.Length); - writer.WriteBytes(texdata); + writer.WriteBytes(texdata.data(), texdata.size()); writer.close(); @@ -165,7 +168,7 @@ example: std::string texdata; m_Reader->ReadData(texdata, entry.Length); - writer.WriteBytes(texdata); + writer.WriteBytes(texdata.data(), texdata.size()); writer.close(); } else @@ -176,7 +179,7 @@ example: } } - void EXPKG::ExtractTex(const std::filesystem::path& path) + void EXPKG::ExtractTex(const std::filesystem::path& path) const { std::shared_ptr reader = m_Reader; if (path != "") @@ -214,12 +217,6 @@ example: if ((int)tex.Header.Flags & (int)TexType::IsVideoTexture) tex.IsVideoTexture = true; tex.ImageContainer = ImageReader::ImageContainerReaderReadFrom(*reader, tex.Header.Format); - - if (tex.IsGif) - { - // TODO: to impl this - //Read Frame - } // ReadHeader end if (!tex.ImageContainer.Images.empty()) @@ -231,15 +228,28 @@ example: else format = tex.ImageContainer.Images[0].Mipmaps[0].Format; - switch (format) + auto tmpfotmat = format; + for(auto& Image : tex.ImageContainer.Images) { - case MipmapFormat::CompressedDXT5: - case MipmapFormat::CompressedDXT3: - case MipmapFormat::CompressedDXT1: - std::cerr << "raw mipmap meybe compressed" << std::endl; - break; - default: - break; + auto& mipmap = Image.Mipmaps[0]; + switch (tmpfotmat) + { + case MipmapFormat::CompressedDXT5: + DXT::DecompressImage(mipmap.Width, mipmap.Height, mipmap.Data, DXTFlags::DXT5); + mipmap.Format = MipmapFormat::RGBA8888; format = MipmapFormat::RGBA8888; + break; + case MipmapFormat::CompressedDXT3: + DXT::DecompressImage(mipmap.Width, mipmap.Height, mipmap.Data, DXTFlags::DXT3); + mipmap.Format = MipmapFormat::RGBA8888; format = MipmapFormat::RGBA8888; + break; + case MipmapFormat::CompressedDXT1: + DXT::DecompressImage(mipmap.Width, mipmap.Height, mipmap.Data, DXTFlags::DXT1); + mipmap.Format = MipmapFormat::RGBA8888; format = MipmapFormat::RGBA8888; + break; + default: + std::cerr << "raw mipmap meybe compressed" << std::endl; + break; + } } if ((int)format >= 1 && (int)format <= 3) @@ -253,98 +263,281 @@ example: if (tex.IsGif) { - // TODO: to impl it // convert gif - } - auto& sourceMipmap = tex.ImageContainer.Images[0].Mipmaps[0]; + // tex.GifContainer + auto& container = tex.FrameInfoContainer; - if (tex.IsVideoTexture) - { - if (sourceMipmap.Data.size() < 12) + container.Magic = reader->ReadNString(16); + + int frameCount = reader->ReadInt32(); + + /* + switch (container.Magic) { - std::cerr << "expect mp4 magic header" << std::endl; - } - - std::string mp4Magic = std::string(reinterpret_cast(&sourceMipmap.Data[4]), 8); - - if (mp4Magic != "ftypisom" && mp4Magic != "ftypmsnv" && mp4Magic != "ftypmp42") - { - std::cerr << "bad mp4 magic header" << std::endl; - } - } - else - { - auto imgformat = sourceMipmap.Format; - - switch (imgformat) - { - case MipmapFormat::CompressedDXT5: - case MipmapFormat::CompressedDXT3: - case MipmapFormat::CompressedDXT1: - std::cerr << "raw mipmap meybe compressed" << std::endl; - default: - break; - } - - if ((int)imgformat >= 1 && (int)imgformat <= 3) - { - int len; - auto& imgData = tex.ImageContainer.Images[0].Mipmaps[0].Data; - - uint8_t* data = nullptr; - - switch (imgformat) - { - case MipmapFormat::R8: - data = stbi_write_png_to_mem(sourceMipmap.Data.data(), - sourceMipmap.Width * 1, - sourceMipmap.Width, - sourceMipmap.Height, - 1, - &len); + case "TEXS0001": + case "TEXS0002": break; - case MipmapFormat::RG88: - data = stbi_write_png_to_mem(sourceMipmap.Data.data(), - sourceMipmap.Width * 2, - sourceMipmap.Width, - sourceMipmap.Height, - 2, - &len); - break; - case MipmapFormat::RGBA8888: - data = stbi_write_png_to_mem(sourceMipmap.Data.data(), - sourceMipmap.Width * 4, - sourceMipmap.Width, - sourceMipmap.Height, - 4, - &len); + case "TEXS0003": + container.GifWidth = reader->ReadInt32(); + container.GifHeight = reader->ReadInt32(); break; default: - break; - } + std::cerr << "bad magic" << std::endl; + } + */ - if (data) + { + if (container.Magic == "TEXS0001" || container.Magic == "TEXS0002") { - imgData.assign(data, data + len); - free(data); + }else if (container.Magic == "TEXS0003") + { + container.GifWidth = reader->ReadInt32(); + container.GifHeight = reader->ReadInt32(); + }else + { + std::cerr << "bad magic" << std::endl; } } + + + /* + switch (container.Magic) + { + case "TEXS0001": + for (int i = 0; i < frameCount; i++) + { + TexFrameInfo frameInfo = {}; + frameInfo.ImageId = reader->ReadInt32(); + frameInfo.Frametime = reader->ReadSingle(); + frameInfo.X = reader->ReadInt32(); + frameInfo.Y = reader->ReadInt32(); + frameInfo.Width = reader->ReadInt32(); + frameInfo.WidthY = reader->ReadInt32(); + frameInfo.HeightX = reader->ReadInt32(); + frameInfo.Height = reader->ReadInt32(); + + container.Frames.push_back(frameInfo); + } + case "TEXS0002": + case "TEXS0003": + for (int i = 0; i < frameCount; i++) + { + TexFrameInfo frameInfo = {}; + frameInfo.ImageId = reader->ReadInt32(); + frameInfo.Frametime = reader->ReadSingle(); + frameInfo.X = reader->ReadSingle(); + frameInfo.Y = reader->ReadSingle(); + frameInfo.Width = reader->ReadSingle(); + frameInfo.WidthY = reader->ReadSingle(); + frameInfo.HeightX = reader->ReadSingle(); + frameInfo.Height = reader->ReadSingle(); + + container.Frames.push_back(frameInfo); + } + default: + std::cerr << "bad magic" << std::endl; + } + */ + { + if (container.Magic == "TEXS0001") + { + for (int i = 0; i < frameCount; i++) + { + TexFrameInfo frameInfo = {}; + frameInfo.ImageId = reader->ReadInt32(); + frameInfo.Frametime = reader->ReadSingle(); + frameInfo.PosX = reader->ReadInt32(); + frameInfo.PosY = reader->ReadInt32(); + frameInfo.Width = reader->ReadInt32(); + frameInfo.WidthY = reader->ReadInt32(); + frameInfo.HeightX = reader->ReadInt32(); + frameInfo.Height = reader->ReadInt32(); + + container.Frames.push_back(frameInfo); + } + }else if (container.Magic == "TEXS0002" || container.Magic == "TEXS0003") + { + for (int i = 0; i < frameCount; i++) + { + TexFrameInfo frameInfo = {}; + frameInfo.ImageId = reader->ReadInt32(); + frameInfo.Frametime = reader->ReadSingle(); + frameInfo.PosX = reader->ReadSingle(); + frameInfo.PosY = reader->ReadSingle(); + frameInfo.Width = reader->ReadSingle(); + frameInfo.WidthY = reader->ReadSingle(); + frameInfo.HeightX = reader->ReadSingle(); + frameInfo.Height = reader->ReadSingle(); + + container.Frames.push_back(frameInfo); + } + }else + { + std::cerr << "bad magic" << std::endl; + } + } + + + if (container.GifWidth == 0 || container.GifHeight == 0) + { + container.GifWidth = (int) container.Frames[0].Width; + container.GifHeight = (int) container.Frames[0].Height; + } + + + std::filesystem::path outPath = reader->GetFilePath(); + outPath.replace_extension("gif"); + + std::cout << "convert file: " << outPath << std::endl; + + GifWriter writer; + + uint32_t delay = (int)(tex.FrameInfoContainer.Frames[0].Frametime * 100); + + GifBegin(&writer, + outPath.string().c_str(), + (uint32_t)tex.FrameInfoContainer.Frames[0].Width, + (uint32_t)tex.FrameInfoContainer.Frames[0].Height, + delay); + + + int frameIndex = 0; + int imageIndex = 1; + + for (const auto& [Mipmap] : tex.ImageContainer.Images) + { + + const auto& Image = Mipmap[0]; + + int SingleImageFrameCount = (Image.Width / container.GifWidth) * (Image.Height / container.GifHeight); + + + for (; frameIndex < SingleImageFrameCount * imageIndex; frameIndex++) + { + const auto& frameInfo = container.Frames[frameIndex]; + std::vector frameImage; + + + for (int heightIndex = 0; heightIndex < container.GifHeight; heightIndex++) + { + auto lineDataStart = Image.Data.begin() + Image.Width * 4 * ((int)frameInfo.PosY + heightIndex)+ (int)frameInfo.PosX * 4; + auto lineData = std::vector(lineDataStart, lineDataStart + (int)frameInfo.Width * 4); + + frameImage.insert(frameImage.end(), lineData.begin(), lineData.end()); + } + + // output by gif + { + GifWriteFrame(&writer, frameImage.data(), (uint32_t)frameInfo.Width, (uint32_t)frameInfo.Height, delay); + } + + + // output one by one + /* + { + int len = 0; + static int index = 0; + + auto* data = stbi_write_png_to_mem(frameImage.data(), + (int)frameInfo.Width * 4, + (int)frameInfo.Width, + (int)frameInfo.Height, + 4, + &len); + + std::filesystem::path outputfile = outPath; + std::string name = outputfile.filename().string(); + outputfile = outputfile.parent_path(); + + + std::string filename = std::to_string(index++) + "_" + name; + + outputfile /= "out"; + outputfile /= filename; + + std::cout << "convert file: " << outputfile << std::endl; + + BinaryWriter imageWriter(outputfile, std::ios::binary); + imageWriter.WriteBytes(reinterpret_cast(data), len); + imageWriter.close(); + + free(data); + data = nullptr; + } + */ + } + imageIndex ++; + } + + GifEnd(&writer); + + }else + { + auto& sourceMipmap = tex.ImageContainer.Images[0].Mipmaps[0]; + + if (tex.IsVideoTexture) + { + if (sourceMipmap.Data.size() < 12) + { + std::cerr << "expect mp4 magic header" << std::endl; + } + + std::string mp4Magic = std::string(reinterpret_cast(&sourceMipmap.Data[4]), 8); + + if (mp4Magic != "ftypisom" && mp4Magic != "ftypmsnv" && mp4Magic != "ftypmp42") + { + std::cerr << "bad mp4 magic header" << std::endl; + } + } + else + { + auto imgformat = sourceMipmap.Format; + + if ((int)imgformat >= 1 && (int)imgformat <= 3) + { + int len = 0; + auto& imgData = tex.ImageContainer.Images[0].Mipmaps[0].Data; + + uint8_t* data = nullptr; + int channel = 4; + + switch (imgformat) + { + case MipmapFormat::R8: channel = 1; break; + case MipmapFormat::RG88: channel = 2; break; + case MipmapFormat::RGBA8888: channel = 4; break; + default: break; + } + + data = stbi_write_png_to_mem(sourceMipmap.Data.data(), + sourceMipmap.Width * channel, + sourceMipmap.Width, + sourceMipmap.Height, + channel, + &len); + + if (data) + { + imgData.assign(data, data + len); + free(data); + } + } + } + + + // return ImageResult + // data format + std::filesystem::path outPath = reader->GetFilePath(); + outPath.replace_extension(GetFileExtension(format)); + + std::cout << "convert file: " << outPath << std::endl; + + BinaryWriter imageWriter(outPath, std::ios::binary); + imageWriter.WriteBytes(reinterpret_cast(sourceMipmap.Data.data()), sourceMipmap.Data.size()); + imageWriter.close(); + // Convert source end } - - - // return ImageResult - // data format - std::filesystem::path outPath = reader->GetFilePath(); - outPath.replace_extension(GetFileExtension(format)); - - std::cout << "convert file: " << outPath << std::endl; - - BinaryWriter imageWriter(outPath, std::ios::binary); - imageWriter.WriteBytes(std::string(reinterpret_cast(sourceMipmap.Data.data()), - sourceMipmap.Data.size())); - imageWriter.close(); - // Convert source end } } } diff --git a/expkg/src/EXPKG/EXPKG.h b/expkg/src/EXPKG/EXPKG.h index 87f62d8..11f226d 100644 --- a/expkg/src/EXPKG/EXPKG.h +++ b/expkg/src/EXPKG/EXPKG.h @@ -25,13 +25,13 @@ namespace PKG { public: EXPKG(const CommandArgs& commandArgs); - EXPKG(const std::filesystem::path& filePath, const std::filesystem::path& outDir = "out"); + EXPKG(const std::string& filePath, const std::string& outDir = "out"); private: static FILE_EXTENSION checkExtension(const std::filesystem::path& filePath); - void ExtractTex(const std::filesystem::path& path = ""); + void ExtractTex(const std::filesystem::path& path = "") const; void ExtractPkg(); void Run(); diff --git a/expkg/src/Tex/Tex.h b/expkg/src/Tex/Tex.h index 516fec4..9e8579a 100644 --- a/expkg/src/Tex/Tex.h +++ b/expkg/src/Tex/Tex.h @@ -5,6 +5,7 @@ #ifndef TEX_H #define TEX_H +#include "TexFrameInfoContainer.h" #include "TexImageContainer.h" @@ -21,7 +22,7 @@ namespace PKG bool IsGif = false; bool IsVideoTexture = false; - // std::optional FrameInfoContainer = {}; + TexFrameInfoContainer FrameInfoContainer = {}; }; } diff --git a/expkg/src/Tex/TexFrameInfoContainer.cpp b/expkg/src/Tex/TexFrameInfoContainer.cpp new file mode 100644 index 0000000..825a83a --- /dev/null +++ b/expkg/src/Tex/TexFrameInfoContainer.cpp @@ -0,0 +1,5 @@ +// +// Created by sfd on 25-9-21. +// + +#include "TexFrameInfoContainer.h" diff --git a/expkg/src/Tex/TexFrameInfoContainer.h b/expkg/src/Tex/TexFrameInfoContainer.h new file mode 100644 index 0000000..c8743cf --- /dev/null +++ b/expkg/src/Tex/TexFrameInfoContainer.h @@ -0,0 +1,39 @@ +// +// Created by sfd on 25-9-21. +// + +#ifndef TEXFRAMEINFOCONTAINER_H +#define TEXFRAMEINFOCONTAINER_H +#include +#include + + +namespace PKG +{ + + struct TexFrameInfo + { + int ImageId; + float Frametime; + float PosX; + float PosY; + float Width; + float WidthY; + float HeightX; + float Height; + }; + + + class TexFrameInfoContainer + { + public: + std::string Magic; + + std::vector Frames; + int GifWidth; + int GifHeight; + }; +} + + +#endif //TEXFRAMEINFOCONTAINER_H diff --git a/expkg/vendor/gif-h/LICENSE b/expkg/vendor/gif-h/LICENSE new file mode 100644 index 0000000..6bb8a29 --- /dev/null +++ b/expkg/vendor/gif-h/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/expkg/vendor/gif-h/gif.h b/expkg/vendor/gif-h/gif.h new file mode 100644 index 0000000..1c614b4 --- /dev/null +++ b/expkg/vendor/gif-h/gif.h @@ -0,0 +1,864 @@ +// +// gif.h +// by Charlie Tangora +// Public domain. +// Email me : ctangora -at- gmail -dot- com +// +// This file offers a simple, very limited way to create animated GIFs directly in code. +// +// Those looking for particular cleverness are likely to be disappointed; it's pretty +// much a straight-ahead implementation of the GIF format with optional Floyd-Steinberg +// dithering. (It does at least use delta encoding - only the changed portions of each +// frame are saved.) +// +// So resulting files are often quite large. The hope is that it will be handy nonetheless +// as a quick and easily-integrated way for programs to spit out animations. +// +// Only RGBA8 is currently supported as an input format. (The alpha is ignored.) +// +// If capturing a buffer with a bottom-left origin (such as OpenGL), define GIF_FLIP_VERT +// to automatically flip the buffer data when writing the image (the buffer itself is +// unchanged. +// +// USAGE: +// Create a GifWriter struct. Pass it to GifBegin() to initialize and write the header. +// Pass subsequent frames to GifWriteFrame(). +// Finally, call GifEnd() to close the file handle and free memory. +// + +#ifndef gif_h +#define gif_h + +#include // for FILE* +#include // for memcpy and bzero +#include // for integer typedefs +#include // for bool macros + +// Define these macros to hook into a custom memory allocator. +// TEMP_MALLOC and TEMP_FREE will only be called in stack fashion - frees in the reverse order of mallocs +// and any temp memory allocated by a function will be freed before it exits. +// MALLOC and FREE are used only by GifBegin and GifEnd respectively (to allocate a buffer the size of the image, which +// is used to find changed pixels for delta-encoding.) + +#ifndef GIF_TEMP_MALLOC +#include +#define GIF_TEMP_MALLOC malloc +#endif + +#ifndef GIF_TEMP_FREE +#include +#define GIF_TEMP_FREE free +#endif + +#ifndef GIF_MALLOC +#include +#define GIF_MALLOC malloc +#endif + +#ifndef GIF_FREE +#include +#define GIF_FREE free +#endif + +const int kGifTransIndex = 0; + +typedef struct +{ + int bitDepth; + + uint8_t r[256]; + uint8_t g[256]; + uint8_t b[256]; + + // k-d tree over RGB space, organized in heap fashion + // i.e. left child of node i is node i*2, right child is node i*2+1 + // nodes 256-511 are implicitly the leaves, containing a color + uint8_t treeSplitElt[256]; + uint8_t treeSplit[256]; +} GifPalette; + +// max, min, and abs functions +int GifIMax(int l, int r) { return l>r?l:r; } +int GifIMin(int l, int r) { return l (1<bitDepth)-1) + { + int ind = treeRoot-(1<bitDepth); + if(ind == kGifTransIndex) return; + + // check whether this color is better than the current winner + int r_err = r - ((int32_t)pPal->r[ind]); + int g_err = g - ((int32_t)pPal->g[ind]); + int b_err = b - ((int32_t)pPal->b[ind]); + int diff = GifIAbs(r_err)+GifIAbs(g_err)+GifIAbs(b_err); + + if(diff < *bestDiff) + { + *bestInd = ind; + *bestDiff = diff; + } + + return; + } + + // take the appropriate color (r, g, or b) for this node of the k-d tree + int comps[3]; comps[0] = r; comps[1] = g; comps[2] = b; + int splitComp = comps[pPal->treeSplitElt[treeRoot]]; + + int splitPos = pPal->treeSplit[treeRoot]; + if(splitPos > splitComp) + { + // check the left subtree + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot*2); + if( *bestDiff > splitPos - splitComp ) + { + // cannot prove there's not a better value in the right subtree, check that too + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot*2+1); + } + } + else + { + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot*2+1); + if( *bestDiff > splitComp - splitPos ) + { + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot*2); + } + } +} + +void GifSwapPixels(uint8_t* image, int pixA, int pixB) +{ + uint8_t rA = image[pixA*4]; + uint8_t gA = image[pixA*4+1]; + uint8_t bA = image[pixA*4+2]; + uint8_t aA = image[pixA*4+3]; + + uint8_t rB = image[pixB*4]; + uint8_t gB = image[pixB*4+1]; + uint8_t bB = image[pixB*4+2]; + uint8_t aB = image[pixA*4+3]; + + image[pixA*4] = rB; + image[pixA*4+1] = gB; + image[pixA*4+2] = bB; + image[pixA*4+3] = aB; + + image[pixB*4] = rA; + image[pixB*4+1] = gA; + image[pixB*4+2] = bA; + image[pixB*4+3] = aA; +} + +// just the partition operation from quicksort +int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotValue) +{ + int storeIndex = left; + bool split = 0; + for(int ii=left; ii neededCenter) + GifPartitionByMedian(image, left, pivotIndex, com, neededCenter); + + if(pivotIndex < neededCenter) + GifPartitionByMedian(image, pivotIndex+1, right, com, neededCenter); + } +} + +// Just partition around a given pivot, returning the split point +int GifPartitionByMean(uint8_t* image, int left, int right, int com, int neededMean) +{ + if(left < right-1) + { + return GifPartition(image, left, right-1, com, neededMean); + } + return left; +} + +// Builds a palette by creating a balanced k-d tree of all pixels in the image +void GifSplitPalette(uint8_t* image, int numPixels, int treeNode, int treeLevel, bool buildForDither, GifPalette* pal) +{ + if(numPixels == 0) + return; + + int numColors = (1 << pal->bitDepth); + + // base case, bottom of the tree + if(treeNode >= numColors) + { + int entry = treeNode - numColors; + + if(buildForDither) + { + // Dithering needs at least one color as dark as anything + // in the image and at least one brightest color - + // otherwise it builds up error and produces strange artifacts + if( entry == 1 ) + { + // special case: the darkest color in the image + uint32_t r=255, g=255, b=255; + for(int ii=0; iir[entry] = (uint8_t)r; + pal->g[entry] = (uint8_t)g; + pal->b[entry] = (uint8_t)b; + + return; + } + + if( entry == numColors-1 ) + { + // special case: the lightest color in the image + uint32_t r=0, g=0, b=0; + for(int ii=0; iir[entry] = (uint8_t)r; + pal->g[entry] = (uint8_t)g; + pal->b[entry] = (uint8_t)b; + + return; + } + } + + // otherwise, take the average of all colors in this subcube + uint64_t r=0, g=0, b=0; + for(int ii=0; iir[entry] = (uint8_t)r; + pal->g[entry] = (uint8_t)g; + pal->b[entry] = (uint8_t)b; + + return; + } + + // Find the axis with the largest range + int minR = 255, maxR = 0; + int minG = 255, maxG = 0; + int minB = 255, maxB = 0; + for(int ii=0; ii maxR) maxR = r; + if(r < minR) minR = r; + + if(g > maxG) maxG = g; + if(g < minG) minG = g; + + if(b > maxB) maxB = b; + if(b < minB) minB = b; + } + + int rRange = maxR - minR; + int gRange = maxG - minG; + int bRange = maxB - minB; + + // and split along that axis. (incidentally, this means this isn't a "proper" k-d tree but I don't know what else to call it) + int splitCom = 1; int rangeMin = minG; int rangeMax = maxG; + if(bRange > gRange) { splitCom = 2; rangeMin = minB; rangeMax = maxB; } + if(rRange > bRange && rRange > gRange) { splitCom = 0; rangeMin = minR; rangeMax = maxR; } + + int subPixelsA = numPixels / 2; + + GifPartitionByMedian(image, 0, numPixels, splitCom, subPixelsA); + int splitValue = image[subPixelsA*4+splitCom]; + + // if the split is very unbalanced, split at the mean instead of the median to preserve rare colors + int splitUnbalance = GifIAbs( (splitValue - rangeMin) - (rangeMax - splitValue) ); + if( splitUnbalance > (1536 >> treeLevel) ) + { + splitValue = rangeMin + (rangeMax-rangeMin) / 2; + subPixelsA = GifPartitionByMean(image, 0, numPixels, splitCom, splitValue); + } + + // add the bottom node for the transparency index + if( treeNode == numColors/2 ) + { + subPixelsA = 0; + splitValue = 0; + } + + int subPixelsB = numPixels-subPixelsA; + pal->treeSplitElt[treeNode] = (uint8_t)splitCom; + pal->treeSplit[treeNode] = (uint8_t)splitValue; + + GifSplitPalette(image, subPixelsA, treeNode*2, treeLevel+1, buildForDither, pal); + GifSplitPalette(image+subPixelsA*4, subPixelsB, treeNode*2+1, treeLevel+1, buildForDither, pal); +} + +// Finds all pixels that have changed from the previous image and +// moves them to the fromt of th buffer. +// This allows us to build a palette optimized for the colors of the +// changed pixels only. +int GifPickChangedPixels( const uint8_t* lastFrame, uint8_t* frame, int numPixels ) +{ + int numChanged = 0; + uint8_t* writeIter = frame; + + for (int ii=0; iibitDepth = bitDepth; + + // SplitPalette is destructive (it sorts the pixels by color) so + // we must create a copy of the image for it to destroy + size_t imageSize = (size_t)(width * height * 4 * sizeof(uint8_t)); + uint8_t* destroyableImage = (uint8_t*)GIF_TEMP_MALLOC(imageSize); + memcpy(destroyableImage, nextFrame, imageSize); + + int numPixels = (int)(width * height); + if(lastFrame) + numPixels = GifPickChangedPixels(lastFrame, destroyableImage, numPixels); + + GifSplitPalette(destroyableImage, numPixels, 1, 0, buildForDither, pPal); + + GIF_TEMP_FREE(destroyableImage); + + // add the bottom node for the transparency index + pPal->treeSplit[1 << (bitDepth-1)] = 0; + pPal->treeSplitElt[1 << (bitDepth-1)] = 0; + + pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; +} + +// Implements Floyd-Steinberg dithering, writes palette value to alpha +void GifDitherImage( const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal ) +{ + int numPixels = (int)(width * height); + + // quantPixels initially holds color*256 for all pixels + // The extra 8 bits of precision allow for sub-single-color error values + // to be propagated + int32_t *quantPixels = (int32_t *)GIF_TEMP_MALLOC(sizeof(int32_t) * (size_t)numPixels * 4); + + for( int ii=0; iir[bestInd]) * 256; + int32_t g_err = nextPix[1] - (int32_t)(pPal->g[bestInd]) * 256; + int32_t b_err = nextPix[2] - (int32_t)(pPal->b[bestInd]) * 256; + + nextPix[0] = pPal->r[bestInd]; + nextPix[1] = pPal->g[bestInd]; + nextPix[2] = pPal->b[bestInd]; + nextPix[3] = bestInd; + + // Propagate the error to the four adjacent locations + // that we haven't touched yet + int quantloc_7 = (int)(yy * width + xx + 1); + int quantloc_3 = (int)(yy * width + width + xx - 1); + int quantloc_5 = (int)(yy * width + width + xx); + int quantloc_1 = (int)(yy * width + width + xx + 1); + + if(quantloc_7 < numPixels) + { + int32_t* pix7 = quantPixels+4*quantloc_7; + pix7[0] += GifIMax( -pix7[0], r_err * 7 / 16 ); + pix7[1] += GifIMax( -pix7[1], g_err * 7 / 16 ); + pix7[2] += GifIMax( -pix7[2], b_err * 7 / 16 ); + } + + if(quantloc_3 < numPixels) + { + int32_t* pix3 = quantPixels+4*quantloc_3; + pix3[0] += GifIMax( -pix3[0], r_err * 3 / 16 ); + pix3[1] += GifIMax( -pix3[1], g_err * 3 / 16 ); + pix3[2] += GifIMax( -pix3[2], b_err * 3 / 16 ); + } + + if(quantloc_5 < numPixels) + { + int32_t* pix5 = quantPixels+4*quantloc_5; + pix5[0] += GifIMax( -pix5[0], r_err * 5 / 16 ); + pix5[1] += GifIMax( -pix5[1], g_err * 5 / 16 ); + pix5[2] += GifIMax( -pix5[2], b_err * 5 / 16 ); + } + + if(quantloc_1 < numPixels) + { + int32_t* pix1 = quantPixels+4*quantloc_1; + pix1[0] += GifIMax( -pix1[0], r_err / 16 ); + pix1[1] += GifIMax( -pix1[1], g_err / 16 ); + pix1[2] += GifIMax( -pix1[2], b_err / 16 ); + } + } + } + + // Copy the palettized result to the output buffer + for( int ii=0; iir[bestInd]; + outFrame[1] = pPal->g[bestInd]; + outFrame[2] = pPal->b[bestInd]; + outFrame[3] = (uint8_t)bestInd; + } + + if(lastFrame) lastFrame += 4; + outFrame += 4; + nextFrame += 4; + } +} + +// Simple structure to write out the LZW-compressed portion of the image +// one bit at a time +typedef struct +{ + uint32_t chunkIndex; + uint8_t chunk[256]; // bytes are written in here until we have 256 of them, then written to the file + + uint8_t bitIndex; // how many bits in the partial byte written so far + uint8_t byte; // current partial byte + + uint8_t padding[2]; // make padding explicit +} GifBitStatus; + +// insert a single bit +void GifWriteBit( GifBitStatus* stat, uint32_t bit ) +{ + bit = bit & 1; + bit = bit << stat->bitIndex; + stat->byte |= bit; + + ++stat->bitIndex; + if( stat->bitIndex > 7 ) + { + // move the newly-finished byte to the chunk buffer + stat->chunk[stat->chunkIndex++] = stat->byte; + // and start a new byte + stat->bitIndex = 0; + stat->byte = 0; + } +} + +// write all bytes so far to the file +void GifWriteChunk( FILE* f, GifBitStatus* stat ) +{ + fputc((int)stat->chunkIndex, f); + fwrite(stat->chunk, 1, stat->chunkIndex, f); + + stat->bitIndex = 0; + stat->byte = 0; + stat->chunkIndex = 0; +} + +void GifWriteCode( FILE* f, GifBitStatus* stat, uint32_t code, uint32_t length ) +{ + for( uint32_t ii=0; ii> 1; + + if( stat->chunkIndex == 255 ) + { + GifWriteChunk(f, stat); + } + } +} + +// The LZW dictionary is a 256-ary tree constructed as the file is encoded, +// this is one node +typedef struct +{ + uint16_t m_next[256]; +} GifLzwNode; + +// write a 256-color (8-bit) image palette to the file +void GifWritePalette( const GifPalette* pPal, FILE* f ) +{ + fputc(0, f); // first color: transparency + fputc(0, f); + fputc(0, f); + + for(int ii=1; ii<(1 << pPal->bitDepth); ++ii) + { + uint32_t r = pPal->r[ii]; + uint32_t g = pPal->g[ii]; + uint32_t b = pPal->b[ii]; + + fputc((int)r, f); + fputc((int)g, f); + fputc((int)b, f); + } +} + +// write the image header, LZW-compress and write out the image +void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal) +{ + // graphics control extension + fputc(0x21, f); + fputc(0xf9, f); + fputc(0x04, f); + fputc(0x05, f); // leave prev frame in place, this frame has transparency + fputc(delay & 0xff, f); + fputc((delay >> 8) & 0xff, f); + fputc(kGifTransIndex, f); // transparent color index + fputc(0, f); + + fputc(0x2c, f); // image descriptor block + + fputc(left & 0xff, f); // corner of image in canvas space + fputc((left >> 8) & 0xff, f); + fputc(top & 0xff, f); + fputc((top >> 8) & 0xff, f); + + fputc(width & 0xff, f); // width and height of image + fputc((width >> 8) & 0xff, f); + fputc(height & 0xff, f); + fputc((height >> 8) & 0xff, f); + + //fputc(0, f); // no local color table, no transparency + //fputc(0x80, f); // no local color table, but transparency + + fputc(0x80 + pPal->bitDepth-1, f); // local color table present, 2 ^ bitDepth entries + GifWritePalette(pPal, f); + + const int minCodeSize = pPal->bitDepth; + const uint32_t clearCode = 1 << pPal->bitDepth; + + fputc(minCodeSize, f); // min code size 8 bits + + GifLzwNode* codetree = (GifLzwNode*)GIF_TEMP_MALLOC(sizeof(GifLzwNode)*4096); + + memset(codetree, 0, sizeof(GifLzwNode)*4096); + int32_t curCode = -1; + uint32_t codeSize = (uint32_t)minCodeSize + 1; + uint32_t maxCode = clearCode+1; + + GifBitStatus stat; + stat.byte = 0; + stat.bitIndex = 0; + stat.chunkIndex = 0; + + GifWriteCode(f, &stat, clearCode, codeSize); // start with a fresh LZW dictionary + + for(uint32_t yy=0; yy= (1ul << codeSize) ) + { + // dictionary entry count has broken a size barrier, + // we need more bits for codes + codeSize++; + } + if( maxCode == 4095 ) + { + // the dictionary is full, clear it out and begin anew + GifWriteCode(f, &stat, clearCode, codeSize); // clear tree + + memset(codetree, 0, sizeof(GifLzwNode)*4096); + codeSize = (uint32_t)(minCodeSize + 1); + maxCode = clearCode+1; + } + + curCode = nextValue; + } + } + } + + // compression footer + GifWriteCode(f, &stat, (uint32_t)curCode, codeSize); + GifWriteCode(f, &stat, clearCode, codeSize); + GifWriteCode(f, &stat, clearCode + 1, (uint32_t)minCodeSize + 1); + + // write out the last partial chunk + while( stat.bitIndex ) GifWriteBit(&stat, 0); + if( stat.chunkIndex ) GifWriteChunk(f, &stat); + + fputc(0, f); // image block terminator + + GIF_TEMP_FREE(codetree); +} + +typedef struct +{ + FILE* f; + uint8_t* oldImage; + bool firstFrame; + + uint8_t padding[7]; // make padding explicit +} GifWriter; + +// Creates a gif file. +// The input GIFWriter is assumed to be uninitialized. +// The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. +bool GifBegin( GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth = 8, bool dither = false ) +{ + (void)bitDepth; (void)dither; // Mute "Unused argument" warnings +#if defined(_MSC_VER) && (_MSC_VER >= 1400) + writer->f = 0; + fopen_s(&writer->f, filename, "wb"); +#else + writer->f = fopen(filename, "wb"); +#endif + if(!writer->f) return false; + + writer->firstFrame = true; + + // allocate + writer->oldImage = (uint8_t*)GIF_MALLOC(width*height*4); + + fputs("GIF89a", writer->f); + + // screen descriptor + fputc(width & 0xff, writer->f); + fputc((width >> 8) & 0xff, writer->f); + fputc(height & 0xff, writer->f); + fputc((height >> 8) & 0xff, writer->f); + + fputc(0xf0, writer->f); // there is an unsorted global color table of 2 entries + fputc(0, writer->f); // background color + fputc(0, writer->f); // pixels are square (we need to specify this because it's 1989) + + // now the "global" palette (really just a dummy palette) + // color 0: black + fputc(0, writer->f); + fputc(0, writer->f); + fputc(0, writer->f); + // color 1: also black + fputc(0, writer->f); + fputc(0, writer->f); + fputc(0, writer->f); + + if( delay != 0 ) + { + // animation header + fputc(0x21, writer->f); // extension + fputc(0xff, writer->f); // application specific + fputc(11, writer->f); // length 11 + fputs("NETSCAPE2.0", writer->f); // yes, really + fputc(3, writer->f); // 3 bytes of NETSCAPE2.0 data + + fputc(1, writer->f); // this is the Netscape 2.0 sub-block ID and it must be 1, otherwise some viewers error + fputc(0, writer->f); // loop infinitely (byte 0) + fputc(0, writer->f); // loop infinitely (byte 1) + + fputc(0, writer->f); // block terminator + } + + return true; +} + +// Writes out a new frame to a GIF in progress. +// The GIFWriter should have been created by GIFBegin. +// AFAIK, it is legal to use different bit depths for different frames of an image - +// this may be handy to save bits in animations that don't change much. +bool GifWriteFrame( GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, int bitDepth = 8, bool dither = false ) +{ + if(!writer->f) return false; + + const uint8_t* oldImage = writer->firstFrame? NULL : writer->oldImage; + writer->firstFrame = false; + + GifPalette pal; + GifMakePalette((dither? NULL : oldImage), image, width, height, bitDepth, dither, &pal); + + if(dither) + GifDitherImage(oldImage, image, writer->oldImage, width, height, &pal); + else + GifThresholdImage(oldImage, image, writer->oldImage, width, height, &pal); + + GifWriteLzwImage(writer->f, writer->oldImage, 0, 0, width, height, delay, &pal); + + return true; +} + +// Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. +// Many if not most viewers will still display a GIF properly if the EOF code is missing, +// but it's still a good idea to write it out. +bool GifEnd( GifWriter* writer ) +{ + if(!writer->f) return false; + + fputc(0x3b, writer->f); // end of file + fclose(writer->f); + GIF_FREE(writer->oldImage); + + writer->f = NULL; + writer->oldImage = NULL; + + return true; +} + +#endif