Implement GIF conversion

This commit is contained in:
2025-09-27 19:43:27 +08:00
parent fcb7c6be8d
commit d92f477390
14 changed files with 1545 additions and 128 deletions

View File

@ -3,16 +3,24 @@ project(${TARGET})
add_subdirectory(vendor/lz4/build/cmake) 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)
if(MSVC)
add_compile_options(/utf-8)
endif()
# static # static
add_library(expkg-static STATIC add_library(expkg-static STATIC
${SRC_SOURCE} ${SRC_SOURCE}
vendor/stb/stb_image_write.cpp ${STB_SOURCE}
) )
target_link_libraries(expkg-static PRIVATE lz4) 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) target_include_directories(expkg-static PUBLIC src)
@ -20,7 +28,7 @@ target_include_directories(expkg-static PUBLIC src)
# shared # shared
add_library(expkg-shared SHARED add_library(expkg-shared SHARED
${SRC_SOURCE} ${SRC_SOURCE}
vendor/stb/stb_image_write.cpp ${STB_SOURCE}
) )
set_target_properties(expkg-shared PROPERTIES set_target_properties(expkg-shared PROPERTIES
@ -31,7 +39,7 @@ set_target_properties(expkg-shared PROPERTIES
target_link_libraries(expkg-shared PRIVATE lz4) target_link_libraries(expkg-shared PRIVATE lz4)
target_compile_definitions(expkg-shared PRIVATE -DPKG_SHARED -DPKG_BUILD_DLL) 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) target_include_directories(expkg-shared PUBLIC src)

View File

@ -43,11 +43,17 @@ namespace PKG
return result; return result;
} }
float_t BinaryReader::ReadSingle()
{
float_t result = 0;
m_File.read(reinterpret_cast<char*>(&result), sizeof(float_t));
return result;
}
char BinaryReader::ReadChar() char BinaryReader::ReadChar()
{ {
char result; char result;
m_File.read(&result, sizeof(char)); m_File.read(&result, sizeof(char));
pos_type a = m_File.tellg();
return result; return result;
} }
@ -69,7 +75,16 @@ namespace PKG
result.resize(length); result.resize(length);
m_File.read(reinterpret_cast<char*>(result.data()), length); m_File.read(reinterpret_cast<char*>(result.data()), length);
return std::filesystem::u8path(std::string(reinterpret_cast<const char*>(result.data()), length)).string(); // TODO: fix me! chinese charactor bug return std::filesystem::u8path(std::string(reinterpret_cast<const char*>(result.data()), length)).string();
}
std::string BinaryReader::ReadStringFileData(const uint32_t length)
{
std::vector<uint8_t> result;
result.resize(length);
m_File.read(reinterpret_cast<char*>(result.data()), length);
return std::string(reinterpret_cast<const char*>(result.data()), length);
} }
std::string BinaryReader::ReadNString(const int32_t maxLength) std::string BinaryReader::ReadNString(const int32_t maxLength)

View File

@ -25,9 +25,11 @@ namespace PKG
int32_t ReadInt32(); int32_t ReadInt32();
uint32_t ReadUInt32(); uint32_t ReadUInt32();
float_t ReadSingle();
char ReadChar(); char ReadChar();
std::string ReadString(uint32_t length); std::string ReadString(uint32_t length);
std::string ReadNString(int32_t maxLength = -1); std::string ReadNString(int32_t maxLength = -1);
std::string ReadStringFileData(uint32_t length);
void ReadData(std::string& data, uint32_t length); void ReadData(std::string& data, uint32_t length);
void ReadData(std::vector<uint8_t>& data, uint32_t length); void ReadData(std::vector<uint8_t>& data, uint32_t length);

View File

@ -26,16 +26,12 @@ namespace PKG
BinaryWriter::~BinaryWriter() 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, size);
m_File.write(data.data(), data.size());
else
m_File.write(data.data(), size);
} }
void BinaryWriter::WriteString(const std::string& str) void BinaryWriter::WriteString(const std::string& str)
@ -45,6 +41,7 @@ namespace PKG
void BinaryWriter::close() void BinaryWriter::close()
{ {
m_File.close(); if (!m_File.is_open())
m_File.close();
} }
} }

View File

@ -19,7 +19,7 @@ namespace PKG
explicit BinaryWriter(const std::filesystem::path& fileName, std::ios_base::openmode optMode = std::ios::out); explicit BinaryWriter(const std::filesystem::path& fileName, std::ios_base::openmode optMode = std::ios::out);
~BinaryWriter(); ~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); void WriteString(const std::string& str);
std::string GetFilePath() const { return m_FilePath; } std::string GetFilePath() const { return m_FilePath; }

230
expkg/src/DXT/DXT.cpp Normal file
View File

@ -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<uint8_t>& data, const DXTFlags flags)
{
std::vector<uint8_t> 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<uint8_t> 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<uint8_t>& rgba, std::vector<uint8_t>& 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<uint8_t>& rgba, std::vector<uint8_t>& block, const int blockIndex, const bool isDxt1)
{
// unpack end points
std::vector<uint8_t> 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<uint8_t> 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<uint8_t>& rgba, std::vector<uint8_t>& 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<uint8_t>& rgba, std::vector<uint8_t>& 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<uint8_t> 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<uint8_t> 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<uint8_t>& block, const int blockIndex, const int packedOffset, std::vector<uint8_t>& 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;
}
}

38
expkg/src/DXT/DXT.h Normal file
View File

@ -0,0 +1,38 @@
//
// Created by sfd on 25-9-21.
//
#ifndef DXT_H
#define DXT_H
#include <cstdint>
#include <vector>
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<uint8_t>& data, DXTFlags flags);
private:
static void Decompress(std::vector<uint8_t>& rgba, std::vector<uint8_t>& block, int blockIndex, DXTFlags flags);
static int UnPack565(std::vector<uint8_t>& block, int blockIndex, int packedOffset, std::vector<uint8_t>& color, int colorOffset);
static void DecompressColor(std::vector<uint8_t>& rgba, std::vector<uint8_t>& block, int blockIndex, bool isDxt1);
static void DecompressAlphaDxt3(std::vector<uint8_t>& rgba, std::vector<uint8_t>& block, int blockIndex);
static void DecompressAlphaDxt5(std::vector<uint8_t>& rgba, std::vector<uint8_t>& block, int blockIndex);
};
}
#endif //DXT_H

View File

@ -6,8 +6,10 @@
#include <iostream> #include <iostream>
#include "gif.h"
#include "BinaryOPT/BinaryWriter.h" #include "BinaryOPT/BinaryWriter.h"
#include "BinaryOPT/ImageReader.h" #include "BinaryOPT/ImageReader.h"
#include "DXT/DXT.h"
#include "Tex/Tex.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, 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,16 @@ example:
Run(); 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<BinaryReader>(filePath);
m_OutDir = outDir;
m_Reader = std::make_shared<BinaryReader>(std::filesystem::u8path(filePath));
m_OutDir = std::filesystem::u8path(outDir);
m_OutDir = m_OutDir.make_preferred(); m_OutDir = m_OutDir.make_preferred();
if (m_Reader)
Run(); Run();
} }
FILE_EXTENSION EXPKG::checkExtension(const std::filesystem::path& filePath) FILE_EXTENSION EXPKG::checkExtension(const std::filesystem::path& filePath)
@ -72,6 +76,7 @@ example:
if (filePath.extension() == ".tex") if (filePath.extension() == ".tex")
return FILE_EXTENSION::TEX; return FILE_EXTENSION::TEX;
std::cerr << "not a pkg file or mpkg file or tex file" << std::endl;
return FILE_EXTENSION::UNKNOWN; return FILE_EXTENSION::UNKNOWN;
} }
@ -143,15 +148,15 @@ example:
if (entry.Type == ".tex") if (entry.Type == ".tex")
{ {
std::filesystem::path texPath = m_OutDir / entry.FullPath; std::filesystem::path texPath = m_OutDir / entry.FullPath;
BinaryWriter writer(texPath, std::ios::binary); {
BinaryWriter writer(texPath, std::ios::binary);
std::string texdata;
m_Reader->ReadData(texdata, entry.Length);
writer.WriteBytes(texdata);
writer.close();
std::string texdata;
m_Reader->ReadData(texdata, entry.Length);
writer.WriteBytes(texdata.data(), texdata.size());
writer.close();
}
ExtractTex(texPath); ExtractTex(texPath);
} }
@ -165,18 +170,18 @@ example:
std::string texdata; std::string texdata;
m_Reader->ReadData(texdata, entry.Length); m_Reader->ReadData(texdata, entry.Length);
writer.WriteBytes(texdata); writer.WriteBytes(texdata.data(), texdata.size());
writer.close(); writer.close();
} }
else else
{ {
BinaryWriter writer(m_OutDir / entry.FullPath); BinaryWriter writer(m_OutDir / entry.FullPath);
writer.WriteString(m_Reader->ReadString(entry.Length)); writer.WriteString(m_Reader->ReadStringFileData(entry.Length));
} }
} }
} }
void EXPKG::ExtractTex(const std::filesystem::path& path) void EXPKG::ExtractTex(const std::filesystem::path& path) const
{ {
std::shared_ptr<BinaryReader> reader = m_Reader; std::shared_ptr<BinaryReader> reader = m_Reader;
if (path != "") if (path != "")
@ -214,12 +219,6 @@ example:
if ((int)tex.Header.Flags & (int)TexType::IsVideoTexture) tex.IsVideoTexture = true; if ((int)tex.Header.Flags & (int)TexType::IsVideoTexture) tex.IsVideoTexture = true;
tex.ImageContainer = ImageReader::ImageContainerReaderReadFrom(*reader, tex.Header.Format); tex.ImageContainer = ImageReader::ImageContainerReaderReadFrom(*reader, tex.Header.Format);
if (tex.IsGif)
{
// TODO: to impl this
//Read Frame
}
// ReadHeader end // ReadHeader end
if (!tex.ImageContainer.Images.empty()) if (!tex.ImageContainer.Images.empty())
@ -231,15 +230,27 @@ example:
else else
format = tex.ImageContainer.Images[0].Mipmaps[0].Format; format = tex.ImageContainer.Images[0].Mipmaps[0].Format;
switch (format) auto tmpfotmat = format;
for(auto& Image : tex.ImageContainer.Images)
{ {
case MipmapFormat::CompressedDXT5: auto& mipmap = Image.Mipmaps[0];
case MipmapFormat::CompressedDXT3: switch (tmpfotmat)
case MipmapFormat::CompressedDXT1: {
std::cerr << "raw mipmap meybe compressed" << std::endl; case MipmapFormat::CompressedDXT5:
break; DXT::DecompressImage(mipmap.Width, mipmap.Height, mipmap.Data, DXTFlags::DXT5);
default: mipmap.Format = MipmapFormat::RGBA8888; format = MipmapFormat::RGBA8888;
break; 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:
break;
}
} }
if ((int)format >= 1 && (int)format <= 3) if ((int)format >= 1 && (int)format <= 3)
@ -253,98 +264,281 @@ example:
if (tex.IsGif) if (tex.IsGif)
{ {
// TODO: to impl it
// convert gif // convert gif
}
auto& sourceMipmap = tex.ImageContainer.Images[0].Mipmaps[0]; // tex.GifContainer
auto& container = tex.FrameInfoContainer;
if (tex.IsVideoTexture) container.Magic = reader->ReadNString(16);
{
if (sourceMipmap.Data.size() < 12) int frameCount = reader->ReadInt32();
/*
switch (container.Magic)
{ {
std::cerr << "expect mp4 magic header" << std::endl; case "TEXS0001":
} case "TEXS0002":
std::string mp4Magic = std::string(reinterpret_cast<const char*>(&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);
break; break;
case MipmapFormat::RG88: case "TEXS0003":
data = stbi_write_png_to_mem(sourceMipmap.Data.data(), container.GifWidth = reader->ReadInt32();
sourceMipmap.Width * 2, container.GifHeight = reader->ReadInt32();
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);
break; break;
default: default:
break; std::cerr << "bad magic" << std::endl;
} }
*/
if (data) {
if (container.Magic == "TEXS0001" || container.Magic == "TEXS0002")
{ {
imgData.assign(data, data + len); }else if (container.Magic == "TEXS0003")
free(data); {
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<uint8_t> 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<uint8_t>(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<const char*>(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<const char*>(&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<const char*>(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<const char*>(sourceMipmap.Data.data()),
sourceMipmap.Data.size()));
imageWriter.close();
// Convert source end
} }
} }
} }

View File

@ -25,13 +25,13 @@ namespace PKG
{ {
public: public:
EXPKG(const CommandArgs& commandArgs); 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: private:
static FILE_EXTENSION checkExtension(const std::filesystem::path& filePath); 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 ExtractPkg();
void Run(); void Run();

View File

@ -5,6 +5,7 @@
#ifndef TEX_H #ifndef TEX_H
#define TEX_H #define TEX_H
#include "TexFrameInfoContainer.h"
#include "TexImageContainer.h" #include "TexImageContainer.h"
@ -21,7 +22,7 @@ namespace PKG
bool IsGif = false; bool IsGif = false;
bool IsVideoTexture = false; bool IsVideoTexture = false;
// std::optional<TexFrameInfoContainer> FrameInfoContainer = {}; TexFrameInfoContainer FrameInfoContainer = {};
}; };
} }

View File

@ -0,0 +1,5 @@
//
// Created by sfd on 25-9-21.
//
#include "TexFrameInfoContainer.h"

View File

@ -0,0 +1,39 @@
//
// Created by sfd on 25-9-21.
//
#ifndef TEXFRAMEINFOCONTAINER_H
#define TEXFRAMEINFOCONTAINER_H
#include <string>
#include <vector>
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<TexFrameInfo> Frames;
int GifWidth;
int GifHeight;
};
}
#endif //TEXFRAMEINFOCONTAINER_H

24
expkg/vendor/gif-h/LICENSE vendored Normal file
View File

@ -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 <http://unlicense.org>

864
expkg/vendor/gif-h/gif.h vendored Normal file
View File

@ -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 <stdio.h> // for FILE*
#include <string.h> // for memcpy and bzero
#include <stdint.h> // for integer typedefs
#include <stdbool.h> // 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 <stdlib.h>
#define GIF_TEMP_MALLOC malloc
#endif
#ifndef GIF_TEMP_FREE
#include <stdlib.h>
#define GIF_TEMP_FREE free
#endif
#ifndef GIF_MALLOC
#include <stdlib.h>
#define GIF_MALLOC malloc
#endif
#ifndef GIF_FREE
#include <stdlib.h>
#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<r?l:r; }
int GifIAbs(int i) { return i<0?-i:i; }
// walks the k-d tree to pick the palette entry for a desired color.
// Takes as in/out parameters the current best color and its error -
// only changes them if it finds a better color in its subtree.
// this is the major hotspot in the code at the moment.
void GifGetClosestPaletteColor( GifPalette* pPal, int r, int g, int b, int* bestInd, int* bestDiff, int treeRoot )
{
// base case, reached the bottom of the tree
if(treeRoot > (1<<pPal->bitDepth)-1)
{
int ind = treeRoot-(1<<pPal->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<right; ++ii)
{
int arrayVal = image[ii*4+elt];
if( arrayVal < pivotValue )
{
GifSwapPixels(image, ii, storeIndex);
++storeIndex;
}
else if( arrayVal == pivotValue )
{
if(split)
{
GifSwapPixels(image, ii, storeIndex);
++storeIndex;
}
split = !split;
}
}
return storeIndex;
}
// Perform an incomplete sort, finding all elements above and below the desired median
void GifPartitionByMedian(uint8_t* image, int left, int right, int com, int neededCenter)
{
if(left < right-1)
{
int pivotValue = image[(neededCenter)*4+com];
GifSwapPixels(image, neededCenter, right-1);
int pivotIndex = GifPartition(image, left, right-1, com, pivotValue);
GifSwapPixels(image, pivotIndex, right-1);
// Only "sort" the section of the array that contains the median
if(pivotIndex > 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; ii<numPixels; ++ii)
{
r = (uint32_t)GifIMin((int32_t)r, image[ii * 4 + 0]);
g = (uint32_t)GifIMin((int32_t)g, image[ii * 4 + 1]);
b = (uint32_t)GifIMin((int32_t)b, image[ii * 4 + 2]);
}
pal->r[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; ii<numPixels; ++ii)
{
r = (uint32_t)GifIMax((int32_t)r, image[ii * 4 + 0]);
g = (uint32_t)GifIMax((int32_t)g, image[ii * 4 + 1]);
b = (uint32_t)GifIMax((int32_t)b, image[ii * 4 + 2]);
}
pal->r[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; ii<numPixels; ++ii)
{
r += image[ii*4+0];
g += image[ii*4+1];
b += image[ii*4+2];
}
r += (uint64_t)numPixels / 2; // round to nearest
g += (uint64_t)numPixels / 2;
b += (uint64_t)numPixels / 2;
r /= (uint64_t)numPixels;
g /= (uint64_t)numPixels;
b /= (uint64_t)numPixels;
pal->r[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<numPixels; ++ii)
{
int r = image[ii*4+0];
int g = image[ii*4+1];
int b = image[ii*4+2];
if(r > 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; ii<numPixels; ++ii)
{
if(lastFrame[0] != frame[0] ||
lastFrame[1] != frame[1] ||
lastFrame[2] != frame[2])
{
writeIter[0] = frame[0];
writeIter[1] = frame[1];
writeIter[2] = frame[2];
++numChanged;
writeIter += 4;
}
lastFrame += 4;
frame += 4;
}
return numChanged;
}
// Creates a palette by placing all the image pixels in a k-d tree and then averaging the blocks at the bottom.
// This is known as the "median split" technique
void GifMakePalette( const uint8_t* lastFrame, const uint8_t* nextFrame, uint32_t width, uint32_t height, int bitDepth, bool buildForDither, GifPalette* pPal )
{
pPal->bitDepth = 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; ii<numPixels*4; ++ii )
{
uint8_t pix = nextFrame[ii];
int32_t pix16 = (int32_t)(pix) * 256;
quantPixels[ii] = pix16;
}
for( uint32_t yy=0; yy<height; ++yy )
{
for( uint32_t xx=0; xx<width; ++xx )
{
int32_t* nextPix = quantPixels + 4*(yy*width+xx);
const uint8_t* lastPix = lastFrame? lastFrame + 4*(yy*width+xx) : NULL;
// Compute the colors we want (rounding to nearest)
int32_t rr = (nextPix[0] + 127) / 256;
int32_t gg = (nextPix[1] + 127) / 256;
int32_t bb = (nextPix[2] + 127) / 256;
// if it happens that we want the color from last frame, then just write out
// a transparent pixel
if( lastFrame &&
lastPix[0] == rr &&
lastPix[1] == gg &&
lastPix[2] == bb )
{
nextPix[0] = rr;
nextPix[1] = gg;
nextPix[2] = bb;
nextPix[3] = kGifTransIndex;
continue;
}
int32_t bestDiff = 1000000;
int32_t bestInd = kGifTransIndex;
// Search the palete
GifGetClosestPaletteColor(pPal, rr, gg, bb, &bestInd, &bestDiff, 1);
// Write the result to the temp buffer
int32_t r_err = nextPix[0] - (int32_t)(pPal->r[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; ii<numPixels*4; ++ii )
{
outFrame[ii] = (uint8_t)quantPixels[ii];
}
GIF_TEMP_FREE(quantPixels);
}
// Picks palette colors for the image using simple thresholding, no dithering
void GifThresholdImage( const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal )
{
uint32_t numPixels = width*height;
for( uint32_t ii=0; ii<numPixels; ++ii )
{
// if a previous color is available, and it matches the current color,
// set the pixel to transparent
if(lastFrame &&
lastFrame[0] == nextFrame[0] &&
lastFrame[1] == nextFrame[1] &&
lastFrame[2] == nextFrame[2])
{
outFrame[0] = lastFrame[0];
outFrame[1] = lastFrame[1];
outFrame[2] = lastFrame[2];
outFrame[3] = kGifTransIndex;
}
else
{
// palettize the pixel
int32_t bestDiff = 1000000;
int32_t bestInd = 1;
GifGetClosestPaletteColor(pPal, nextFrame[0], nextFrame[1], nextFrame[2], &bestInd, &bestDiff, 1);
// Write the resulting color to the output buffer
outFrame[0] = pPal->r[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<length; ++ii )
{
GifWriteBit(stat, code);
code = code >> 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<height; ++yy)
{
for(uint32_t xx=0; xx<width; ++xx)
{
#ifdef GIF_FLIP_VERT
// bottom-left origin image (such as an OpenGL capture)
uint8_t nextValue = image[((height-1-yy)*width+xx)*4+3];
#else
// top-left origin
uint8_t nextValue = image[(yy*width+xx)*4+3];
#endif
// "worst possible mode" - no compression, every single code is followed immediately by a clear
//WriteCode( f, stat, nextValue, codeSize );
//WriteCode( f, stat, 256, codeSize );
if( curCode < 0 )
{
// first value in a new run
curCode = nextValue;
}
else if( codetree[curCode].m_next[nextValue] )
{
// current run already in the dictionary
curCode = codetree[curCode].m_next[nextValue];
}
else
{
// finish the current run, write a code
GifWriteCode(f, &stat, (uint32_t)curCode, codeSize);
// insert the new run into the dictionary
codetree[curCode].m_next[nextValue] = (uint16_t)++maxCode;
if( maxCode >= (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