4 Commits
0.1.1 ... 0.2.0

Author SHA1 Message Date
13f07db882 添加误忽略的文件 2026-06-20 18:18:03 +08:00
0e0bf9b357 重构项目,现支持多线程操作 2026-06-20 18:03:34 +08:00
db3be495d7 Add basic Linux build support 2025-12-03 17:55:50 +08:00
d92f477390 Implement GIF conversion 2025-09-27 19:43:27 +08:00
262 changed files with 3814 additions and 1680 deletions

9
.gitignore vendored
View File

@ -1,8 +1,11 @@
# idea
.idea/
cmake-build-debug/
cmake-build-release/
cmake-build-minsizerel/
cmake-build-*/
# build仅根目录不影响 vendor/lz4/build 等子目录)
/build/
compile_commands.json
# vs
.vs/

View File

@ -2,9 +2,31 @@ cmake_minimum_required(VERSION 3.31)
project(expkg)
# set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_subdirectory(expkg)
add_subdirectory(testApp)
# 默认 Release 构建Debug 模式下 GIF/PNG 编码慢 3-5 倍)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
# 第三方依赖
add_subdirectory(vendor/lz4/build/cmake)
# 源文件CONFIGURE_DEPENDS: 文件增删时自动重新配置)
file(GLOB_RECURSE SRC_SOURCE CONFIGURE_DEPENDS src/*.cpp)
file(GLOB STB_SOURCE CONFIGURE_DEPENDS vendor/stb/*.cpp)
# 可执行文件
add_executable(${PROJECT_NAME} ${SRC_SOURCE} ${STB_SOURCE})
# MSVC 默认编码设为 UTF-8
if(MSVC)
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8)
endif()
# 依赖与头文件路径
target_link_libraries(${PROJECT_NAME} PRIVATE lz4)
target_include_directories(${PROJECT_NAME}
PRIVATE vendor/stb vendor/gif-h src
)

View File

@ -1,35 +1,12 @@
# exPKG
## useage
> ```c++
> #include "EXPKG/EXPKG.h"
>
> int main(int argc, char** argv) {
> CommandArgs args{ argc, argv};
>
> PKG::EXPKG decompress(args);
> PKG::EXPKG decompress("path/to/file");
> PKG::EXPKG decompress("path/to/file", "path/to/output/directory");
>
> return 0;
> }
>```
```bash
expkg /path/to/file.pkg|file.mpkg [output/path]
```
## Cmake
>
> use as subdirectory
>
> ```cmake
> add_subdirectory(path/to/expkg)
>
>add_executable(${TARGET} ${SRC_SOURCE})
>target_link_libraries(${TARGET} expkg-static)
>
> # or use dll by
> # target_link_libraries(${TARGET} expkg-shared)
>```
> then
> ```bash
> cmake -B build
> cmake --build build --config Release -j8
>```
build
```bash
cmake -B build
cmake --build build --config Release -j8
```

View File

@ -1,38 +0,0 @@
set(TARGET expkg)
project(${TARGET})
add_subdirectory(vendor/lz4/build/cmake)
file(GLOB_RECURSE SRC_SOURCE src/**.cpp)
# static
add_library(expkg-static STATIC
${SRC_SOURCE}
vendor/stb/stb_image_write.cpp
)
target_link_libraries(expkg-static PRIVATE lz4)
target_include_directories(expkg-static PRIVATE vendor/stb)
target_include_directories(expkg-static PUBLIC src)
# shared
add_library(expkg-shared SHARED
${SRC_SOURCE}
vendor/stb/stb_image_write.cpp
)
set_target_properties(expkg-shared PROPERTIES
OUTPUT_NAME "expkg"
PREFIX ""
)
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 PUBLIC src)

View File

@ -1,90 +0,0 @@
//
// Created by sfd on 25-8-4.
//
#include "BinaryReader.h"
#include <filesystem>
#include <iostream>
namespace PKG
{
BinaryReader::BinaryReader(const std::filesystem::path& fileName)
{
m_FilePath = fileName.string();
m_File.open(m_FilePath, std::ios::in | std::ios::binary);
if (!m_File.is_open())
{
std::cerr << "Failed to open file " << m_FilePath << std::endl;
system("pause");
exit(0);
}
}
BinaryReader::~BinaryReader()
{
if (m_File.is_open())
m_File.close();
}
int32_t BinaryReader::ReadInt32()
{
int32_t result = 0;
m_File.read(reinterpret_cast<char*>(&result), sizeof(int32_t));
return result;
}
uint32_t BinaryReader::ReadUInt32()
{
uint32_t result = 0;
m_File.read(reinterpret_cast<char*>(&result), sizeof(uint32_t));
return result;
}
char BinaryReader::ReadChar()
{
char result;
m_File.read(&result, sizeof(char));
pos_type a = m_File.tellg();
return result;
}
void BinaryReader::ReadData(std::string& data, const uint32_t length)
{
data.resize(length);
m_File.read(data.data(), length);
}
void BinaryReader::ReadData(std::vector<uint8_t>& data, uint32_t length)
{
data.resize(length);
m_File.read(reinterpret_cast<char*>(data.data()), length);
}
std::string BinaryReader::ReadString(const uint32_t length)
{
std::vector<uint8_t> result;
result.resize(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
}
std::string BinaryReader::ReadNString(const int32_t maxLength)
{
std::vector<uint8_t> result;
result.resize(0);
int count = maxLength <= 0 ? 16 : maxLength;
char chr = ReadChar();
while (chr != '\0' && (maxLength == -1 || count <= maxLength))
{
result.push_back(chr);
chr = ReadChar();
}
return std::string(reinterpret_cast<const char*>(result.data()), result.size());
}
}

View File

@ -1,48 +0,0 @@
//
// Created by sfd on 25-8-4.
//
#ifndef BINARYREADER_H
#define BINARYREADER_H
#include <filesystem>
#include <fstream>
#include <vector>
#include "Core.h"
namespace PKG
{
using pos_type = long long;
class PKG_API BinaryReader
{
public:
BinaryReader() = delete;
explicit BinaryReader(const std::filesystem::path& fileName);
~BinaryReader();
int32_t ReadInt32();
uint32_t ReadUInt32();
char ReadChar();
std::string ReadString(uint32_t length);
std::string ReadNString(int32_t maxLength = -1);
void ReadData(std::string& data, uint32_t length);
void ReadData(std::vector<uint8_t>& data, uint32_t length);
void seekg(const pos_type pos) { m_File.seekg(pos); }
pos_type tellg() { return m_File.tellg(); }
std::string GetFilePath() const { return m_FilePath; }
std::string GetFileName() const { return m_FilePath.substr(m_FilePath.find_last_of("\\/") + 1); }
private:
std::ifstream m_File;
std::string m_FilePath;
};
}
#endif //BINARYREADER_H

View File

@ -1,50 +0,0 @@
//
// Created by sfd on 25-8-4.
//
#include "BinaryWriter.h"
#include <filesystem>
#include <iostream>
namespace PKG
{
BinaryWriter::BinaryWriter(const std::filesystem::path& fileName, const std::ios_base::openmode optMode)
{
m_FilePath = fileName.string();
const std::filesystem::path path(fileName.parent_path());
m_File.open(m_FilePath, optMode);
if (!m_File.is_open() && !std::filesystem::exists(path))
{
std::filesystem::create_directories(path);
m_File.open(m_FilePath, optMode);
if (!m_File.is_open())
std::cerr << "cound not create file: " << m_FilePath << std::endl;
}
}
BinaryWriter::~BinaryWriter()
{
if (!m_File.is_open())
close();
}
void BinaryWriter::WriteBytes(const std::string& data, const uint32_t size)
{
if (size == 0)
m_File.write(data.data(), data.size());
else
m_File.write(data.data(), size);
}
void BinaryWriter::WriteString(const std::string& str)
{
m_File.write(str.c_str(), str.size());
}
void BinaryWriter::close()
{
m_File.close();
}
}

View File

@ -1,38 +0,0 @@
//
// Created by sfd on 25-8-4.
//
#ifndef BINARYWRITER_H
#define BINARYWRITER_H
#include <filesystem>
#include <fstream>
#include "Core.h"
namespace PKG
{
class PKG_API BinaryWriter
{
public:
BinaryWriter() = delete;
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 WriteString(const std::string& str);
std::string GetFilePath() const { return m_FilePath; }
std::string GetFileName() const { return m_FilePath.substr(m_FilePath.find_last_of("\\/") + 1); }
void close();
private:
std::ofstream m_File;
std::string m_FilePath;
};
}
#endif //BINARYWRITER_H

View File

@ -1,265 +0,0 @@
//
// Created by sfd on 25-8-5.
//
#include "ImageReader.h"
#include <iostream>
#include "lz4.h"
namespace PKG
{
#include <vector>
std::vector<unsigned char> Lz4Decompress(const unsigned char* compressedData,
size_t compressedSize,
size_t decompressedSize)
{
// 准备输出缓冲区
std::vector<unsigned char> output(decompressedSize);
// 执行解压缩
int result = LZ4_decompress_safe(
reinterpret_cast<const char*>(compressedData),
reinterpret_cast<char*>(output.data()),
static_cast<int>(compressedSize),
static_cast<int>(decompressedSize)
);
// 检查解压结果
if (result < 0 || static_cast<size_t>(result) != decompressedSize) {
throw std::runtime_error("LZ4 decompression failed");
}
return output;
}
TexImage ImageReader::ReadFrom(BinaryReader& reader, const TexImageContainer& container, const TexFormat format)
{
int mipMapCount = reader.ReadInt32();
auto aFormat = GetFormatFromTex(container.ImageFormat, format);
TexImage image{};
for (int i = 0; i < mipMapCount; i++)
{
TexMipMap mipmap;
switch (container.ImageContainerVersion)
{
case ImageContainerVersion::VERSION1:
mipmap = ReadMipMapV1(reader); break;
case ImageContainerVersion::VERSION2:
case ImageContainerVersion::VERSION3:
mipmap = ReadMipMapV2AndV3(reader); break;
case ImageContainerVersion::VERSION4:
mipmap = ReadMipMapV4(reader); break;
}
mipmap.Format = aFormat;
if (mipmap.IsZ4Compressed)
{
mipmap.Data = Lz4Decompress(mipmap.Data.data(), mipmap.Data.size(), mipmap.DecompressedDataCount);
mipmap.IsZ4Compressed = false;
}
image.Mipmaps.push_back(mipmap);
}
return image;
}
TexImageContainer ImageReader::ImageContainerReaderReadFrom(BinaryReader& reader, TexFormat texFormat)
{
TexImageContainer container;
container.Magic = reader.ReadNString();
int imageCount = reader.ReadInt32();
if (container.Magic == "TEXB0001")
{
}
else if (container.Magic == "TEXB0002")
{
}
else if (container.Magic == "TEXB0003")
{
container.ImageFormat = (FreeImageFormat)reader.ReadInt32();
}
else if (container.Magic == "TEXB0004")
{
auto format = (FreeImageFormat)reader.ReadInt32();
bool isVideoMp4 = reader.ReadInt32();
if (format == FreeImageFormat::FIF_UNKNOWN)
{
if (isVideoMp4)
format = FreeImageFormat::FIF_MP4;
}
container.ImageFormat = format;
}
else
{
std::cerr << "bad image format" << std::endl;
}
int version = std::stoi(container.Magic.substr(4, 4));
container.ImageContainerVersion = (ImageContainerVersion)version;
if (container.ImageContainerVersion == ImageContainerVersion::VERSION4 && container.ImageFormat != FreeImageFormat::FIF_MP4)
{
container.ImageContainerVersion = ImageContainerVersion::VERSION3;
}
for (int i = 0; i < imageCount; i++)
{
container.Images.push_back(ReadFrom(reader, container, texFormat));
}
return container;
}
TexMipMap ImageReader::ReadMipMapV1(BinaryReader& reader)
{
TexMipMap mipmap{};
mipmap.Width = reader.ReadInt32();
mipmap.Height = reader.ReadInt32();
mipmap.Data = ReadBytes(reader);
return mipmap;
}
TexMipMap ImageReader::ReadMipMapV2AndV3(BinaryReader& reader)
{
TexMipMap mipmap{};
mipmap.Width = reader.ReadInt32();
mipmap.Height = reader.ReadInt32();
mipmap.IsZ4Compressed = reader.ReadInt32() == 1;
mipmap.DecompressedDataCount = reader.ReadInt32();
mipmap.Data = ReadBytes(reader);
return mipmap;
}
TexMipMap ImageReader::ReadMipMapV4(BinaryReader& reader)
{
int param1 = reader.ReadInt32();
if (param1 != 1)
{
std::cerr << "ReadMipmapV4 unknow param1: " << param1 << std::endl;
}
int param2 = reader.ReadInt32();
if (param2 != 2)
{
std::cerr << "ReadMipmapV4 unknow param2: " << param1 << std::endl;
}
std::string condition = reader.ReadNString();
int param3 = reader.ReadInt32();
if (param3 != 1)
{
std::cerr << "ReadMipmapV4 unknow param3: " << param1 << std::endl;
}
TexMipMap mipmap{};
mipmap.Width = reader.ReadInt32();
mipmap.Height = reader.ReadInt32();
mipmap.IsZ4Compressed = reader.ReadInt32() == 1;
mipmap.DecompressedDataCount = reader.ReadInt32();
mipmap.Data = ReadBytes(reader);
return mipmap;
}
std::vector<uint8_t> ImageReader::ReadBytes(BinaryReader& reader)
{
int count = reader.ReadInt32();
std::vector<uint8_t> bytes;
reader.ReadData(bytes, count);
return bytes;
}
MipmapFormat ImageReader::GetFormatFromTex(FreeImageFormat imageFormat, TexFormat format)
{
if (imageFormat != FreeImageFormat::FIF_UNKNOWN)
{
return FreeImageFormatToMipmapFormat(imageFormat);
}
switch (format)
{
case TexFormat::RGBA8888: return MipmapFormat::RGBA8888;
case TexFormat::DXT5: return MipmapFormat::CompressedDXT5;
case TexFormat::DXT3: return MipmapFormat::CompressedDXT3;
case TexFormat::DXT1: return MipmapFormat::CompressedDXT1;
case TexFormat::RG88: return MipmapFormat::RG88;
case TexFormat::R8: return MipmapFormat::R8;
}
std::cerr << "unknow format" << std::endl;
return MipmapFormat::Invalid;
}
MipmapFormat ImageReader::FreeImageFormatToMipmapFormat(FreeImageFormat imageFormat)
{
switch (imageFormat)
{
case FreeImageFormat::FIF_BMP: return MipmapFormat::ImageBMP;
case FreeImageFormat::FIF_ICO: return MipmapFormat::ImageICO;
case FreeImageFormat::FIF_JPEG: return MipmapFormat::ImageJPEG;
case FreeImageFormat::FIF_JNG: return MipmapFormat::ImageJNG;
case FreeImageFormat::FIF_KOALA: return MipmapFormat::ImageKOALA;
case FreeImageFormat::FIF_LBM: return MipmapFormat::ImageLBM;
case FreeImageFormat::FIF_MNG: return MipmapFormat::ImageMNG;
case FreeImageFormat::FIF_PBM: return MipmapFormat::ImagePBM;
case FreeImageFormat::FIF_PBMRAW: return MipmapFormat::ImagePBMRAW;
case FreeImageFormat::FIF_PCD: return MipmapFormat::ImagePCD;
case FreeImageFormat::FIF_PCX: return MipmapFormat::ImagePCX;
case FreeImageFormat::FIF_PGM: return MipmapFormat::ImagePGM;
case FreeImageFormat::FIF_PGMRAW: return MipmapFormat::ImagePGMRAW;
case FreeImageFormat::FIF_PNG: return MipmapFormat::ImagePNG;
case FreeImageFormat::FIF_PPM: return MipmapFormat::ImagePPM;
case FreeImageFormat::FIF_PPMRAW: return MipmapFormat::ImagePPMRAW;
case FreeImageFormat::FIF_RAS: return MipmapFormat::ImageRAS;
case FreeImageFormat::FIF_TARGA: return MipmapFormat::ImageTARGA;
case FreeImageFormat::FIF_TIFF: return MipmapFormat::ImageTIFF;
case FreeImageFormat::FIF_WBMP: return MipmapFormat::ImageWBMP;
case FreeImageFormat::FIF_PSD: return MipmapFormat::ImagePSD;
case FreeImageFormat::FIF_CUT: return MipmapFormat::ImageCUT;
case FreeImageFormat::FIF_XBM: return MipmapFormat::ImageXBM;
case FreeImageFormat::FIF_XPM: return MipmapFormat::ImageXPM;
case FreeImageFormat::FIF_DDS: return MipmapFormat::ImageDDS;
case FreeImageFormat::FIF_GIF: return MipmapFormat::ImageGIF;
case FreeImageFormat::FIF_HDR: return MipmapFormat::ImageHDR;
case FreeImageFormat::FIF_FAXG3: return MipmapFormat::ImageFAXG3;
case FreeImageFormat::FIF_SGI: return MipmapFormat::ImageSGI;
case FreeImageFormat::FIF_EXR: return MipmapFormat::ImageEXR;
case FreeImageFormat::FIF_J2K: return MipmapFormat::ImageJ2K;
case FreeImageFormat::FIF_JP2: return MipmapFormat::ImageJP2;
case FreeImageFormat::FIF_PFM: return MipmapFormat::ImagePFM;
case FreeImageFormat::FIF_PICT: return MipmapFormat::ImagePICT;
case FreeImageFormat::FIF_RAW: return MipmapFormat::ImageRAW;
case FreeImageFormat::FIF_MP4: return MipmapFormat::VideoMp4;
}
std::cerr << "unknown format" << std::endl;
return MipmapFormat::Invalid;
}
}

View File

@ -1,32 +0,0 @@
//
// Created by sfd on 25-8-5.
//
#ifndef IMAGEREADER_H
#define IMAGEREADER_H
#include "BinaryOPT/BinaryReader.h"
#include "Tex/TexImageContainer.h"
namespace PKG
{
class PKG_API ImageReader
{
public:
static TexImage ReadFrom(BinaryReader& reader, const TexImageContainer& container, TexFormat format);
static TexImageContainer ImageContainerReaderReadFrom(BinaryReader& reader, TexFormat texFormat);
private:
static TexMipMap ReadMipMapV1(BinaryReader& reader);
static TexMipMap ReadMipMapV2AndV3(BinaryReader& reader);
static TexMipMap ReadMipMapV4(BinaryReader& reader);
static std::vector<uint8_t> ReadBytes(BinaryReader& reader);
static MipmapFormat GetFormatFromTex(FreeImageFormat imageFormat, TexFormat format);
static MipmapFormat FreeImageFormatToMipmapFormat(FreeImageFormat imageFormat);
};
}
#endif //IMAGEREADER_H

View File

@ -1,5 +0,0 @@
//
// Created by sfd on 25-8-4.
//
#include "Core.h"

View File

@ -1,19 +0,0 @@
//
// Created by sfd on 25-8-4.
//
#ifndef CORE_H
#define CORE_H
#ifdef PKG_SHARED
#ifdef PKG_BUILD_DLL
#define PKG_API __declspec(dllexport)
#else
#define PKG_API __declspec(dllimport)
#endif
#else
#define PKG_API
#endif
#endif //CORE_H

View File

@ -1,350 +0,0 @@
//
// Created by sfd on 25-8-4.
//
#include "EXPKG.h"
#include <iostream>
#include "BinaryOPT/BinaryWriter.h"
#include "BinaryOPT/ImageReader.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,
int* out_len);
namespace PKG
{
const char* help = R"(
usage:
expkg path/to/file.pkg [path/to/output](optional)
example:
expkg demo.pkg
expkg demo.pkg outdir
)";
enum class FILE_EXTENSION
{
TEX,
PKG,
UNKNOWN
};
EXPKG::EXPKG(const CommandArgs& commandArgs)
{
if (commandArgs.argc < 2)
{
std::cout << help << std::endl;
system("pause");
exit(0);
}
m_Reader = std::make_shared<BinaryReader>(commandArgs.argv[1]);
if (commandArgs.argc == 3)
{
m_OutDir = commandArgs.argv[2];
m_OutDir = m_OutDir.make_preferred();
}
else
{
m_OutDir = "out";
}
Run();
}
EXPKG::EXPKG(const std::filesystem::path& filePath, const std::filesystem::path& outDir)
{
m_Reader = std::make_shared<BinaryReader>(filePath);
m_OutDir = outDir;
m_OutDir = m_OutDir.make_preferred();
Run();
}
FILE_EXTENSION EXPKG::checkExtension(const std::filesystem::path& filePath)
{
if (filePath.extension() == ".pkg")
return FILE_EXTENSION::PKG;
if (filePath.extension() == ".mpkg")
return FILE_EXTENSION::PKG;
if (filePath.extension() == ".tex")
return FILE_EXTENSION::TEX;
return FILE_EXTENSION::UNKNOWN;
}
void EXPKG::Run()
{
switch (checkExtension(m_Reader->GetFilePath()))
{
case FILE_EXTENSION::TEX:
ExtractTex();
break;
case FILE_EXTENSION::PKG:
ExtractPkg();
break;
case FILE_EXTENSION::UNKNOWN:
break;
}
}
void EXPKG::ExtractPkg()
{
// Read Binary resource
const int head = m_Reader->ReadInt32();
const std::string magicHeader = m_Reader->ReadString(head);
if (magicHeader.substr(0,3) != "PKG")
{
std::cerr << "unknown header: " << m_Reader->GetFilePath() << std::endl;
std::cerr << "not a pkg file " << std::endl;
exit(0);
}
const int count = m_Reader->ReadInt32();
if (count > 0)
{
for (int i = 0; i < count; i++)
{
Entry entry;
const int size = m_Reader->ReadInt32();
entry.FullPath = m_Reader->ReadString(size);
entry.FullPath = entry.FullPath.make_preferred(); // conflict with system "\\" or "/", use it to solve
entry.Offset = m_Reader->ReadInt32();
entry.Length = m_Reader->ReadInt32();
entry.Type = entry.FullPath.extension().string();
m_Entries.push_back(entry);
}
}
else
{
std::cerr << "could not get resource: " << m_Reader->GetFilePath() << std::endl;
exit(0);
}
// try Extract
uint32_t offsetPosition = m_Reader->tellg();
for (const auto& entry : m_Entries)
{
std::cout << "convert file: " << m_OutDir / entry.FullPath << std::endl;
m_Reader->seekg(entry.Offset + offsetPosition);
if (entry.Type == ".tex")
{
std::filesystem::path texPath = m_OutDir / entry.FullPath;
BinaryWriter writer(texPath, std::ios::binary);
std::string texdata;
m_Reader->ReadData(texdata, entry.Length);
writer.WriteBytes(texdata);
writer.close();
ExtractTex(texPath);
}
else if (entry.Type == ".gif" ||
entry.Type == ".jpg" ||
entry.Type == ".png" ||
entry.Type == ".jpeg" ||
entry.Type == ".webp")
{
BinaryWriter writer(m_OutDir / entry.FullPath, std::ios::binary);
std::string texdata;
m_Reader->ReadData(texdata, entry.Length);
writer.WriteBytes(texdata);
writer.close();
}
else
{
BinaryWriter writer(m_OutDir / entry.FullPath);
writer.WriteString(m_Reader->ReadString(entry.Length));
}
}
}
void EXPKG::ExtractTex(const std::filesystem::path& path)
{
std::shared_ptr<BinaryReader> reader = m_Reader;
if (path != "")
{
reader = std::make_shared<BinaryReader>(path);
}
// Tex ReadHeader
Tex tex{};
tex.Magic1 = reader->ReadNString(16);
if (tex.Magic1 != "TEXV0005")
{
std::cerr << "bad magic" << std::endl;
return;
}
tex.Magic2 = reader->ReadNString(16);
if (tex.Magic2 != "TEXI0001")
{
std::cerr << "bad magic" << std::endl;
return;
}
tex.Header.Format = (TexFormat)reader->ReadInt32();
tex.Header.Flags = (TexType)reader->ReadInt32();
tex.Header.TextureWidth = reader->ReadInt32();
tex.Header.TextureHeight = reader->ReadInt32();
tex.Header.ImageWidth = reader->ReadInt32();
tex.Header.ImageHeight = reader->ReadInt32();
tex.Header.UnkInt0 = reader->ReadInt32();
if ((int)tex.Header.Flags & (int)TexType::IsGif) tex.IsGif = true;
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())
{
// GetConvertFormat
MipmapFormat format;
if (tex.IsVideoTexture)
format = MipmapFormat::VideoMp4;
else
format = tex.ImageContainer.Images[0].Mipmaps[0].Format;
switch (format)
{
case MipmapFormat::CompressedDXT5:
case MipmapFormat::CompressedDXT3:
case MipmapFormat::CompressedDXT1:
std::cerr << "raw mipmap meybe compressed" << std::endl;
break;
default:
break;
}
if ((int)format >= 1 && (int)format <= 3)
{
format = MipmapFormat::ImagePNG;
}
// GetConvertFormat end
// Convert source
if (tex.IsGif)
{
// TODO: to impl it
// convert gif
}
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;
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;
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);
break;
default:
break;
}
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(std::string(reinterpret_cast<const char*>(sourceMipmap.Data.data()),
sourceMipmap.Data.size()));
imageWriter.close();
// Convert source end
}
}
}

View File

@ -1,48 +0,0 @@
//
// Created by sfd on 25-8-4.
//
#ifndef EXPKG_H
#define EXPKG_H
#include <vector>
#include "Entry.h"
typedef struct CommandArgs
{
int argc;
char** argv;
} CommandArgs;
namespace PKG
{
class BinaryReader;
enum class FILE_EXTENSION;
class PKG_API EXPKG
{
public:
EXPKG(const CommandArgs& commandArgs);
EXPKG(const std::filesystem::path& filePath, const std::filesystem::path& outDir = "out");
private:
static FILE_EXTENSION checkExtension(const std::filesystem::path& filePath);
void ExtractTex(const std::filesystem::path& path = "");
void ExtractPkg();
void Run();
private:
std::shared_ptr<BinaryReader> m_Reader;
std::filesystem::path m_OutDir;
std::vector<Entry> m_Entries;
};
}
#endif //EXPKG_H

View File

@ -1,5 +0,0 @@
//
// Created by sfd on 25-8-4.
//
#include "Entry.h"

View File

@ -1,427 +0,0 @@
//
// Created by sfd on 25-8-4.
//
#ifndef ENTRY_H
#define ENTRY_H
#include <filesystem>
#include <string>
#include "Core.h"
namespace PKG
{
enum class TexFormat
{
RGBA8888 = 0,
DXT5 = 4,
DXT3 = 6,
DXT1 = 7,
RG88 = 8,
R8 = 9
};
enum class TexType
{
None = 0,
NoInterpolation = 1,
ClampUVs = 2,
IsGif = 4,
// Placeholders
Unk3 = 8,
Unk4 = 16,
IsVideoTexture = 32,
Unk6 = 64,
Unk7 = 128,
};
enum class MipmapFormat
{
Invalid = 0,
/// Raw pixels (4 bytes per pixel) (RGBA8888)
RGBA8888 = 1,
/// Raw pixels (1 byte per pixel) (R8)
R8 = 2,
/// Raw pixels (2 bytes per pixel) (RG88)
RG88 = 3,
/// Raw pixels compressed using DXT5
CompressedDXT5,
/// Raw pixels compressed using DXT3
CompressedDXT3,
/// Raw pixels compressed using DXT1
CompressedDXT1,
/// MP4 Video
VideoMp4,
/// Windows or OS/2 Bitmap File (*.BMP)
/// Keep '= 1000' because MipmapFormatExtensions.IsImage uses this to check if format is an image format
ImageBMP = 1000,
/// Windows Icon (*.ICO)
ImageICO,
/// Independent JPEG Group (*.JPG, *.JIF, *.JPEG, *.JPE)
ImageJPEG,
/// JPEG Network Graphics (*.JNG)
ImageJNG,
/// Commodore 64 Koala format (*.KOA)
ImageKOALA,
/// Amiga IFF (*.IFF, *.LBM)
ImageLBM,
/// Amiga IFF (*.IFF, *.LBM)
ImageIFF,
/// Multiple Network Graphics (*.MNG)
ImageMNG,
/// Portable Bitmap (ASCII) (*.PBM)
ImagePBM,
/// Portable Bitmap (BINARY) (*.PBM)
ImagePBMRAW,
/// Kodak PhotoCD (*.PCD)
ImagePCD,
/// Zsoft Paintbrush PCX bitmap format (*.PCX)
ImagePCX,
/// Portable Graymap (ASCII) (*.PGM)
ImagePGM,
/// Portable Graymap (BINARY) (*.PGM)
ImagePGMRAW,
/// Portable Network Graphics (*.PNG)
ImagePNG,
/// Portable Pixelmap (ASCII) (*.PPM)
ImagePPM,
/// Portable Pixelmap (BINARY) (*.PPM)
ImagePPMRAW,
/// Sun Rasterfile (*.RAS)
ImageRAS,
/// truevision Targa files (*.TGA, *.TARGA)
ImageTARGA,
/// Tagged Image File Format (*.TIF, *.TIFF)
ImageTIFF,
/// Wireless Bitmap (*.WBMP)
ImageWBMP,
/// Adobe Photoshop (*.PSD)
ImagePSD,
/// Dr. Halo (*.CUT)
ImageCUT,
/// X11 Bitmap Format (*.XBM)
ImageXBM,
/// X11 Pixmap Format (*.XPM)
ImageXPM,
/// DirectDraw Surface (*.DDS)
ImageDDS,
/// Graphics Interchange Format (*.GIF)
ImageGIF,
/// High Dynamic Range (*.HDR)
ImageHDR,
/// Raw Fax format CCITT G3 (*.G3)
ImageFAXG3,
/// Silicon Graphics SGI image format (*.SGI)
ImageSGI,
/// OpenEXR format (*.EXR)
ImageEXR,
/// JPEG-2000 format (*.J2K, *.J2C)
ImageJ2K,
/// JPEG-2000 format (*.JP2)
ImageJP2,
/// Portable FloatMap (*.PFM)
ImagePFM,
/// Macintosh PICT (*.PICT)
ImagePICT,
/// RAW camera image (*.*)
ImageRAW,
};
enum class FreeImageFormat {
/// <summary>
/// Unknown format (returned value only, never use it as input value)
/// </summary>
FIF_UNKNOWN = -1,
/// <summary>
/// Windows or OS/2 Bitmap File (*.BMP)
/// </summary>
FIF_BMP = 0,
/// <summary>
/// Windows Icon (*.ICO)
/// </summary>
FIF_ICO = 1,
/// <summary>
/// Independent JPEG Group (*.JPG, *.JIF, *.JPEG, *.JPE)
/// </summary>
FIF_JPEG = 2,
/// <summary>
/// JPEG Network Graphics (*.JNG)
/// </summary>
FIF_JNG = 3,
/// <summary>
/// Commodore 64 Koala format (*.KOA)
/// </summary>
FIF_KOALA = 4,
/// <summary>
/// Amiga IFF (*.IFF, *.LBM)
/// </summary>
FIF_LBM = 5,
/// <summary>
/// Amiga IFF (*.IFF, *.LBM)
/// </summary>
FIF_IFF = 5,
/// <summary>
/// Multiple Network Graphics (*.MNG)
/// </summary>
FIF_MNG = 6,
/// <summary>
/// Portable Bitmap (ASCII) (*.PBM)
/// </summary>
FIF_PBM = 7,
/// <summary>
/// Portable Bitmap (BINARY) (*.PBM)
/// </summary>
FIF_PBMRAW = 8,
/// <summary>
/// Kodak PhotoCD (*.PCD)
/// </summary>
FIF_PCD = 9,
/// <summary>
/// Zsoft Paintbrush PCX bitmap format (*.PCX)
/// </summary>
FIF_PCX = 10,
/// <summary>
/// Portable Graymap (ASCII) (*.PGM)
/// </summary>
FIF_PGM = 11,
/// <summary>
/// Portable Graymap (BINARY) (*.PGM)
/// </summary>
FIF_PGMRAW = 12,
/// <summary>
/// Portable Network Graphics (*.PNG)
/// </summary>
FIF_PNG = 13,
/// <summary>
/// Portable Pixelmap (ASCII) (*.PPM)
/// </summary>
FIF_PPM = 14,
/// <summary>
/// Portable Pixelmap (BINARY) (*.PPM)
/// </summary>
FIF_PPMRAW = 15,
/// <summary>
/// Sun Rasterfile (*.RAS)
/// </summary>
FIF_RAS = 16,
/// <summary>
/// truevision Targa files (*.TGA, *.TARGA)
/// </summary>
FIF_TARGA = 17,
/// <summary>
/// Tagged Image File Format (*.TIF, *.TIFF)
/// </summary>
FIF_TIFF = 18,
/// <summary>
/// Wireless Bitmap (*.WBMP)
/// </summary>
FIF_WBMP = 19,
/// <summary>
/// Adobe Photoshop (*.PSD)
/// </summary>
FIF_PSD = 20,
/// <summary>
/// Dr. Halo (*.CUT)
/// </summary>
FIF_CUT = 21,
/// <summary>
/// X11 Bitmap Format (*.XBM)
/// </summary>
FIF_XBM = 22,
/// <summary>
/// X11 Pixmap Format (*.XPM)
/// </summary>
FIF_XPM = 23,
/// <summary>
/// DirectDraw Surface (*.DDS)
/// </summary>
FIF_DDS = 24,
/// <summary>
/// Graphics Interchange Format (*.GIF)
/// </summary>
FIF_GIF = 25,
/// <summary>
/// High Dynamic Range (*.HDR)
/// </summary>
FIF_HDR = 26,
/// <summary>
/// Raw Fax format CCITT G3 (*.G3)
/// </summary>
FIF_FAXG3 = 27,
/// <summary>
/// Silicon Graphics SGI image format (*.SGI)
/// </summary>
FIF_SGI = 28,
/// <summary>
/// OpenEXR format (*.EXR)
/// </summary>
FIF_EXR = 29,
/// <summary>
/// JPEG-2000 format (*.J2K, *.J2C)
/// </summary>
FIF_J2K = 30,
/// <summary>
/// JPEG-2000 format (*.JP2)
/// </summary>
FIF_JP2 = 31,
/// <summary>
/// Portable FloatMap (*.PFM)
/// </summary>
FIF_PFM = 32,
/// <summary>
/// Macintosh PICT (*.PICT)
/// </summary>
FIF_PICT = 33,
/// <summary>
/// RAW camera image (*.*)
/// </summary>
FIF_RAW = 34,
/// <summary>
/// RAW camera MP4 (*.mp4)
/// </summary>
FIF_MP4 = 35,
};
struct PKG_API Entry
{
std::filesystem::path FullPath;
int32_t Offset{};
int32_t Length{};
std::string Type;
};
struct PKG_API TexHeader
{
TexFormat Format;
TexType Flags;
int32_t TextureWidth;
int32_t TextureHeight;
int32_t ImageWidth;
int32_t ImageHeight;
uint32_t UnkInt0;
};
}
#endif //ENTRY_H

View File

@ -1,5 +0,0 @@
//
// Created by sfd on 25-8-5.
//
#include "Tex.h"

View File

@ -1,29 +0,0 @@
//
// Created by sfd on 25-8-5.
//
#ifndef TEX_H
#define TEX_H
#include "TexImageContainer.h"
namespace PKG
{
class Tex
{
public:
std::string Magic1;
std::string Magic2;
TexHeader Header = {};
TexImageContainer ImageContainer = {};
bool IsGif = false;
bool IsVideoTexture = false;
// std::optional<TexFrameInfoContainer> FrameInfoContainer = {};
};
}
#endif //TEX_H

View File

@ -1,88 +0,0 @@
//
// Created by sfd on 25-8-5.
//
#include "TexImage.h"
#include <iostream>
std::string PKG::GetFileExtension(const MipmapFormat format)
{
switch (format)
{
case MipmapFormat::ImageBMP:
return "bmp";
case MipmapFormat::ImageICO:
return "ico";
case MipmapFormat::ImageJPEG:
return "jpg";
case MipmapFormat::ImageJNG:
return "jng";
case MipmapFormat::ImageKOALA:
return "koa";
case MipmapFormat::ImageLBM:
return "lbm";
case MipmapFormat::ImageIFF:
return "iff";
case MipmapFormat::ImageMNG:
return "mng";
case MipmapFormat::ImagePBM:
case MipmapFormat::ImagePBMRAW:
return "pbm";
case MipmapFormat::ImagePCD:
return "pcd";
case MipmapFormat::ImagePCX:
return "pcx";
case MipmapFormat::ImagePGM:
case MipmapFormat::ImagePGMRAW:
return "pgm";
case MipmapFormat::ImagePNG:
return "png";
case MipmapFormat::ImagePPM:
case MipmapFormat::ImagePPMRAW:
return "ppm";
case MipmapFormat::ImageRAS:
return "ras";
case MipmapFormat::ImageTARGA:
return "tga";
case MipmapFormat::ImageTIFF:
return "tif";
case MipmapFormat::ImageWBMP:
return "wbmp";
case MipmapFormat::ImagePSD:
return "psd";
case MipmapFormat::ImageCUT:
return "cut";
case MipmapFormat::ImageXBM:
return "xbm";
case MipmapFormat::ImageXPM:
return "xpm";
case MipmapFormat::ImageDDS:
return "dds";
case MipmapFormat::ImageGIF:
return "gif";
case MipmapFormat::ImageHDR:
return "hdr";
case MipmapFormat::ImageFAXG3:
return "g3";
case MipmapFormat::ImageSGI:
return "sgi";
case MipmapFormat::ImageEXR:
return "exr";
case MipmapFormat::ImageJ2K:
return "j2k";
case MipmapFormat::ImageJP2:
return "jp2";
case MipmapFormat::ImagePFM:
return "pfm";
case MipmapFormat::ImagePICT:
return "pict";
case MipmapFormat::ImageRAW:
return "raw";
case MipmapFormat::VideoMp4:
return "mp4";
}
std::cerr << "unknown file type" << std::endl;
return ".unknown";
}

View File

@ -1,37 +0,0 @@
//
// Created by sfd on 25-8-5.
//
#ifndef TEXIMAGE_H
#define TEXIMAGE_H
#include <vector>
#include "Entry.h"
namespace PKG
{
std::string GetFileExtension(MipmapFormat format);
class TexMipMap
{
public:
int Width;
int Height;
int DecompressedDataCount;
bool IsZ4Compressed = false;
MipmapFormat Format;
std::vector<uint8_t> Data;
};
class TexImage
{
public:
std::vector<TexMipMap> Mipmaps;
};
}
#endif //TEXIMAGE_H

View File

@ -1,6 +0,0 @@
//
// Created by sfd on 25-8-5.
//
#include "TexImageContainer.h"

View File

@ -1,31 +0,0 @@
//
// Created by sfd on 25-8-5.
//
#ifndef TEXIMAGECONTAINER_H
#define TEXIMAGECONTAINER_H
#include "TexImage.h"
namespace PKG
{
enum class ImageContainerVersion
{
VERSION1 = 1,
VERSION2 = 2,
VERSION3 = 3,
VERSION4 = 4,
};
class TexImageContainer
{
public:
std::string Magic;
FreeImageFormat ImageFormat{};
ImageContainerVersion ImageContainerVersion;
std::vector<TexImage> Images;
};
}
#endif //TEXIMAGECONTAINER_H

View File

@ -1,14 +0,0 @@
#ifndef EXPKG_H
#define EXPKG_H
#include "Entry.h"
#include "BinaryOPT/BinaryReader.h"
#include "BinaryOPT/BinaryWriter.h"
#include "BinaryOPT/ImageReader.h"
#include "EXPKG/EXPKG.h"
#include "Tex/Tex.h"
#include "Tex/TexImage.h"
#include "Tex/TexImageContainer.h"
#endif // EXPKG_H

15
src/Core/Config.h Normal file
View File

@ -0,0 +1,15 @@
#ifndef PKG_CONFIG_H
#define PKG_CONFIG_H
#include <cstdint>
namespace PKG {
// 提取配置
struct ExtractConfig {
uint32_t threadCount = 0; // 工作线程数0 = 自动hardware_concurrency
};
} // namespace PKG
#endif // PKG_CONFIG_H

51
src/Core/ExtractStats.h Normal file
View File

@ -0,0 +1,51 @@
#ifndef PKG_EXTRACT_STATS_H
#define PKG_EXTRACT_STATS_H
#include <atomic>
#include <chrono>
#include <cstddef>
#include <map>
#include <mutex>
#include <string>
namespace PKG {
// 提取统计信息(线程安全,可在多线程环境中累加)
// 由 ExtractPipeline::Run 填充,调用方可读取展示
struct ExtractStats {
std::atomic<size_t> totalEntries{0}; // 总条目数
std::atomic<size_t> succeeded{0}; // 成功数
std::atomic<size_t> failed{0}; // 失败数
std::atomic<uint64_t> totalOutputBytes{0}; // 输出总字节数
// 各输出格式数量(按扩展名统计,如 "png" -> 23
// 注意map 操作非原子,内部用 mutex 保护
std::map<std::string, size_t> formatCounts;
std::mutex formatMutex;
std::chrono::steady_clock::time_point startTime; // 由 Run 设置
std::chrono::steady_clock::time_point endTime;
uint64_t inputFileSize = 0; // 输入文件大小(字节)
// 记录一个成功输出的文件(线程安全)
void AddOutput(const std::string &ext, uint64_t bytes) {
succeeded.fetch_add(1, std::memory_order_relaxed);
totalOutputBytes.fetch_add(bytes, std::memory_order_relaxed);
std::lock_guard<std::mutex> lock(formatMutex);
formatCounts[ext]++;
}
// 记录一个失败(线程安全)
void AddFailure() {
failed.fetch_add(1, std::memory_order_relaxed);
}
// 耗时(秒)
double ElapsedSeconds() const {
return std::chrono::duration<double>(endTime - startTime).count();
}
};
} // namespace PKG
#endif // PKG_EXTRACT_STATS_H

33
src/Core/Logger.cpp Normal file
View File

@ -0,0 +1,33 @@
#include "Logger.h"
#include "ProgressBar.h"
#include <iostream>
namespace PKG {
Logger& Logger::Instance() {
static Logger instance;
return instance;
}
void Logger::SetProgressBar(ProgressBar* pb) {
std::lock_guard<std::mutex> lock(m_Mutex);
m_ProgressBar = pb;
}
void Logger::Info(const std::string& msg) {
std::lock_guard<std::mutex> lock(m_Mutex);
if (m_ProgressBar)
m_ProgressBar->PrintLog(msg);
else
std::cout << msg << std::endl;
}
void Logger::Error(const std::string& msg) {
std::lock_guard<std::mutex> lock(m_Mutex);
if (m_ProgressBar)
m_ProgressBar->PrintLog("[ERROR] " + msg);
else
std::cerr << "[ERROR] " << msg << std::endl;
}
} // namespace PKG

32
src/Core/Logger.h Normal file
View File

@ -0,0 +1,32 @@
#ifndef PKG_LOGGER_H
#define PKG_LOGGER_H
#include <mutex>
#include <string>
namespace PKG {
class ProgressBar; // 前向声明
// 线程安全的日志输出(单例)
// 关联 ProgressBar 后日志会通过进度条协调打印pacman 风格:
// 日志在进度条上方滚动,进度条始终固定在底部)
class Logger {
public:
static Logger& Instance();
void Info(const std::string& msg);
void Error(const std::string& msg);
// 关联/取消关联进度条(线程安全)
void SetProgressBar(ProgressBar* pb);
private:
Logger() = default;
std::mutex m_Mutex;
ProgressBar* m_ProgressBar = nullptr;
};
} // namespace PKG
#endif // PKG_LOGGER_H

59
src/Core/ProcessInfo.cpp Normal file
View File

@ -0,0 +1,59 @@
#include "ProcessInfo.h"
#include <iomanip>
#include <sstream>
// ─── 字节格式化(跨平台通用)──────────────────────────────────────
namespace PKG {
std::string FormatBytes(uint64_t bytes) {
static const char* units[] = {"B", "KB", "MB", "GB"};
int u = 0;
double size = static_cast<double>(bytes);
while (size >= 1024 && u < 3) { size /= 1024; ++u; }
std::ostringstream oss;
oss << std::fixed << std::setprecision(u == 0 ? 0 : 1) << size << " " << units[u];
return oss.str();
}
} // namespace PKG
// ─── POSIX (Linux/macOS) ──────────────────────────────────────────
#if defined(__unix__) || defined(__APPLE__)
#include <sys/resource.h>
#include <unistd.h>
namespace PKG {
size_t GetPeakRssBytes() {
struct rusage usage {};
if (getrusage(RUSAGE_SELF, &usage) != 0)
return 0;
#if defined(__APPLE__)
return static_cast<size_t>(usage.ru_maxrss); // macOS: 字节
#else
return static_cast<size_t>(usage.ru_maxrss) * 1024; // Linux: KB → 字节
#endif
}
} // namespace PKG
// ─── Windows ──────────────────────────────────────────────────────
#elif defined(_WIN32)
#include <windows.h>
#include <psapi.h>
namespace PKG {
size_t GetPeakRssBytes() {
PROCESS_MEMORY_COUNTERS pmc;
if (!GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc)))
return 0;
return static_cast<size_t>(pmc.PeakWorkingSetSize);
}
} // namespace PKG
#else
#warning "Unsupported platform: GetPeakRssBytes returns 0"
namespace PKG {
size_t GetPeakRssBytes() { return 0; }
} // namespace PKG
#endif

23
src/Core/ProcessInfo.h Normal file
View File

@ -0,0 +1,23 @@
#ifndef PKG_PROCESS_INFO_H
#define PKG_PROCESS_INFO_H
// 跨平台进程内存占用查询与字节格式化工具
// POSIX: getrusage(RUSAGE_SELF)
// Windows: GetProcessMemoryInfo
#include <cstddef>
#include <cstdint>
#include <string>
namespace PKG {
// 返回当前进程峰值物理内存占用,单位字节;失败返回 0
size_t GetPeakRssBytes();
// 将字节数格式化为人类可读字符串(如 "1.2 MB"
// 单位自动选择B / KB / MB / GB
std::string FormatBytes(uint64_t bytes);
} // namespace PKG
#endif // PKG_PROCESS_INFO_H

262
src/Core/ProgressBar.cpp Normal file
View File

@ -0,0 +1,262 @@
#include "ProgressBar.h"
#include <cstdio>
#include <string>
// Windows: 启用 VT100 转义序列 + 终端宽度检测
#if defined(_WIN32)
#include <windows.h>
static void EnableVt100() {
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
if (h == INVALID_HANDLE_VALUE)
return;
DWORD mode = 0;
if (!GetConsoleMode(h, &mode))
return;
SetConsoleMode(h, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
#else
#include <sys/ioctl.h>
#include <unistd.h>
static void EnableVt100() {}
#endif
namespace PKG {
// braille 盲文点 spinner 字符序列
static const char *kSpinnerFrames[] = {
"\xe2\xa0\x8b", // ⠋
"\xe2\xa0\x99", // ⠙
"\xe2\xa0\xb9", // ⠹
"\xe2\xa0\xb8", // ⠸
"\xe2\xa0\xbc", // ⠼
"\xe2\xa0\xb4", // ⠴
"\xe2\xa0\xa6", // ⠦
"\xe2\xa0\xa7", // ⠧
"\xe2\xa0\x87", // ⠇
"\xe2\xa0\x8f", // ⠏
};
static const size_t kSpinnerCount = sizeof(kSpinnerFrames) / sizeof(kSpinnerFrames[0]);
// ─── ProgressStyle 预设 ───────────────────────────────────────────
ProgressStyle ProgressStyle::Modern() {
ProgressStyle s;
s.filledChar = "\xe2\x94\x81"; // ━
s.emptyChar = "\xe2\x94\x80"; // ─
s.suffixFormat = " {percent}% ({done}/{total}) {elapsed}s {spinner}";
s.filledColor = "\033[36m"; // cyan
s.emptyColor = "\033[90m"; // gray
s.suffixColor = "\033[2m"; // dim
s.showSpinner = true;
return s;
}
ProgressStyle ProgressStyle::Classic() {
ProgressStyle s;
s.filledChar = "\xe2\x96\x88"; // █
s.emptyChar = "\xe2\x96\x91"; // ░
s.leftBracket = "[";
s.rightBracket = "]";
s.suffixFormat = " {percent}% ({done}/{total}) {elapsed}s";
s.filledColor.clear();
s.emptyColor.clear();
s.suffixColor.clear();
s.showSpinner = false;
return s;
}
ProgressStyle ProgressStyle::Minimal() {
ProgressStyle s;
s.filledChar = "\xe2\x96\x88"; // █
s.emptyChar = " ";
s.suffixFormat = " {percent}% {done}/{total}";
s.filledColor = "\033[32m"; // green
s.showSpinner = false;
return s;
}
ProgressStyle ProgressStyle::Blocks() {
ProgressStyle s;
s.filledChar = "\xe2\x96\x93"; // ▓
s.emptyChar = "\xe2\x96\x91"; // ░
s.suffixFormat = " {percent}% ({done}/{total}) {elapsed}s {spinner}";
s.filledColor = "\033[35m"; // magenta
s.emptyColor = "\033[90m"; // gray
s.suffixColor = "\033[2m"; // dim
s.showSpinner = true;
return s;
}
// ─── ProgressBar ──────────────────────────────────────────────────
ProgressBar::ProgressBar(size_t total, ProgressStyle style)
: m_Total(total), m_Style(std::move(style)), m_Start(std::chrono::steady_clock::now()) {
EnableVt100();
if (m_Total == 0) {
m_Running = false;
return;
}
m_Thread = std::thread(&ProgressBar::RefreshLoop, this);
}
ProgressBar::~ProgressBar() {
if (!m_Running.exchange(false))
return;
if (m_Thread.joinable())
m_Thread.join();
std::lock_guard<std::mutex> lock(m_DrawMutex);
DrawLocked();
std::fputc('\n', stdout);
std::fflush(stdout);
}
void ProgressBar::PrintLog(const std::string &msg) {
std::lock_guard<std::mutex> lock(m_DrawMutex);
// 清除当前进度条行:\r 回到行首,\033[K 清除到行尾
std::fputs("\r\033[K", stdout);
std::fputs(msg.c_str(), stdout);
std::fputc('\n', stdout);
DrawLocked();
std::fflush(stdout);
}
void ProgressBar::RefreshLoop() {
while (m_Running.load(std::memory_order_relaxed)) {
{
std::lock_guard<std::mutex> lock(m_DrawMutex);
DrawLocked();
}
if (m_Done.load(std::memory_order_relaxed) >= m_Total)
break;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int ProgressBar::GetTerminalWidth() {
#if defined(_WIN32)
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO csbi;
if (GetConsoleScreenBufferInfo(h, &csbi))
return csbi.srWindow.Right - csbi.srWindow.Left + 1;
return 80;
#else
struct winsize w;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0 && w.ws_col > 0)
return w.ws_col;
return 80;
#endif
}
// 占位符替换:{percent} {done} {total} {elapsed} {spinner}
std::string ProgressBar::FormatSuffix(size_t done, size_t total, size_t percent, double secs) {
std::string out;
out.reserve(m_Style.suffixFormat.size() + 32);
const std::string &fmt = m_Style.suffixFormat;
for (size_t i = 0; i < fmt.size();) {
if (fmt[i] == '{') {
size_t end = fmt.find('}', i);
if (end == std::string::npos) {
out += fmt[i++];
continue;
}
std::string key = fmt.substr(i + 1, end - i - 1);
if (key == "percent")
out += std::to_string(percent);
else if (key == "done")
out += std::to_string(done);
else if (key == "total")
out += std::to_string(total);
else if (key == "elapsed")
out += std::to_string(secs);
else if (key == "spinner") {
if (m_Style.showSpinner)
out += kSpinnerFrames[m_SpinnerIndex % kSpinnerCount];
} else {
out += fmt.substr(i, end - i + 1);
}
i = end + 1;
} else {
out += fmt[i++];
}
}
return out;
}
void ProgressBar::DrawLocked() {
size_t done = m_Done.load(std::memory_order_relaxed);
size_t total = m_Total;
if (total == 0)
return;
size_t percent = (done * 100) / total;
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - m_Start)
.count();
double secs = elapsed / 1000.0;
// 推进 spinner
m_SpinnerIndex = (m_SpinnerIndex + 1) % kSpinnerCount;
// 格式化后缀(含颜色)
std::string suffix = FormatSuffix(done, total, percent, secs);
if (!m_Style.suffixColor.empty())
suffix = m_Style.suffixColor + suffix + m_Style.resetColor;
// 计算后缀可见宽度(排除 ANSI 转义序列)
size_t suffixVisible = 0;
bool inEscape = false;
for (char c : suffix) {
if (c == '\033') {
inEscape = true;
continue;
}
if (inEscape) {
if (c == 'm')
inEscape = false;
continue;
}
++suffixVisible;
}
int termWidth = GetTerminalWidth();
int fixedWidth = static_cast<int>(m_Style.leftBracket.size()) + static_cast<int>(m_Style.rightBracket.size()) + static_cast<int>(suffixVisible);
int barWidth = termWidth - fixedWidth;
if (barWidth < 1)
barWidth = 1;
size_t filled = (done * static_cast<size_t>(barWidth)) / total;
// 构建进度条
std::string bar;
bar.reserve(static_cast<size_t>(barWidth) * 6); // 含颜色预留
if (!m_Style.leftBracket.empty())
bar += m_Style.leftBracket;
if (!m_Style.filledColor.empty())
bar += m_Style.filledColor;
for (int i = 0; i < barWidth; ++i) {
if (static_cast<size_t>(i) < filled) {
bar += m_Style.filledChar;
} else {
// 切换到 empty 颜色(仅首次遇到空白时)
if (i > 0 && !m_Style.emptyColor.empty())
bar += m_Style.resetColor + m_Style.emptyColor;
bar += m_Style.emptyChar;
}
}
if (!m_Style.filledColor.empty() || !m_Style.emptyColor.empty())
bar += m_Style.resetColor;
if (!m_Style.rightBracket.empty())
bar += m_Style.rightBracket;
bar += suffix;
std::fprintf(stdout, "\r%s", bar.c_str());
std::fflush(stdout);
}
} // namespace PKG

73
src/Core/ProgressBar.h Normal file
View File

@ -0,0 +1,73 @@
#ifndef PKG_PROGRESS_BAR_H
#define PKG_PROGRESS_BAR_H
#include <atomic>
#include <chrono>
#include <cstddef>
#include <mutex>
#include <string>
#include <thread>
namespace PKG {
// 进度条样式(可定制)
// 后缀格式支持占位符:{percent} {done} {total} {elapsed} {spinner}
struct ProgressStyle {
std::string filledChar = "\xe2\x94\x81"; // ━ U+2501 heavy horizontal
std::string emptyChar = "\xe2\x94\x80"; // ─ U+2500 light horizontal
std::string leftBracket;
std::string rightBracket;
std::string suffixFormat = " {percent}% ({done}/{total}) {elapsed}s {spinner}";
// ANSI 颜色(空字符串表示不着色)
std::string filledColor = "\033[36m"; // cyan
std::string emptyColor = "\033[90m"; // bright black (gray)
std::string suffixColor = "\033[2m"; // dim
std::string resetColor = "\033[0m";
bool showSpinner = true;
// 预设样式
static ProgressStyle Modern(); // 横线 + 青色 + braille spinner默认
static ProgressStyle Classic(); // 方块 █░ + 无色 + 括号
static ProgressStyle Minimal(); // 极简:百分比 + 计数
static ProgressStyle Blocks(); // 彩色方块 ▓░ + spinner
};
// 线程安全终端进度条
// - 全宽度自适应终端宽度
// - 日志在进度条上方滚动打印(通过 PrintLog 协调pacman 风格)
// - 样式可通过构造函数定制
class ProgressBar {
public:
explicit ProgressBar(size_t total, ProgressStyle style = ProgressStyle::Modern());
~ProgressBar();
ProgressBar(const ProgressBar&) = delete;
ProgressBar& operator=(const ProgressBar&) = delete;
// 原子递增已完成数(线程安全)
void Increment() { m_Done.fetch_add(1, std::memory_order_relaxed); }
// 打印日志:清除进度条行 → 打印日志 → 重绘进度条
void PrintLog(const std::string& msg);
private:
void RefreshLoop();
void DrawLocked();
int GetTerminalWidth();
std::string FormatSuffix(size_t done, size_t total, size_t percent, double secs);
std::atomic<size_t> m_Done{0};
size_t m_Total;
ProgressStyle m_Style;
std::atomic<bool> m_Running{true};
std::thread m_Thread;
std::chrono::steady_clock::time_point m_Start;
std::mutex m_DrawMutex;
size_t m_SpinnerIndex{0};
};
} // namespace PKG
#endif // PKG_PROGRESS_BAR_H

77
src/Core/Result.h Normal file
View File

@ -0,0 +1,77 @@
#ifndef PKG_RESULT_H
#define PKG_RESULT_H
#include <string>
#include <utility>
namespace PKG {
// ===== 错误码 =====
enum class ErrorCode {
None,
FileOpen, // 文件打开失败
FileCreate, // 文件创建失败
BadMagic, // 魔数校验失败(格式不符)
ReadFailed, // 读取失败
WriteFailed, // 写入失败
DecompressFailed, // 解压失败LZ4/DXT
EncodeFailed, // 编码失败PNG/GIF
UnsupportedFormat, // 不支持的格式
InvalidArgument, // 无效参数
};
// ===== 错误信息 =====
struct Error {
ErrorCode code = ErrorCode::None;
std::string message;
bool IsOk() const { return code == ErrorCode::None; }
explicit operator bool() const { return IsOk(); }
};
// ===== Result<T>: 携带值与错误的返回类型 =====
template <typename T>
struct Result {
T value;
Error error;
bool IsOk() const { return error.IsOk(); }
explicit operator bool() const { return IsOk(); }
T &operator*() { return value; }
const T &operator*() const { return value; }
T *operator->() { return &value; }
const T *operator->() const { return &value; }
};
// ===== Result<void> 特化:仅含错误 =====
template <>
struct Result<void> {
Error error;
bool IsOk() const { return error.IsOk(); }
explicit operator bool() const { return IsOk(); }
};
// ===== 工厂函数 =====
template <typename T>
inline Result<T> Ok(T value) {
return Result<T>{std::move(value), Error{}};
}
inline Result<void> Ok() {
return Result<void>{Error{}};
}
template <typename T>
inline Result<T> Fail(ErrorCode code, std::string message = "") {
return Result<T>{T{}, Error{code, std::move(message)}};
}
inline Result<void> Fail(ErrorCode code, std::string message = "") {
return Result<void>{Error{code, std::move(message)}};
}
} // namespace PKG
#endif // PKG_RESULT_H

223
src/Core/Types.h Normal file
View File

@ -0,0 +1,223 @@
#ifndef PKG_TYPES_H
#define PKG_TYPES_H
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
namespace PKG {
// ===== 纹理原始格式TEX 文件头中声明)=====
enum class TexFormat {
RGBA8888 = 0, // 未压缩 RGBA
DXT5 = 4, // DXT5 压缩(含 alpha
DXT3 = 6, // DXT3 压缩(显式 alpha
DXT1 = 7, // DXT1 压缩(无 alpha
RG88 = 8, // 双通道
R8 = 9, // 单通道
};
// ===== 纹理标志位(可按位组合)=====
enum class TexType {
None = 0,
NoInterpolation = 1, // 禁用纹理过滤
ClampUVs = 2, // UV 钳制(非重复)
IsGif = 4, // GIF 动画纹理
Unk3 = 8,
Unk4 = 16,
IsVideoTexture = 32, // 视频纹理MP4
Unk6 = 64,
Unk7 = 128,
};
// ===== Mipmap 数据格式 =====
enum class MipmapFormat {
Invalid = 0,
// 原始像素格式1-3
RGBA8888 = 1,
R8 = 2,
RG88 = 3,
// DXT 块压缩格式
CompressedDXT5,
CompressedDXT3,
CompressedDXT1,
// 视频
VideoMp4,
// 已编码图像格式(>= 1000对应 FreeImage 格式)
ImageBMP = 1000,
ImageICO,
ImageJPEG,
ImageJNG,
ImageKOALA,
ImageLBM,
ImageIFF,
ImageMNG,
ImagePBM,
ImagePBMRAW,
ImagePCD,
ImagePCX,
ImagePGM,
ImagePGMRAW,
ImagePNG,
ImagePPM,
ImagePPMRAW,
ImageRAS,
ImageTARGA,
ImageTIFF,
ImageWBMP,
ImagePSD,
ImageCUT,
ImageXBM,
ImageXPM,
ImageDDS,
ImageGIF,
ImageHDR,
ImageFAXG3,
ImageSGI,
ImageEXR,
ImageJ2K,
ImageJP2,
ImagePFM,
ImagePICT,
ImageRAW,
};
// ===== FreeImage 格式标识(对应原项目 FreeImageFormat 枚举)=====
enum class FreeImageFormat {
FIF_UNKNOWN = -1,
FIF_BMP = 0,
FIF_ICO,
FIF_JPEG,
FIF_JNG,
FIF_KOALA,
FIF_LBM,
FIF_IFF = 5,
FIF_MNG,
FIF_PBM,
FIF_PBMRAW,
FIF_PCD,
FIF_PCX,
FIF_PGM,
FIF_PGMRAW,
FIF_PNG,
FIF_PPM,
FIF_PPMRAW,
FIF_RAS,
FIF_TARGA,
FIF_TIFF,
FIF_WBMP,
FIF_PSD,
FIF_CUT,
FIF_XBM,
FIF_XPM,
FIF_DDS,
FIF_GIF,
FIF_HDR,
FIF_FAXG3,
FIF_SGI,
FIF_EXR,
FIF_J2K,
FIF_JP2,
FIF_PFM,
FIF_PICT,
FIF_RAW,
FIF_MP4 = 35,
};
// ===== 图像容器版本(对应 TEXB0001~0004=====
enum class ImageContainerVersion {
VERSION1 = 1,
VERSION2 = 2,
VERSION3 = 3,
VERSION4 = 4,
};
// ===== DXT 解压标志 =====
enum class DXTFlags {
DXT1 = 1,
DXT3 = 1 << 1,
DXT5 = 1 << 2,
};
// ===== 数据结构 =====
// PKG 索引条目:描述一个文件在 PKG 中的位置与类型
struct Entry {
std::filesystem::path FullPath; // 文件相对路径
int32_t Offset{}; // 相对数据区起始的偏移
int32_t Length{}; // 数据长度(字节)
std::string Type; // 扩展名(含点,如 ".tex"
};
// TEX 文件头
struct TexHeader {
TexFormat Format = TexFormat::RGBA8888;
TexType Flags = TexType::None;
int32_t TextureWidth = 0; // 纹理尺寸(可能含 padding
int32_t TextureHeight = 0;
int32_t ImageWidth = 0; // 实际图像尺寸
int32_t ImageHeight = 0;
uint32_t UnkInt0 = 0; // 未知字段
};
// 单个 mipmap 层级
struct TexMipMap {
int Width = 0;
int Height = 0;
int DecompressedDataCount = 0; // LZ4 解压后大小
bool IsZ4Compressed = false;
MipmapFormat Format = MipmapFormat::Invalid;
std::vector<uint8_t> Data; // 像素或压缩数据
};
// 一张图像(可含多个 mipmap 层级)
struct TexImage {
std::vector<TexMipMap> Mipmaps;
};
// 图像容器TEXB 块)
struct TexImageContainer {
std::string Magic; // "TEXB0001"~"TEXB0004"
FreeImageFormat ImageFormat = FreeImageFormat::FIF_UNKNOWN;
ImageContainerVersion imageContainerVersion = ImageContainerVersion::VERSION1;
std::vector<TexImage> Images;
};
// GIF 单帧信息(位置、尺寸、帧时长)
struct TexFrameInfo {
int ImageId = 0;
float Frametime = 0.0f; // 秒
float PosX = 0.0f, PosY = 0.0f;
float Width = 0.0f, WidthY = 0.0f, HeightX = 0.0f, Height = 0.0f;
};
// GIF 帧信息容器TEXS 块)
struct TexFrameInfoContainer {
std::string Magic; // "TEXS0001" 或 "TEXS0003"
std::vector<TexFrameInfo> Frames;
int GifWidth = 0; // GIF 画布尺寸
int GifHeight = 0;
};
// 完整 TEX 文件结构
struct Tex {
std::string Magic1; // "TEXV0005"
std::string Magic2; // "TEXI0001"
TexHeader Header;
TexImageContainer ImageContainer;
bool IsGif = false; // 是否为 GIF 动画
bool IsVideoTexture = false; // 是否为视频纹理
TexFrameInfoContainer FrameInfoContainer;
};
// 根据 MipmapFormat 获取对应文件扩展名(不含点,如 "png"
std::string GetFileExtension(MipmapFormat format);
} // namespace PKG
#endif // PKG_TYPES_H

142
src/Decoder/DxtDecoder.cpp Normal file
View File

@ -0,0 +1,142 @@
#include "DxtDecoder.h"
namespace PKG {
// DXT 解压:每 4x4 像素为一个块
// - DXT1: 8 字节/块仅颜色1-bit alpha
// - DXT3: 16 字节/块(显式 alpha + 颜色)
// - DXT5: 16 字节/块(插值 alpha + 颜色)
void DxtDecoder::DecompressImage(int width, int height, std::vector<uint8_t>& data, DXTFlags flags) {
std::vector<uint8_t> rgba(width * height * 4);
int sourceBlockPos = 0;
int bytesPerBlock = (flags == DXTFlags::DXT1) ? 8 : 16;
// 循环外分配,避免每块重复分配
std::vector<uint8_t> targetRGBA(4 * 16); // 当前块 4x4 像素的 RGBA
std::vector<uint8_t> codes(16); // 4 个颜色调色板RGBA
std::vector<uint8_t> indices(16); // 每像素的调色板索引2-bit
std::vector<uint8_t> alphaCodes(8); // DXT5: 8 个 alpha 值
std::vector<uint8_t> alphaIndices(16); // DXT5: 每像素的 alpha 索引3-bit
for (int y = 0; y < height; y += 4) {
for (int x = 0; x < width; x += 4) {
if (static_cast<size_t>(sourceBlockPos) >= data.size())
continue;
// 颜色块位置DXT3/DXT5 前 8 字节是 alpha
int colorBlockIndex = sourceBlockPos;
if (flags == DXTFlags::DXT3 || flags == DXTFlags::DXT5)
colorBlockIndex += 8;
// 解码 2 个 16-bit RGB565 基色,插值出 4 个颜色
int a = data[colorBlockIndex] | (data[colorBlockIndex + 1] << 8);
int b = data[colorBlockIndex + 2] | (data[colorBlockIndex + 3] << 8);
codes[0] = (a >> 11) & 0x1F;
codes[1] = (a >> 5) & 0x3F;
codes[2] = a & 0x1F;
codes[3] = 255;
codes[4] = (b >> 11) & 0x1F;
codes[5] = (b >> 5) & 0x3F;
codes[6] = b & 0x1F;
codes[7] = 255;
for (int i = 0; i < 3; i++) {
int c = codes[i];
int d = codes[4 + i];
if (flags == DXTFlags::DXT1 && a <= b) {
codes[8 + i] = static_cast<uint8_t>((c + d) / 2);
codes[12 + i] = 0;
} else {
codes[8 + i] = static_cast<uint8_t>((2 * c + d) / 3);
codes[12 + i] = static_cast<uint8_t>((c + 2 * d) / 3);
}
}
codes[8 + 3] = 255;
codes[12 + 3] = (flags == DXTFlags::DXT1 && a <= b) ? 0 : 255;
// 解码 4x4 像素的 2-bit 颜色索引
for (int i = 0; i < 4; i++) {
int packed = data[colorBlockIndex + 4 + i];
indices[0 + i * 4] = packed & 0x3;
indices[1 + i * 4] = (packed >> 2) & 0x3;
indices[2 + i * 4] = (packed >> 4) & 0x3;
indices[3 + i * 4] = (packed >> 6) & 0x3;
}
// 根据索引查表得到 RGBA
for (int i = 0; i < 16; i++) {
int offset = 4 * indices[i];
targetRGBA[4 * i + 0] = (codes[offset] << 3) | (codes[offset] >> 2);
targetRGBA[4 * i + 1] = (codes[offset + 1] << 2) | (codes[offset + 1] >> 4);
targetRGBA[4 * i + 2] = (codes[offset + 2] << 3) | (codes[offset + 2] >> 2);
targetRGBA[4 * i + 3] = codes[offset + 3];
}
// 解码 alpha
if (flags == DXTFlags::DXT3) {
// DXT3: 显式 4-bit alpha
for (int i = 0; i < 8; i++) {
int quant = data[sourceBlockPos + i];
uint8_t lo = quant & 0x0F;
uint8_t hi = (quant >> 4) & 0x0F;
targetRGBA[8 * i + 3] = lo | (lo << 4);
targetRGBA[8 * i + 7] = hi | (hi << 4);
}
} else if (flags == DXTFlags::DXT5) {
// DXT5: 2 个 alpha 基值 + 3-bit 索引插值
uint8_t alpha0 = data[sourceBlockPos + 0];
uint8_t alpha1 = data[sourceBlockPos + 1];
alphaCodes[0] = alpha0;
alphaCodes[1] = alpha1;
if (alpha0 <= alpha1) {
for (int i = 1; i < 5; i++)
alphaCodes[1 + i] = static_cast<uint8_t>(((5 - i) * alpha0 + i * alpha1) / 5);
alphaCodes[6] = 0;
alphaCodes[7] = 255;
} else {
for (int i = 1; i < 7; i++)
alphaCodes[i + 1] = static_cast<uint8_t>(((7 - i) * alpha0 + i * alpha1) / 7);
}
// 解码 16 个 3-bit alpha 索引(共 6 字节)
int blockSrcPos = 2;
int indicesPos = 0;
for (int i = 0; i < 2; i++) {
int value = 0;
for (int j = 0; j < 3; j++)
value |= (data[sourceBlockPos + blockSrcPos++] << (8 * j));
for (int j = 0; j < 8; j++)
alphaIndices[indicesPos++] = (value >> (3 * j)) & 0x07;
}
for (int i = 0; i < 16; i++)
targetRGBA[4 * i + 3] = alphaCodes[alphaIndices[i]];
}
// 将 4x4 块写入目标图像
uint8_t targetRGBA_pos = 0;
for (int py = 0; py < 4; py++) {
for (int px = 0; px < 4; px++) {
int sx = x + px;
int sy = y + py;
if (sx < width && sy < height) {
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;
}
}
sourceBlockPos += bytesPerBlock;
}
}
data = std::move(rgba);
}
} // namespace PKG

18
src/Decoder/DxtDecoder.h Normal file
View File

@ -0,0 +1,18 @@
#ifndef PKG_DXT_DECODER_H
#define PKG_DXT_DECODER_H
#include "Core/Types.h"
namespace PKG {
// DXT 块压缩纹理解码器
// 将 DXT1/DXT3/DXT5 压缩数据解压为 RGBA8888
class DxtDecoder {
public:
// 就地解压data 既输入压缩数据,也输出 RGBA8888 数据
static void DecompressImage(int width, int height, std::vector<uint8_t>& data, DXTFlags flags);
};
} // namespace PKG
#endif // PKG_DXT_DECODER_H

264
src/Decoder/ImageReader.cpp Normal file
View File

@ -0,0 +1,264 @@
#include "ImageReader.h"
#include "lz4.h"
namespace PKG {
// LZ4 解压
static std::vector<uint8_t> Lz4Decompress(const unsigned char* compressedData, size_t compressedSize, size_t decompressedSize) {
std::vector<uint8_t> output(decompressedSize);
int result = LZ4_decompress_safe(
reinterpret_cast<const char*>(compressedData),
reinterpret_cast<char*>(output.data()),
static_cast<int>(compressedSize),
static_cast<int>(decompressedSize));
if (result < 0 || static_cast<size_t>(result) != decompressedSize)
throw std::runtime_error("LZ4 decompression failed");
return output;
}
// ─── Mipmap 读取(按容器版本区分)──────────────────────────────────
// V1: [width][height][count][data:count]
static Result<TexMipMap> ReadMipMapV1(StreamReader& reader) {
auto widthRes = reader.ReadInt32();
if (!widthRes) return Fail<TexMipMap>(widthRes.error.code, widthRes.error.message);
auto heightRes = reader.ReadInt32();
if (!heightRes) return Fail<TexMipMap>(heightRes.error.code, heightRes.error.message);
auto countRes = reader.ReadInt32();
if (!countRes) return Fail<TexMipMap>(countRes.error.code, countRes.error.message);
std::vector<uint8_t> bytes;
auto dataRes = reader.ReadData(bytes, *countRes);
if (!dataRes) return Fail<TexMipMap>(dataRes.error.code, dataRes.error.message);
return Ok(TexMipMap{ *widthRes, *heightRes, 0, false, MipmapFormat::Invalid, std::move(bytes) });
}
// V2/V3: [width][height][compressed][decompressed][count][data:count]
static Result<TexMipMap> ReadMipMapV2AndV3(StreamReader& reader) {
auto widthRes = reader.ReadInt32();
if (!widthRes) return Fail<TexMipMap>(widthRes.error.code, widthRes.error.message);
auto heightRes = reader.ReadInt32();
if (!heightRes) return Fail<TexMipMap>(heightRes.error.code, heightRes.error.message);
auto compressedRes = reader.ReadInt32();
if (!compressedRes) return Fail<TexMipMap>(compressedRes.error.code, compressedRes.error.message);
auto decompressedRes = reader.ReadInt32();
if (!decompressedRes) return Fail<TexMipMap>(decompressedRes.error.code, decompressedRes.error.message);
auto countRes = reader.ReadInt32();
if (!countRes) return Fail<TexMipMap>(countRes.error.code, countRes.error.message);
std::vector<uint8_t> bytes;
auto dataRes = reader.ReadData(bytes, *countRes);
if (!dataRes) return Fail<TexMipMap>(dataRes.error.code, dataRes.error.message);
return Ok(TexMipMap{ *widthRes, *heightRes, *decompressedRes,
(*compressedRes) == 1, MipmapFormat::Invalid, std::move(bytes) });
}
// V4: [param1][param2][condition:NString][param3] + V2/V3 数据
static Result<TexMipMap> ReadMipMapV4(StreamReader& reader) {
auto param1Res = reader.ReadInt32();
if (!param1Res) return Fail<TexMipMap>(param1Res.error.code, param1Res.error.message);
auto param2Res = reader.ReadInt32();
if (!param2Res) return Fail<TexMipMap>(param2Res.error.code, param2Res.error.message);
auto conditionRes = reader.ReadNString();
if (!conditionRes) return Fail<TexMipMap>(conditionRes.error.code, conditionRes.error.message);
auto param3Res = reader.ReadInt32();
if (!param3Res) return Fail<TexMipMap>(param3Res.error.code, param3Res.error.message);
return ReadMipMapV2AndV3(reader);
}
// ─── 格式映射 ─────────────────────────────────────────────────────
// FreeImage 格式 → MipmapFormat已编码图像
static MipmapFormat FreeImageToMipmap(FreeImageFormat fmt) {
switch (fmt) {
case FreeImageFormat::FIF_BMP: return MipmapFormat::ImageBMP;
case FreeImageFormat::FIF_ICO: return MipmapFormat::ImageICO;
case FreeImageFormat::FIF_JPEG: return MipmapFormat::ImageJPEG;
case FreeImageFormat::FIF_JNG: return MipmapFormat::ImageJNG;
case FreeImageFormat::FIF_KOALA: return MipmapFormat::ImageKOALA;
case FreeImageFormat::FIF_LBM: return MipmapFormat::ImageLBM;
// FIF_IFF 与 FIF_LBM 同值(均为 5共用一个 case
case FreeImageFormat::FIF_MNG: return MipmapFormat::ImageMNG;
case FreeImageFormat::FIF_PBM: return MipmapFormat::ImagePBM;
case FreeImageFormat::FIF_PBMRAW: return MipmapFormat::ImagePBMRAW;
case FreeImageFormat::FIF_PCD: return MipmapFormat::ImagePCD;
case FreeImageFormat::FIF_PCX: return MipmapFormat::ImagePCX;
case FreeImageFormat::FIF_PGM: return MipmapFormat::ImagePGM;
case FreeImageFormat::FIF_PGMRAW: return MipmapFormat::ImagePGMRAW;
case FreeImageFormat::FIF_PNG: return MipmapFormat::ImagePNG;
case FreeImageFormat::FIF_PPM: return MipmapFormat::ImagePPM;
case FreeImageFormat::FIF_PPMRAW: return MipmapFormat::ImagePPMRAW;
case FreeImageFormat::FIF_RAS: return MipmapFormat::ImageRAS;
case FreeImageFormat::FIF_TARGA: return MipmapFormat::ImageTARGA;
case FreeImageFormat::FIF_TIFF: return MipmapFormat::ImageTIFF;
case FreeImageFormat::FIF_WBMP: return MipmapFormat::ImageWBMP;
case FreeImageFormat::FIF_PSD: return MipmapFormat::ImagePSD;
case FreeImageFormat::FIF_CUT: return MipmapFormat::ImageCUT;
case FreeImageFormat::FIF_XBM: return MipmapFormat::ImageXBM;
case FreeImageFormat::FIF_XPM: return MipmapFormat::ImageXPM;
case FreeImageFormat::FIF_DDS: return MipmapFormat::ImageDDS;
case FreeImageFormat::FIF_GIF: return MipmapFormat::ImageGIF;
case FreeImageFormat::FIF_HDR: return MipmapFormat::ImageHDR;
case FreeImageFormat::FIF_FAXG3: return MipmapFormat::ImageFAXG3;
case FreeImageFormat::FIF_SGI: return MipmapFormat::ImageSGI;
case FreeImageFormat::FIF_EXR: return MipmapFormat::ImageEXR;
case FreeImageFormat::FIF_J2K: return MipmapFormat::ImageJ2K;
case FreeImageFormat::FIF_JP2: return MipmapFormat::ImageJP2;
case FreeImageFormat::FIF_PFM: return MipmapFormat::ImagePFM;
case FreeImageFormat::FIF_PICT: return MipmapFormat::ImagePICT;
case FreeImageFormat::FIF_RAW: return MipmapFormat::ImageRAW;
case FreeImageFormat::FIF_MP4: return MipmapFormat::VideoMp4;
default: return MipmapFormat::Invalid;
}
}
// TexFormat → MipmapFormat原始像素或 DXT 压缩)
static MipmapFormat TexToMipmap(TexFormat fmt) {
switch (fmt) {
case TexFormat::RGBA8888: return MipmapFormat::RGBA8888;
case TexFormat::DXT5: return MipmapFormat::CompressedDXT5;
case TexFormat::DXT3: return MipmapFormat::CompressedDXT3;
case TexFormat::DXT1: return MipmapFormat::CompressedDXT1;
case TexFormat::RG88: return MipmapFormat::RG88;
case TexFormat::R8: return MipmapFormat::R8;
default: return MipmapFormat::Invalid;
}
}
// 综合格式判定:优先使用 FreeImage 格式,否则用 TexFormat
static MipmapFormat GetFormatFromTex(FreeImageFormat imageFormat, TexFormat format) {
if (imageFormat != FreeImageFormat::FIF_UNKNOWN)
return FreeImageToMipmap(imageFormat);
return TexToMipmap(format);
}
// ─── 公开接口 ─────────────────────────────────────────────────────
Result<TexImage> ImageReader::ReadFrom(StreamReader& reader, const TexImageContainer& container, TexFormat format) {
auto mipMapCountRes = reader.ReadInt32();
if (!mipMapCountRes)
return Fail<TexImage>(mipMapCountRes.error.code, mipMapCountRes.error.message);
MipmapFormat aFormat = GetFormatFromTex(container.ImageFormat, format);
TexImage image{};
for (int i = 0; i < *mipMapCountRes; i++) {
TexMipMap mipmap{};
switch (container.imageContainerVersion) {
case ImageContainerVersion::VERSION1:
{
auto res = ReadMipMapV1(reader);
if (!res) return Fail<TexImage>(res.error.code, res.error.message);
mipmap = *res;
break;
}
case ImageContainerVersion::VERSION2:
case ImageContainerVersion::VERSION3:
{
auto res = ReadMipMapV2AndV3(reader);
if (!res) return Fail<TexImage>(res.error.code, res.error.message);
mipmap = *res;
break;
}
case ImageContainerVersion::VERSION4:
{
auto res = ReadMipMapV4(reader);
if (!res) return Fail<TexImage>(res.error.code, res.error.message);
mipmap = *res;
break;
}
}
mipmap.Format = aFormat;
// LZ4 解压
if (mipmap.IsZ4Compressed) {
try {
mipmap.Data = Lz4Decompress(mipmap.Data.data(), mipmap.Data.size(), mipmap.DecompressedDataCount);
mipmap.IsZ4Compressed = false;
} catch (const std::exception& e) {
return Fail<TexImage>(ErrorCode::DecompressFailed, e.what());
}
}
image.Mipmaps.push_back(mipmap);
}
return Ok(std::move(image));
}
// TEXB 容器结构:
// [magic:NString] "TEXB0001"~"TEXB0004"
// [imageCount:int32]
// V3: [imageFormat:int32]
// V4: [imageFormat:int32][isVideo:int32] (FIF_UNKNOWN + isVideo=1 → MP4)
// repeat imageCount: <TexImage>
Result<TexImageContainer> ImageReader::ReadContainer(StreamReader& reader, TexFormat texFormat) {
TexImageContainer container{};
auto magicRes = reader.ReadNString();
if (!magicRes)
return Fail<TexImageContainer>(magicRes.error.code, magicRes.error.message);
container.Magic = *magicRes;
auto imageCountRes = reader.ReadInt32();
if (!imageCountRes)
return Fail<TexImageContainer>(imageCountRes.error.code, imageCountRes.error.message);
// 按版本读取附加字段
if (container.Magic == "TEXB0003") {
auto formatRes = reader.ReadInt32();
if (!formatRes) return Fail<TexImageContainer>(formatRes.error.code, formatRes.error.message);
container.ImageFormat = static_cast<FreeImageFormat>(*formatRes);
} else if (container.Magic == "TEXB0004") {
auto formatRes = reader.ReadInt32();
if (!formatRes) return Fail<TexImageContainer>(formatRes.error.code, formatRes.error.message);
auto format = static_cast<FreeImageFormat>(*formatRes);
auto isVideoRes = reader.ReadInt32();
if (!isVideoRes) return Fail<TexImageContainer>(isVideoRes.error.code, isVideoRes.error.message);
// 视频纹理FIF_UNKNOWN + isVideo=1 → MP4
if (format == FreeImageFormat::FIF_UNKNOWN && (*isVideoRes) == 1)
format = FreeImageFormat::FIF_MP4;
container.ImageFormat = format;
} else if (container.Magic != "TEXB0001" && container.Magic != "TEXB0002") {
return Fail<TexImageContainer>(ErrorCode::BadMagic, "Invalid TEXB magic: " + container.Magic);
}
// 从魔数提取版本号("TEXB0003" → 3
int version = std::stoi(container.Magic.substr(4, 4));
container.imageContainerVersion = static_cast<ImageContainerVersion>(version);
// V4 仅对 MP4 有效,其余降级为 V3
if (container.imageContainerVersion == ImageContainerVersion::VERSION4
&& container.ImageFormat != FreeImageFormat::FIF_MP4)
container.imageContainerVersion = ImageContainerVersion::VERSION3;
for (int i = 0; i < *imageCountRes; i++) {
auto imageRes = ReadFrom(reader, container, texFormat);
if (!imageRes)
return Fail<TexImageContainer>(imageRes.error.code, imageRes.error.message);
container.Images.push_back(*imageRes);
}
return Ok(std::move(container));
}
} // namespace PKG

22
src/Decoder/ImageReader.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef PKG_IMAGE_READER_H
#define PKG_IMAGE_READER_H
#include "Core/Types.h"
#include "Core/Result.h"
#include "IO/StreamReader.h"
namespace PKG {
// TEX 图像容器与 mipmap 读取器
class ImageReader {
public:
// 读取单张图像(含多个 mipmap 层级)
static Result<TexImage> ReadFrom(StreamReader& reader, const TexImageContainer& container, TexFormat format);
// 读取图像容器TEXB 块)
static Result<TexImageContainer> ReadContainer(StreamReader& reader, TexFormat texFormat);
};
} // namespace PKG
#endif // PKG_IMAGE_READER_H

View File

@ -0,0 +1,67 @@
#include "PkgExtractor.h"
namespace PKG {
// PKG 文件结构:
// [magicLen:int32][magic:string(magicLen)] // "PKG..."
// [count:int32]
// repeat count:
// [pathLen:int32][path:string(pathLen)]
// [offset:int32][length:int32]
// <数据区offset 相对此处>
Result<std::vector<Entry>> PkgExtractor::ParseIndex(StreamReader& reader) {
// 读取魔数
auto headRes = reader.ReadInt32();
if (!headRes)
return Fail<std::vector<Entry>>(headRes.error.code, headRes.error.message);
auto magicHeaderRes = reader.ReadString(*headRes);
if (!magicHeaderRes)
return Fail<std::vector<Entry>>(magicHeaderRes.error.code, magicHeaderRes.error.message);
if (magicHeaderRes->substr(0, 3) != "PKG")
return Fail<std::vector<Entry>>(ErrorCode::BadMagic, "Invalid PKG header: " + *magicHeaderRes);
// 读取条目数
auto countRes = reader.ReadInt32();
if (!countRes)
return Fail<std::vector<Entry>>(countRes.error.code, countRes.error.message);
if (*countRes <= 0)
return Fail<std::vector<Entry>>(ErrorCode::InvalidArgument, "No entries found");
// 逐条解析索引
std::vector<Entry> entries;
entries.reserve(*countRes);
for (int i = 0; i < *countRes; i++) {
Entry entry{};
auto sizeRes = reader.ReadInt32();
if (!sizeRes)
return Fail<std::vector<Entry>>(sizeRes.error.code, sizeRes.error.message);
auto pathRes = reader.ReadString(*sizeRes);
if (!pathRes)
return Fail<std::vector<Entry>>(pathRes.error.code, pathRes.error.message);
entry.FullPath = std::filesystem::path(*pathRes).make_preferred();
auto offsetRes = reader.ReadInt32();
if (!offsetRes)
return Fail<std::vector<Entry>>(offsetRes.error.code, offsetRes.error.message);
entry.Offset = *offsetRes;
auto lengthRes = reader.ReadInt32();
if (!lengthRes)
return Fail<std::vector<Entry>>(lengthRes.error.code, lengthRes.error.message);
entry.Length = *lengthRes;
entry.Type = entry.FullPath.extension().string();
entries.push_back(entry);
}
return Ok(std::move(entries));
}
} // namespace PKG

View File

@ -0,0 +1,19 @@
#ifndef PKG_PKG_EXTRACTOR_H
#define PKG_PKG_EXTRACTOR_H
#include "Core/Types.h"
#include "Core/Result.h"
#include "IO/StreamReader.h"
namespace PKG {
// PKG/MPKG 包索引解析器
// 解析包头部与文件索引表,返回所有条目的元信息
class PkgExtractor {
public:
Result<std::vector<Entry>> ParseIndex(StreamReader& reader);
};
} // namespace PKG
#endif // PKG_PKG_EXTRACTOR_H

View File

@ -0,0 +1,75 @@
#include "TexDecoder.h"
#include "ImageReader.h"
namespace PKG {
// TEX 文件结构:
// [magic1:NString(16)] "TEXV0005"
// [magic2:NString(16)] "TEXI0001"
// [format:int32] TexFormat
// [flags:int32] TexType 标志位
// [texW:int32][texH:int32] 纹理尺寸
// [imgW:int32][imgH:int32] 图像尺寸
// [unk:uint32]
// <TexImageContainer> 图像数据TEXB 块)
Result<Tex> TexDecoder::Decode(StreamReader& reader) {
Tex tex{};
// 校验魔数
auto magic1Res = reader.ReadNString(16);
if (!magic1Res)
return Fail<Tex>(magic1Res.error.code, magic1Res.error.message);
tex.Magic1 = *magic1Res;
if (tex.Magic1 != "TEXV0005")
return Fail<Tex>(ErrorCode::BadMagic, "Invalid TEX magic1: " + tex.Magic1);
auto magic2Res = reader.ReadNString(16);
if (!magic2Res)
return Fail<Tex>(magic2Res.error.code, magic2Res.error.message);
tex.Magic2 = *magic2Res;
if (tex.Magic2 != "TEXI0001")
return Fail<Tex>(ErrorCode::BadMagic, "Invalid TEX magic2: " + tex.Magic2);
// 读取头部字段
auto formatRes = reader.ReadInt32();
if (!formatRes) return Fail<Tex>(formatRes.error.code, formatRes.error.message);
tex.Header.Format = static_cast<TexFormat>(*formatRes);
auto flagsRes = reader.ReadInt32();
if (!flagsRes) return Fail<Tex>(flagsRes.error.code, flagsRes.error.message);
tex.Header.Flags = static_cast<TexType>(*flagsRes);
auto texWidthRes = reader.ReadInt32();
if (!texWidthRes) return Fail<Tex>(texWidthRes.error.code, texWidthRes.error.message);
tex.Header.TextureWidth = *texWidthRes;
auto texHeightRes = reader.ReadInt32();
if (!texHeightRes) return Fail<Tex>(texHeightRes.error.code, texHeightRes.error.message);
tex.Header.TextureHeight = *texHeightRes;
auto imgWidthRes = reader.ReadInt32();
if (!imgWidthRes) return Fail<Tex>(imgWidthRes.error.code, imgWidthRes.error.message);
tex.Header.ImageWidth = *imgWidthRes;
auto imgHeightRes = reader.ReadInt32();
if (!imgHeightRes) return Fail<Tex>(imgHeightRes.error.code, imgHeightRes.error.message);
tex.Header.ImageHeight = *imgHeightRes;
auto unkRes = reader.ReadUInt32();
if (!unkRes) return Fail<Tex>(unkRes.error.code, unkRes.error.message);
tex.Header.UnkInt0 = *unkRes;
// 解析标志位
tex.IsGif = (static_cast<int>(tex.Header.Flags) & static_cast<int>(TexType::IsGif)) != 0;
tex.IsVideoTexture = (static_cast<int>(tex.Header.Flags) & static_cast<int>(TexType::IsVideoTexture)) != 0;
// 读取图像容器
auto containerRes = ImageReader::ReadContainer(reader, tex.Header.Format);
if (!containerRes)
return Fail<Tex>(containerRes.error.code, containerRes.error.message);
tex.ImageContainer = *containerRes;
return Ok(std::move(tex));
}
} // namespace PKG

20
src/Decoder/TexDecoder.h Normal file
View File

@ -0,0 +1,20 @@
#ifndef PKG_TEX_DECODER_H
#define PKG_TEX_DECODER_H
#include "Core/Types.h"
#include "Core/Result.h"
#include "IO/StreamReader.h"
namespace PKG {
// TEX 纹理文件解码器
// 解析 TEXV0005/TEXI0001 头部与图像容器TEXB 块)
// GIF 帧信息TEXS 块)由 ExtractPipeline::ReadGifFrameInfo 单独读取
class TexDecoder {
public:
Result<Tex> Decode(StreamReader& reader);
};
} // namespace PKG
#endif // PKG_TEX_DECODER_H

View File

@ -0,0 +1,223 @@
#include "ImageEncoder.h"
#include "IO/StreamWriter.h"
#include "Decoder/DxtDecoder.h"
#include "gif.h"
#include <memory>
#include <unordered_map>
extern "C" unsigned char* stbi_write_png_to_mem(const unsigned char* pixels, int stride_bytes,
int x, int y, int n, int* out_len);
namespace PKG
{
// ─── RAII 工具 ───────────────────────────────────────────────────
// 管理 stbi_write_png_to_mem 返回的 C 缓冲,确保任何退出路径都释放
struct StbiPngBufferDeleter {
void operator()(uint8_t* p) const { free(p); }
};
using StbiPngBuffer = std::unique_ptr<uint8_t, StbiPngBufferDeleter>;
// GifWriter 的 RAII 守卫:构造时调用 GifBegin析构时调用 GifEnd
// 保证即使编码过程中抛异常或提前 return文件句柄和 oldImage 都被释放
class GifWriterGuard {
public:
GifWriterGuard() = default;
// 调用 GifBegin 初始化;成功返回 true失败返回 false
bool Begin(const char* filename, uint32_t width, uint32_t height, uint32_t delay)
{
return GifBegin(&m_Writer, filename, width, height, delay);
}
GifWriter* Get() { return &m_Writer; }
~GifWriterGuard()
{
if (m_Writer.f) // f 非空表示 GifBegin 成功,需要清理
GifEnd(&m_Writer);
}
GifWriterGuard(const GifWriterGuard&) = delete;
GifWriterGuard& operator=(const GifWriterGuard&) = delete;
private:
GifWriter m_Writer{};
};
// ─── 格式 → 扩展名映射表 ─────────────────────────────────────────
// 使用静态 map 替代冗长的 switch-case便于维护和扩展
static const std::unordered_map<MipmapFormat, std::string>& FormatExtensionMap()
{
static const std::unordered_map<MipmapFormat, std::string> map = {
{MipmapFormat::ImageBMP, "bmp"},
{MipmapFormat::ImageICO, "ico"},
{MipmapFormat::ImageJPEG, "jpg"},
{MipmapFormat::ImageJNG, "jng"},
{MipmapFormat::ImageKOALA, "koa"},
{MipmapFormat::ImageLBM, "lbm"},
{MipmapFormat::ImageIFF, "iff"},
{MipmapFormat::ImageMNG, "mng"},
{MipmapFormat::ImagePBM, "pbm"},
{MipmapFormat::ImagePBMRAW, "pbm"},
{MipmapFormat::ImagePCD, "pcd"},
{MipmapFormat::ImagePCX, "pcx"},
{MipmapFormat::ImagePGM, "pgm"},
{MipmapFormat::ImagePGMRAW, "pgm"},
{MipmapFormat::ImagePNG, "png"},
{MipmapFormat::ImagePPM, "ppm"},
{MipmapFormat::ImagePPMRAW, "ppm"},
{MipmapFormat::ImageRAS, "ras"},
{MipmapFormat::ImageTARGA, "tga"},
{MipmapFormat::ImageTIFF, "tif"},
{MipmapFormat::ImageWBMP, "wbmp"},
{MipmapFormat::ImagePSD, "psd"},
{MipmapFormat::ImageCUT, "cut"},
{MipmapFormat::ImageXBM, "xbm"},
{MipmapFormat::ImageXPM, "xpm"},
{MipmapFormat::ImageDDS, "dds"},
{MipmapFormat::ImageGIF, "gif"},
{MipmapFormat::ImageHDR, "hdr"},
{MipmapFormat::ImageFAXG3, "g3"},
{MipmapFormat::ImageSGI, "sgi"},
{MipmapFormat::ImageEXR, "exr"},
{MipmapFormat::ImageJ2K, "j2k"},
{MipmapFormat::ImageJP2, "jp2"},
{MipmapFormat::ImagePFM, "pfm"},
{MipmapFormat::ImagePICT, "pict"},
{MipmapFormat::ImageRAW, "raw"},
{MipmapFormat::VideoMp4, "mp4"},
};
return map;
}
std::string GetFileExtension(MipmapFormat format)
{
const auto& map = FormatExtensionMap();
auto it = map.find(format);
return it != map.end() ? it->second : "unknown";
}
// ─── PNG 编码 ────────────────────────────────────────────────────
// 将原始像素数据R8/RG88/RGBA8888编码为 PNG 文件
// 使用 stb_image_write 的内存编码接口,再一次性写入磁盘
Result<void> ImageEncoder::EncodePng(const TexMipMap& mipmap, const std::filesystem::path& outPath)
{
// 根据像素格式确定通道数
int channel = 4;
switch (mipmap.Format)
{
case MipmapFormat::R8: channel = 1; break;
case MipmapFormat::RG88: channel = 2; break;
case MipmapFormat::RGBA8888: channel = 4; break;
default:
return Fail(ErrorCode::UnsupportedFormat, "Unsupported format for PNG encoding");
}
// 编码到内存缓冲StbiPngBuffer 的 RAII 析构会自动 free无需手动释放
int len = 0;
StbiPngBuffer data(stbi_write_png_to_mem(mipmap.Data.data(), mipmap.Width * channel,
mipmap.Width, mipmap.Height, channel, &len));
if (!data)
return Fail(ErrorCode::EncodeFailed, "Failed to encode PNG");
// 写入磁盘
StreamWriter writer(outPath, std::ios::binary);
return writer.WriteBytes(reinterpret_cast<const char*>(data.get()), len);
}
// ─── GIF 编码 ────────────────────────────────────────────────────
// 逐帧处理策略(降低峰值内存):
// 1. 遍历每张源图像(图集),仅对当前图像执行 DXT 解压
// 2. 从解压后的图集中按帧信息裁剪出每一帧
// 3. 写入 GIF 后立即释放该图集的像素数据
// 这样任意时刻内存中最多只保留一张图集的 RGBA 数据
Result<void> ImageEncoder::EncodeGif(Tex& tex, const std::filesystem::path& outPath)
{
// GifWriterGuard 在析构时自动调用 GifEnd保证异常/提前返回时也释放资源
GifWriterGuard writer;
// GIF 帧延时单位1/100 秒
uint32_t delay = static_cast<uint32_t>(tex.FrameInfoContainer.Frames[0].Frametime * 100);
if (!writer.Begin(outPath.string().c_str(),
static_cast<uint32_t>(tex.FrameInfoContainer.Frames[0].Width),
static_cast<uint32_t>(tex.FrameInfoContainer.Frames[0].Height), delay))
return Fail(ErrorCode::EncodeFailed, "Failed to create GIF file");
int frameIndex = 0; // 已写入的全局帧序号
int imageIndex = 1; // 当前图集序号(用于计算帧边界)
for (auto& image : tex.ImageContainer.Images)
{
if (image.Mipmaps.empty())
continue;
auto& mipmap = image.Mipmaps[0];
// 就地 DXT 解压(仅当前帧所在的图集),避免一次性解压所有图集
switch (mipmap.Format)
{
case MipmapFormat::CompressedDXT5:
DxtDecoder::DecompressImage(mipmap.Width, mipmap.Height, mipmap.Data, DXTFlags::DXT5);
mipmap.Format = MipmapFormat::RGBA8888;
break;
case MipmapFormat::CompressedDXT3:
DxtDecoder::DecompressImage(mipmap.Width, mipmap.Height, mipmap.Data, DXTFlags::DXT3);
mipmap.Format = MipmapFormat::RGBA8888;
break;
case MipmapFormat::CompressedDXT1:
DxtDecoder::DecompressImage(mipmap.Width, mipmap.Height, mipmap.Data, DXTFlags::DXT1);
mipmap.Format = MipmapFormat::RGBA8888;
break;
default:
break;
}
// 当前图集可切分出的帧数(图集尺寸 / 单帧尺寸)
int singleImageFrameCount = (mipmap.Width / tex.FrameInfoContainer.GifWidth)
* (mipmap.Height / tex.FrameInfoContainer.GifHeight);
// 从图集中逐帧裁剪并写入 GIF
for (; frameIndex < singleImageFrameCount * imageIndex
&& frameIndex < static_cast<int>(tex.FrameInfoContainer.Frames.size());
frameIndex++)
{
const auto& frameInfo = tex.FrameInfoContainer.Frames[frameIndex];
std::vector<uint8_t> frameImage;
frameImage.reserve(static_cast<size_t>(frameInfo.Width) * frameInfo.Height * 4);
// 按行裁剪:从图集的 (PosX, PosY) 处取 Width×Height 的像素块
for (int row = 0; row < tex.FrameInfoContainer.GifHeight; row++)
{
auto lineStart = mipmap.Data.begin()
+ mipmap.Width * 4 * (static_cast<int>(frameInfo.PosY) + row)
+ static_cast<int>(frameInfo.PosX) * 4;
auto lineEnd = lineStart + static_cast<int>(frameInfo.Width) * 4;
frameImage.insert(frameImage.end(), lineStart, lineEnd);
}
GifWriteFrame(writer.Get(), frameImage.data(),
static_cast<uint32_t>(frameInfo.Width),
static_cast<uint32_t>(frameInfo.Height), delay);
}
imageIndex++;
// 释放已编码图集的像素数据,降低峰值内存
std::vector<uint8_t>().swap(mipmap.Data);
}
// writer 析构时自动调用 GifEnd
return Ok();
}
// ─── 原始数据写出 ────────────────────────────────────────────────
// 直接将 mipmap 数据写入文件,不做任何转码
// 用于 MP4 视频纹理或已编码图像PNG/JPEG 等)
Result<void> ImageEncoder::EncodeRaw(const TexMipMap& mipmap, const std::filesystem::path& outPath)
{
StreamWriter writer(outPath, std::ios::binary);
return writer.WriteBytes(reinterpret_cast<const char*>(mipmap.Data.data()),
static_cast<uint32_t>(mipmap.Data.size()));
}
}

View File

@ -0,0 +1,24 @@
#ifndef PKG_IMAGE_ENCODER_H
#define PKG_IMAGE_ENCODER_H
#include "Core/Types.h"
#include "Core/Result.h"
namespace PKG
{
// 图像编码器:将 TEX mipmap 数据编码为 PNG/GIF 或直接写出原始数据
class ImageEncoder
{
public:
// PNG 编码:支持 R8/RG88/RGBA8888 像素格式
Result<void> EncodePng(const TexMipMap& mipmap, const std::filesystem::path& outPath);
// GIF 编码:逐帧 DXT 解压 + 裁剪,降低峰值内存
Result<void> EncodeGif(Tex& tex, const std::filesystem::path& outPath);
// 原始数据写出:用于 MP4 视频纹理或已编码图像
Result<void> EncodeRaw(const TexMipMap& mipmap, const std::filesystem::path& outPath);
};
}
#endif // PKG_IMAGE_ENCODER_H

125
src/IO/StreamReader.cpp Normal file
View File

@ -0,0 +1,125 @@
#include "StreamReader.h"
namespace PKG {
StreamReader::StreamReader(const std::filesystem::path& fileName)
: m_FilePath(fileName.string()) {
m_File.open(m_FilePath, std::ios::in | std::ios::binary);
if (m_File.is_open())
m_Stream = &m_File;
}
StreamReader::StreamReader(std::string data)
: m_MemoryStream(std::move(data), std::ios::in | std::ios::binary)
, m_Stream(&m_MemoryStream)
, m_FilePath("<memory>") {
}
StreamReader::~StreamReader() {
if (m_File.is_open())
m_File.close();
}
Result<int32_t> StreamReader::ReadInt32() {
int32_t result = 0;
m_Stream->read(reinterpret_cast<char*>(&result), sizeof(int32_t));
if (m_Stream->fail())
return Fail<int32_t>(ErrorCode::ReadFailed, "Failed to read int32");
return Ok(result);
}
Result<uint32_t> StreamReader::ReadUInt32() {
uint32_t result = 0;
m_Stream->read(reinterpret_cast<char*>(&result), sizeof(uint32_t));
if (m_Stream->fail())
return Fail<uint32_t>(ErrorCode::ReadFailed, "Failed to read uint32");
return Ok(result);
}
Result<float> StreamReader::ReadSingle() {
float result = 0;
m_Stream->read(reinterpret_cast<char*>(&result), sizeof(float));
if (m_Stream->fail())
return Fail<float>(ErrorCode::ReadFailed, "Failed to read float");
return Ok(result);
}
Result<char> StreamReader::ReadChar() {
char result = 0;
m_Stream->read(&result, sizeof(char));
if (m_Stream->fail())
return Fail<char>(ErrorCode::ReadFailed, "Failed to read char");
return Ok(result);
}
Result<std::string> StreamReader::ReadString(uint32_t length) {
std::vector<uint8_t> result(length);
m_Stream->read(reinterpret_cast<char*>(result.data()), length);
if (m_Stream->fail())
return Fail<std::string>(ErrorCode::ReadFailed, "Failed to read string");
// 按 UTF-8 路径解码(处理多字节字符文件名)
return Ok(std::filesystem::u8path(
std::string(reinterpret_cast<const char*>(result.data()), length)).string());
}
Result<std::string> StreamReader::ReadNString(int32_t maxLength) {
std::vector<uint8_t> result;
int count = 0;
while (maxLength == -1 || count < maxLength) {
auto readRes = ReadChar();
if (!readRes)
return Fail<std::string>(readRes.error.code, readRes.error.message);
char chr = *readRes;
if (chr == '\0')
break;
result.push_back(chr);
count++;
}
return Ok(std::string(reinterpret_cast<const char*>(result.data()), result.size()));
}
Result<std::string> StreamReader::ReadStringFileData(uint32_t length) {
std::vector<uint8_t> result(length);
m_Stream->read(reinterpret_cast<char*>(result.data()), length);
if (m_Stream->fail())
return Fail<std::string>(ErrorCode::ReadFailed, "Failed to read file data");
return Ok(std::string(reinterpret_cast<const char*>(result.data()), length));
}
Result<void> StreamReader::ReadData(std::string& data, uint32_t length) {
data.resize(length);
m_Stream->read(data.data(), length);
if (m_Stream->fail())
return Fail(ErrorCode::ReadFailed, "Failed to read data to string");
return Ok();
}
Result<void> StreamReader::ReadData(std::vector<uint8_t>& data, uint32_t length) {
data.resize(length);
m_Stream->read(reinterpret_cast<char*>(data.data()), length);
if (m_Stream->fail())
return Fail(ErrorCode::ReadFailed, "Failed to read data to vector");
return Ok();
}
void StreamReader::seekg(pos_type pos) {
m_Stream->seekg(pos);
}
pos_type StreamReader::tellg() {
return m_Stream->tellg();
}
std::string StreamReader::GetFilePath() const {
return m_FilePath;
}
std::string StreamReader::GetFileName() const {
return m_FilePath.substr(m_FilePath.find_last_of("\\/") + 1);
}
} // namespace PKG

57
src/IO/StreamReader.h Normal file
View File

@ -0,0 +1,57 @@
#ifndef PKG_STREAM_READER_H
#define PKG_STREAM_READER_H
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include "Core/Result.h"
namespace PKG {
using pos_type = long long;
// 流式读取器:支持文件模式与内存模式
// - 文件模式:从磁盘文件读取
// - 内存模式:从 std::string 缓冲区读取(避免中间文件 I/O
// 两种模式通过 m_Stream 指针统一操作
class StreamReader {
public:
explicit StreamReader(const std::filesystem::path& fileName); // 文件模式
explicit StreamReader(std::string data); // 内存模式move 语义,零拷贝)
~StreamReader();
// 基本类型读取(小端序)
Result<int32_t> ReadInt32();
Result<uint32_t> ReadUInt32();
Result<float> ReadSingle();
Result<char> ReadChar();
// 字符串读取
Result<std::string> ReadString(uint32_t length); // 读取 length 字节并按 UTF-8 路径解码
Result<std::string> ReadNString(int32_t maxLength = -1); // 读取 NUL 结尾字符串
Result<std::string> ReadStringFileData(uint32_t length); // 读取 length 字节为原始字符串
// 数据块读取
Result<void> ReadData(std::string& data, uint32_t length);
Result<void> ReadData(std::vector<uint8_t>& data, uint32_t length);
void seekg(pos_type pos);
pos_type tellg();
bool IsOpen() const { return m_Stream != nullptr; }
std::string GetFilePath() const;
std::string GetFileName() const;
private:
std::ifstream m_File; // 文件模式底层流
std::istringstream m_MemoryStream; // 内存模式底层流
std::istream* m_Stream = nullptr; // 统一操作指针(指向 m_File 或 m_MemoryStream
std::string m_FilePath;
};
} // namespace PKG
#endif // PKG_STREAM_READER_H

46
src/IO/StreamWriter.cpp Normal file
View File

@ -0,0 +1,46 @@
#include "StreamWriter.h"
namespace PKG {
StreamWriter::StreamWriter(const std::filesystem::path& fileName, std::ios_base::openmode optMode)
: m_FilePath(fileName.string()) {
// 自动创建父目录多线程安全create_directories 对已存在目录是 no-op
const std::filesystem::path parent = fileName.parent_path();
if (!parent.empty() && !std::filesystem::exists(parent))
std::filesystem::create_directories(parent);
m_File.open(m_FilePath, optMode);
}
StreamWriter::~StreamWriter() {
close();
}
Result<void> StreamWriter::WriteBytes(const char* data, uint32_t size) {
m_File.write(data, size);
if (m_File.fail())
return Fail(ErrorCode::WriteFailed, "Failed to write bytes");
return Ok();
}
Result<void> StreamWriter::WriteString(const std::string& str) {
m_File.write(str.c_str(), str.size());
if (m_File.fail())
return Fail(ErrorCode::WriteFailed, "Failed to write string");
return Ok();
}
void StreamWriter::close() {
if (m_File.is_open())
m_File.close();
}
std::string StreamWriter::GetFilePath() const {
return m_FilePath;
}
std::string StreamWriter::GetFileName() const {
return m_FilePath.substr(m_FilePath.find_last_of("\\/") + 1);
}
} // namespace PKG

34
src/IO/StreamWriter.h Normal file
View File

@ -0,0 +1,34 @@
#ifndef PKG_STREAM_WRITER_H
#define PKG_STREAM_WRITER_H
#include <filesystem>
#include <fstream>
#include <string>
#include "Core/Result.h"
namespace PKG {
// 文件写入器:构造时自动创建父目录
class StreamWriter {
public:
StreamWriter() = delete;
explicit StreamWriter(const std::filesystem::path& fileName,
std::ios_base::openmode optMode = std::ios::out);
~StreamWriter();
Result<void> WriteBytes(const char* data, uint32_t size);
Result<void> WriteString(const std::string& str);
void close();
std::string GetFilePath() const;
std::string GetFileName() const;
private:
std::ofstream m_File;
std::string m_FilePath;
};
} // namespace PKG
#endif // PKG_STREAM_WRITER_H

View File

@ -0,0 +1,528 @@
#include "Core/Logger.h"
#include "Core/ProgressBar.h"
#include "Decoder/DxtDecoder.h"
#include "Decoder/PkgExtractor.h"
#include "Decoder/TexDecoder.h"
#include "Encoder/ImageEncoder.h"
#include "ExtractPipeline.h"
#include "IO/StreamReader.h"
#include "IO/StreamWriter.h"
#include "TaskScheduler.h"
#include <algorithm>
#include <chrono>
namespace PKG {
// ─── RAII 计时器 ─────────────────────────────────────────────────
// 构造时记录起始时间,析构时打印耗时(自动选择 ms/s 单位)
class ScopedTimer {
public:
ScopedTimer() : m_Start(std::chrono::steady_clock::now()) {}
~ScopedTimer() {
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - m_Start)
.count();
if (ms < 1000)
Logger::Instance().Info("Time: " + std::to_string(ms) + " ms");
else
Logger::Instance().Info("Time: " + std::to_string(ms / 1000.0) + " s");
}
private:
std::chrono::steady_clock::time_point m_Start;
};
// ─── 辅助函数 ─────────────────────────────────────────────────────
// 就地 DXT 解压并将格式标记为 RGBA8888
// 返回是否实际执行了解压
static bool DecompressDxtInPlace(TexMipMap &mipmap) {
switch (mipmap.Format) {
case MipmapFormat::CompressedDXT5:
DxtDecoder::DecompressImage(mipmap.Width, mipmap.Height, mipmap.Data, DXTFlags::DXT5);
mipmap.Format = MipmapFormat::RGBA8888;
return true;
case MipmapFormat::CompressedDXT3:
DxtDecoder::DecompressImage(mipmap.Width, mipmap.Height, mipmap.Data, DXTFlags::DXT3);
mipmap.Format = MipmapFormat::RGBA8888;
return true;
case MipmapFormat::CompressedDXT1:
DxtDecoder::DecompressImage(mipmap.Width, mipmap.Height, mipmap.Data, DXTFlags::DXT1);
mipmap.Format = MipmapFormat::RGBA8888;
return true;
default:
return false;
}
}
// 通过文件头魔数检测图像格式,返回对应扩展名(不含点)
// 用于修正未识别格式的输出扩展名,避免产生 .unknown 文件
static std::string DetectExtensionByMagic(const std::vector<uint8_t> &data) {
if (data.size() >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47)
return "png"; // \x89PNG
if (data.size() >= 3 && data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF)
return "jpg"; // \xFF\xD8\xFF
if (data.size() >= 6 && data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46)
return "gif"; // GIF8
if (data.size() >= 2 && data[0] == 0x42 && data[1] == 0x4D)
return "bmp"; // BM
if (data.size() >= 12 && data[0] == 0x52 && data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x46 && data[8] == 0x57 && data[9] == 0x45 && data[10] == 0x42 && data[11] == 0x50)
return "webp"; // RIFF....WEBP
return {};
}
// 判断是否为已编码图像扩展名(直接拷贝无需转码)
static bool IsRawImageFile(const std::string &ext) {
return ext == ".gif" || ext == ".jpg" || ext == ".png" || ext == ".jpeg" || ext == ".webp";
}
// ─── ExtractPipeline 公开接口 ─────────────────────────────────────
ExtractPipeline::ExtractPipeline() = default;
ExtractPipeline::ExtractPipeline(const ExtractConfig &config)
: m_Config(config) {}
Result<void> ExtractPipeline::Run(const std::filesystem::path &inputPath,
const std::filesystem::path &outDir,
bool showProgress,
ExtractStats *stats) {
ScopedTimer timer;
// 记录起始时间和输入文件大小
if (stats) {
stats->startTime = std::chrono::steady_clock::now();
std::error_code ec;
uint64_t sz = std::filesystem::file_size(inputPath, ec);
stats->inputFileSize = ec ? 0 : sz; // 文件不存在时置 0避免垃圾值
}
// 输出路径outDir/<输入文件名(去扩展名)>/
// 这样多次提取不同 pkg 时,输出按文件名分目录存放,互不混淆
std::filesystem::path subDir = outDir / inputPath.stem();
std::string ext = inputPath.extension().string();
Result<void> res;
if (ext == ".pkg" || ext == ".mpkg")
res = RunPkg(inputPath, subDir, showProgress, stats);
else if (ext == ".tex")
res = RunTex(inputPath, subDir, stats);
else
res = Fail(ErrorCode::UnsupportedFormat, "Unsupported file extension: " + ext);
// 统一设置结束时间,确保早期失败时 stats 时间也有效
if (stats)
stats->endTime = std::chrono::steady_clock::now();
return res;
}
// ─── PKG/MPKG 包提取 ──────────────────────────────────────────────
Result<void> ExtractPipeline::RunPkg(const std::filesystem::path &inputPath,
const std::filesystem::path &outDir,
bool showProgress,
ExtractStats *stats) {
StreamReader reader(inputPath);
if (!reader.IsOpen())
return Fail(ErrorCode::FileOpen, "Failed to open file: " + inputPath.string());
// 解析包索引
PkgExtractor extractor;
auto entriesRes = extractor.ParseIndex(reader);
if (!entriesRes)
return Fail(entriesRes.error.code, entriesRes.error.message);
uint32_t offsetPosition = static_cast<uint32_t>(reader.tellg());
// 预创建所有目录,避免多线程并发创建冲突
for (const auto &entry : *entriesRes) {
auto parentDir = (outDir / entry.FullPath).parent_path();
if (!parentDir.empty() && !std::filesystem::exists(parentDir))
std::filesystem::create_directories(parentDir);
}
// 线程数上限:不超过条目数,避免创建无用线程
uint32_t threadCount = m_Config.threadCount;
if (threadCount == 0)
threadCount = std::thread::hardware_concurrency();
threadCount = std::min(threadCount, static_cast<uint32_t>(entriesRes->size()));
if (threadCount == 0)
threadCount = 1;
TaskScheduler scheduler(threadCount);
Logger::Instance().Info("Using " + std::to_string(scheduler.ThreadCount()) + " threads");
// 限制并发 .tex 处理数:.tex 解码 + DXT 解压 + PNG 编码内存开销大,
// 全部线程同时处理大纹理会导致峰值内存过高
Semaphore texSem(4);
// 进度条在解析索引后创建(此时已知总条目数)
// 注册到 Logger日志会通过进度条协调打印
ProgressBar progress(showProgress ? entriesRes->size() : 0);
if (showProgress)
Logger::Instance().SetProgressBar(&progress);
if (stats)
stats->totalEntries.store(entriesRes->size(), std::memory_order_relaxed);
// 提交所有任务到线程池
std::vector<std::future<Result<void>>> futures;
futures.reserve(entriesRes->size());
for (const auto &entry : *entriesRes) {
auto future = scheduler.Submit(
[this, entry, pkgPath = inputPath, offsetPosition, outDir, &texSem, &progress, stats]() -> Result<void> {
auto res = ProcessEntry(entry, pkgPath, offsetPosition, outDir, texSem, stats);
progress.Increment();
if (!res && stats)
stats->AddFailure();
return res;
});
futures.push_back(std::move(future));
}
scheduler.WaitAll();
Logger::Instance().SetProgressBar(nullptr);
if (stats)
stats->endTime = std::chrono::steady_clock::now();
// 收集错误信息
std::vector<std::string> errors;
for (size_t i = 0; i < futures.size(); i++) {
auto res = futures[i].get();
if (!res)
errors.push_back((*entriesRes)[i].FullPath.string() + ": " + res.error.message);
}
if (!errors.empty()) {
std::string msg = "Completed with " + std::to_string(errors.size()) + " errors:\n";
for (const auto &e : errors)
msg += " " + e + "\n";
return Fail(ErrorCode::ReadFailed, msg);
}
return Ok();
}
// ─── 单个 TEX 文件提取 ────────────────────────────────────────────
Result<void> ExtractPipeline::RunTex(const std::filesystem::path &inputPath,
const std::filesystem::path &outDir,
ExtractStats *stats) {
StreamReader reader(inputPath);
if (!reader.IsOpen())
return Fail(ErrorCode::FileOpen, "Failed to open file: " + inputPath.string());
TexDecoder texDecoder;
auto texRes = texDecoder.Decode(reader);
if (!texRes)
return Fail(texRes.error.code, texRes.error.message);
auto &tex = *texRes;
// GIF 需要额外读取帧信息表(位于 TEX 数据末尾)
if (tex.IsGif) {
auto frameRes = ReadGifFrameInfo(tex, reader);
if (!frameRes)
return frameRes;
}
if (stats)
stats->totalEntries.store(1, std::memory_order_relaxed);
// 输出路径outDir/<输入文件名(去扩展名)>/<输入文件名(去扩展名)>.<新扩展名>
// 确保输出目录存在
std::error_code ec;
std::filesystem::create_directories(outDir, ec);
std::filesystem::path outPath = outDir / inputPath.stem();
auto res = EncodeTex(tex, outPath, stats);
if (stats)
stats->endTime = std::chrono::steady_clock::now();
if (!res && stats)
stats->AddFailure();
return res;
}
// ─── GIF 帧信息读取 ───────────────────────────────────────────────
// TEXS 块位于 TEX 文件末尾,包含 GIF 动画的每一帧位置与时长
// 支持 TEXS0001整数坐标和 TEXS0003浮点坐标 + 画布尺寸)两个版本
Result<void> ExtractPipeline::ReadGifFrameInfo(Tex &tex, StreamReader &reader) {
auto magicRes = reader.ReadNString(16);
if (!magicRes)
return Fail(magicRes.error.code, magicRes.error.message);
tex.FrameInfoContainer.Magic = *magicRes;
auto frameCountRes = reader.ReadInt32();
if (!frameCountRes)
return Fail(frameCountRes.error.code, frameCountRes.error.message);
// TEXS0003 额外包含 GIF 画布尺寸
if (tex.FrameInfoContainer.Magic == "TEXS0003") {
auto widthRes = reader.ReadInt32();
if (!widthRes)
return Fail(widthRes.error.code, widthRes.error.message);
tex.FrameInfoContainer.GifWidth = *widthRes;
auto heightRes = reader.ReadInt32();
if (!heightRes)
return Fail(heightRes.error.code, heightRes.error.message);
tex.FrameInfoContainer.GifHeight = *heightRes;
}
// 逐帧读取
for (int i = 0; i < *frameCountRes; i++) {
TexFrameInfo frameInfo{};
auto imageIdRes = reader.ReadInt32();
if (!imageIdRes)
return Fail(imageIdRes.error.code, imageIdRes.error.message);
frameInfo.ImageId = *imageIdRes;
auto frametimeRes = reader.ReadSingle();
if (!frametimeRes)
return Fail(frametimeRes.error.code, frametimeRes.error.message);
frameInfo.Frametime = *frametimeRes;
// TEXS0001: 坐标为整数TEXS0003: 坐标为浮点
if (tex.FrameInfoContainer.Magic == "TEXS0001") {
auto posXRes = reader.ReadInt32();
if (!posXRes)
return Fail(posXRes.error.code, posXRes.error.message);
frameInfo.PosX = static_cast<float>(*posXRes);
auto posYRes = reader.ReadInt32();
if (!posYRes)
return Fail(posYRes.error.code, posYRes.error.message);
frameInfo.PosY = static_cast<float>(*posYRes);
auto widthRes = reader.ReadInt32();
if (!widthRes)
return Fail(widthRes.error.code, widthRes.error.message);
frameInfo.Width = static_cast<float>(*widthRes);
auto widthYRes = reader.ReadInt32();
if (!widthYRes)
return Fail(widthYRes.error.code, widthYRes.error.message);
frameInfo.WidthY = static_cast<float>(*widthYRes);
auto heightXRes = reader.ReadInt32();
if (!heightXRes)
return Fail(heightXRes.error.code, heightXRes.error.message);
frameInfo.HeightX = static_cast<float>(*heightXRes);
auto heightRes = reader.ReadInt32();
if (!heightRes)
return Fail(heightRes.error.code, heightRes.error.message);
frameInfo.Height = static_cast<float>(*heightRes);
} else {
auto posXRes = reader.ReadSingle();
if (!posXRes)
return Fail(posXRes.error.code, posXRes.error.message);
frameInfo.PosX = *posXRes;
auto posYRes = reader.ReadSingle();
if (!posYRes)
return Fail(posYRes.error.code, posYRes.error.message);
frameInfo.PosY = *posYRes;
auto widthRes = reader.ReadSingle();
if (!widthRes)
return Fail(widthRes.error.code, widthRes.error.message);
frameInfo.Width = *widthRes;
auto widthYRes = reader.ReadSingle();
if (!widthYRes)
return Fail(widthYRes.error.code, widthYRes.error.message);
frameInfo.WidthY = *widthYRes;
auto heightXRes = reader.ReadSingle();
if (!heightXRes)
return Fail(heightXRes.error.code, heightXRes.error.message);
frameInfo.HeightX = *heightXRes;
auto heightRes = reader.ReadSingle();
if (!heightRes)
return Fail(heightRes.error.code, heightRes.error.message);
frameInfo.Height = *heightRes;
}
tex.FrameInfoContainer.Frames.push_back(frameInfo);
}
// 兜底:若画布尺寸未设置,取首帧尺寸
if (tex.FrameInfoContainer.GifWidth == 0 || tex.FrameInfoContainer.GifHeight == 0) {
tex.FrameInfoContainer.GifWidth = static_cast<int>(tex.FrameInfoContainer.Frames[0].Width);
tex.FrameInfoContainer.GifHeight = static_cast<int>(tex.FrameInfoContainer.Frames[0].Height);
}
return Ok();
}
// ─── TEX 编码输出 ────────────────────────────────────────────────
// 根据纹理类型选择编码方式:
// - GIF 动画 → EncodeGif逐帧处理
// - 视频纹理 → 原始数据写出MP4
// - DXT 压缩 → 解压后 PNG 编码
// - 已编码图像 → 魔数检测修正扩展名后直接写出
Result<void> ExtractPipeline::EncodeTex(Tex &tex, const std::filesystem::path &outPath, ExtractStats *stats) {
if (tex.ImageContainer.Images.empty())
return Ok();
// GIF 动画:交给 EncodeGif 逐帧处理
if (tex.IsGif) {
std::filesystem::path gifOutPath = outPath;
gifOutPath.replace_extension("gif");
Logger::Instance().Info("Convert: " + gifOutPath.string());
ImageEncoder encoder;
auto res = encoder.EncodeGif(tex, gifOutPath);
if (res && stats)
stats->AddOutput("gif", std::filesystem::file_size(gifOutPath));
return res;
}
auto &sourceMipmap = tex.ImageContainer.Images[0].Mipmaps[0];
MipmapFormat format = tex.IsVideoTexture ? MipmapFormat::VideoMp4 : sourceMipmap.Format;
// 视频纹理:校验 MP4 头并直接写出
if (tex.IsVideoTexture) {
if (sourceMipmap.Data.size() >= 12) {
std::string mp4Magic = std::string(reinterpret_cast<const char *>(&sourceMipmap.Data[4]), 8);
if (mp4Magic != "ftypisom" && mp4Magic != "ftypmsnv" && mp4Magic != "ftypmp42")
Logger::Instance().Error("Warning: Bad MP4 magic header");
} else {
Logger::Instance().Error("Warning: MP4 data too short");
}
std::filesystem::path imgOutPath = outPath;
imgOutPath.replace_extension(GetFileExtension(format));
Logger::Instance().Info("Convert: " + imgOutPath.string());
ImageEncoder encoder;
auto res = encoder.EncodeRaw(sourceMipmap, imgOutPath);
if (res && stats)
stats->AddOutput(imgOutPath.extension().string().substr(1),
std::filesystem::file_size(imgOutPath));
return res;
}
// 非 GIF、非视频尝试 DXT 解压
DecompressDxtInPlace(sourceMipmap);
// DXT 解压后为原始像素R8/RG88/RGBA8888编码为 PNG
if (static_cast<int>(sourceMipmap.Format) >= 1 && static_cast<int>(sourceMipmap.Format) <= 3) {
std::filesystem::path imgOutPath = outPath;
imgOutPath.replace_extension("png");
Logger::Instance().Info("Convert: " + imgOutPath.string());
ImageEncoder encoder;
auto res = encoder.EncodePng(sourceMipmap, imgOutPath);
if (res && stats)
stats->AddOutput("png", std::filesystem::file_size(imgOutPath));
return res;
}
// 原始数据可能是已编码图像PNG/JPEG/GIF/BMP/WEBP
// 通过魔数检测修正扩展名,避免输出 .unknown
std::filesystem::path imgOutPath = outPath;
std::string detectedExt = DetectExtensionByMagic(sourceMipmap.Data);
if (!detectedExt.empty())
imgOutPath.replace_extension(detectedExt);
else
imgOutPath.replace_extension(GetFileExtension(format));
Logger::Instance().Info("Convert: " + imgOutPath.string());
StreamWriter writer(imgOutPath, std::ios::binary);
auto res = writer.WriteBytes(reinterpret_cast<const char *>(sourceMipmap.Data.data()),
static_cast<uint32_t>(sourceMipmap.Data.size()));
if (res && stats)
stats->AddOutput(imgOutPath.extension().string().substr(1),
std::filesystem::file_size(imgOutPath));
return res;
}
// ─── 单条目处理(线程池任务)──────────────────────────────────────
// 每个任务独立打开 PKG 文件并 seek 到条目偏移,避免共享 StreamReader
Result<void> ExtractPipeline::ProcessEntry(const Entry &entry,
const std::filesystem::path &pkgPath,
uint32_t offsetPosition,
const std::filesystem::path &outDir,
Semaphore &texSem,
ExtractStats *stats) {
std::filesystem::path outPath = outDir / entry.FullPath;
StreamReader reader(pkgPath);
if (!reader.IsOpen())
return Fail(ErrorCode::FileOpen, "Failed to open pkg: " + pkgPath.string());
reader.seekg(entry.Offset + offsetPosition);
// .tex 文件:解码 + 编码(内存开销大,需信号量限流)
if (entry.Type == ".tex") {
SemaphoreGuard guard(texSem);
// 读取条目数据到内存,再用内存模式 reader 解码(零拷贝)
std::string texData;
auto readRes = reader.ReadData(texData, entry.Length);
if (!readRes)
return readRes;
StreamReader texReader(std::move(texData));
TexDecoder texDecoder;
auto texRes = texDecoder.Decode(texReader);
if (!texRes)
return Fail(texRes.error.code, texRes.error.message);
auto &tex = *texRes;
if (tex.IsGif) {
auto frameRes = ReadGifFrameInfo(tex, texReader);
if (!frameRes)
return frameRes;
}
return EncodeTex(tex, outPath, stats);
}
// 已编码图像文件:直接拷贝
if (IsRawImageFile(entry.Type)) {
Logger::Instance().Info("Extract: " + outPath.string());
std::string data;
auto readRes = reader.ReadData(data, entry.Length);
if (!readRes)
return readRes;
StreamWriter writer(outPath, std::ios::binary);
auto res = writer.WriteBytes(data.data(), static_cast<uint32_t>(data.size()));
if (res && stats)
stats->AddOutput(outPath.extension().string().substr(1),
std::filesystem::file_size(outPath));
return res;
}
// 其他文本文件(.json/.frag/.vert/.mdl 等)
Logger::Instance().Info("Extract: " + outPath.string());
auto dataRes = reader.ReadStringFileData(entry.Length);
if (!dataRes)
return Fail(dataRes.error.code, dataRes.error.message);
StreamWriter writer(outPath);
auto res = writer.WriteString(*dataRes);
if (res && stats) {
std::string ext = outPath.extension().string();
stats->AddOutput(ext.empty() ? "bin" : ext.substr(1),
std::filesystem::file_size(outPath));
}
return res;
}
} // namespace PKG

View File

@ -0,0 +1,64 @@
#ifndef PKG_EXTRACT_PIPELINE_H
#define PKG_EXTRACT_PIPELINE_H
#include "Core/Types.h"
#include "Core/Result.h"
#include "Core/Config.h"
#include "Core/ExtractStats.h"
#include "IO/StreamReader.h"
namespace PKG
{
class Semaphore; // 前向声明,定义在 TaskScheduler.h
// 提取流水线PKG/MPKG/TEX 文件解码与编码的协调器
// - PKG/MPKG: 多线程并行提取所有条目
// - TEX: 单文件解码后编码输出
class ExtractPipeline
{
public:
ExtractPipeline();
explicit ExtractPipeline(const ExtractConfig& config);
// 执行提取
// inputPath 输入文件路径(.pkg/.mpkg/.tex
// outDir 输出目录
// showProgress 为 true 时显示终端进度条
// stats 可选,传入则填充统计信息
Result<void> Run(const std::filesystem::path& inputPath,
const std::filesystem::path& outDir,
bool showProgress = false,
ExtractStats* stats = nullptr);
private:
// PKG/MPKG 包提取:解析索引 + 多线程处理所有条目
Result<void> RunPkg(const std::filesystem::path& inputPath,
const std::filesystem::path& outDir,
bool showProgress,
ExtractStats* stats);
// 单个 TEX 文件提取:解码 + 编码输出
Result<void> RunTex(const std::filesystem::path& inputPath,
const std::filesystem::path& outDir,
ExtractStats* stats);
// 处理单个条目(线程池任务,可并行执行)
// texSem 用于限制并发 .tex 处理数,降低峰值内存
Result<void> ProcessEntry(const Entry& entry,
const std::filesystem::path& pkgPath,
uint32_t offsetPosition,
const std::filesystem::path& outDir,
Semaphore& texSem,
ExtractStats* stats);
// 读取 GIF 帧信息TEXS 块,位于 TEX 数据末尾)
Result<void> ReadGifFrameInfo(Tex& tex, StreamReader& reader);
// TEX 编码输出:根据类型选择 GIF/PNG/MP4/原始数据
Result<void> EncodeTex(Tex& tex, const std::filesystem::path& outPath, ExtractStats* stats);
ExtractConfig m_Config;
};
}
#endif // PKG_EXTRACT_PIPELINE_H

View File

@ -0,0 +1,109 @@
#include "TaskScheduler.h"
namespace PKG
{
// ─── 线程池构造 ─────────────────────────────────────────────────
// 创建 threadCount 个工作线程,每个线程循环从任务队列取任务执行
TaskScheduler::TaskScheduler(uint32_t threadCount)
{
if (threadCount == 0)
threadCount = std::thread::hardware_concurrency();
if (threadCount == 0)
threadCount = 4; // hardware_concurrency 失败时的兜底值
for (uint32_t i = 0; i < threadCount; i++)
{
m_Workers.emplace_back([this]
{
while (true)
{
std::function<void()> task;
{
// 等待:有任务可取 或 收到停止信号
std::unique_lock<std::mutex> lock(m_QueueMutex);
m_Condition.wait(lock, [this] { return m_Stop || !m_Tasks.empty(); });
// 停止且队列空 → 退出线程
if (m_Stop && m_Tasks.empty())
return;
task = std::move(m_Tasks.front());
m_Tasks.pop();
}
task(); // 执行任务(已捕获异常,不会抛出)
// 任务完成:递减活跃计数,可能唤醒 WaitAll
{
std::lock_guard<std::mutex> lock(m_ActiveMutex);
if (m_ActiveTasks > 0)
m_ActiveTasks--;
if (m_ActiveTasks == 0)
m_ActiveCondition.notify_all();
}
}
});
}
}
TaskScheduler::~TaskScheduler()
{
{
std::lock_guard<std::mutex> lock(m_QueueMutex);
m_Stop = true;
}
m_Condition.notify_all(); // 唤醒所有等待的工作线程
for (auto& worker : m_Workers)
{
if (worker.joinable())
worker.join();
}
}
// ─── 提交任务 ───────────────────────────────────────────────────
// 将任务包装为 promise/future 模式,捕获所有异常转为错误结果
std::future<Result<void>> TaskScheduler::Submit(std::function<Result<void>()> task)
{
auto promise = std::make_shared<std::promise<Result<void>>>();
auto future = promise->get_future();
// 活跃任务数 +1必须在入队前完成防止 WaitAll 提前返回)
{
std::lock_guard<std::mutex> lock(m_ActiveMutex);
m_ActiveTasks++;
}
// 包装任务:执行并设置结果,捕获所有异常
std::function<void()> wrapped = [promise, task = std::move(task)]()
{
try
{
promise->set_value(task());
}
catch (const std::exception& e)
{
promise->set_value(Fail(ErrorCode::ReadFailed, std::string("Exception: ") + e.what()));
}
catch (...)
{
promise->set_value(Fail(ErrorCode::ReadFailed, "Unknown exception"));
}
};
{
std::lock_guard<std::mutex> lock(m_QueueMutex);
m_Tasks.emplace(std::move(wrapped));
}
m_Condition.notify_one(); // 唤醒一个等待的工作线程
return future;
}
// ─── 等待所有任务完成 ───────────────────────────────────────────
void TaskScheduler::WaitAll()
{
std::unique_lock<std::mutex> lock(m_ActiveMutex);
m_ActiveCondition.wait(lock, [this] { return m_ActiveTasks == 0; });
}
}

View File

@ -0,0 +1,94 @@
#ifndef PKG_TASK_SCHEDULER_H
#define PKG_TASK_SCHEDULER_H
#include <cstdint>
#include <functional>
#include <future>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>
#include <vector>
#include "Core/Result.h"
namespace PKG
{
// ─── 计数信号量 ─────────────────────────────────────────────────
// 用于限制并发重操作(如 .tex 解码)的数量,降低峰值内存
class Semaphore
{
public:
explicit Semaphore(int count) : m_Count(count) {}
// 获取一个许可(计数为 0 时阻塞等待)
void Acquire()
{
std::unique_lock<std::mutex> lock(m_Mutex);
m_Cond.wait(lock, [this] { return m_Count > 0; });
m_Count--;
}
// 释放一个许可(唤醒一个等待者)
void Release()
{
std::lock_guard<std::mutex> lock(m_Mutex);
m_Count++;
m_Cond.notify_one();
}
private:
std::mutex m_Mutex;
std::condition_variable m_Cond;
int m_Count;
};
// ─── RAII 信号量守卫 ────────────────────────────────────────────
// 构造时获取信号量,析构时自动释放(异常安全)
class SemaphoreGuard
{
public:
explicit SemaphoreGuard(Semaphore& sem) : m_Sem(sem) { m_Sem.Acquire(); }
~SemaphoreGuard() { m_Sem.Release(); }
SemaphoreGuard(const SemaphoreGuard&) = delete;
SemaphoreGuard& operator=(const SemaphoreGuard&) = delete;
private:
Semaphore& m_Sem;
};
// ─── 线程池 ─────────────────────────────────────────────────────
// 固定大小的线程池,提交任务返回 future 用于获取结果
// 支持 WaitAll() 等待所有已提交任务完成
class TaskScheduler
{
public:
// threadCount = 0 时自动使用 hardware_concurrency
explicit TaskScheduler(uint32_t threadCount = 0);
~TaskScheduler();
TaskScheduler(const TaskScheduler&) = delete;
TaskScheduler& operator=(const TaskScheduler&) = delete;
// 提交任务,返回 future 以获取结果或等待完成
std::future<Result<void>> Submit(std::function<Result<void>()> task);
// 阻塞等待所有已提交任务完成
void WaitAll();
uint32_t ThreadCount() const { return static_cast<uint32_t>(m_Workers.size()); }
private:
std::vector<std::thread> m_Workers; // 工作线程
std::queue<std::function<void()>> m_Tasks; // 待执行任务队列
std::mutex m_QueueMutex; // 保护 m_Tasks
std::condition_variable m_Condition; // 唤醒等待任务的工作线程
bool m_Stop = false; // 析构标志
uint32_t m_ActiveTasks = 0; // 正在执行的任务数
std::mutex m_ActiveMutex; // 保护 m_ActiveTasks
std::condition_variable m_ActiveCondition; // WaitAll 等待条件
};
}
#endif // PKG_TASK_SCHEDULER_H

20
src/expkg.h Normal file
View File

@ -0,0 +1,20 @@
#ifndef PKG_EXPKG_H
#define PKG_EXPKG_H
#include "Core/Types.h"
#include "Core/Result.h"
#include "Core/Config.h"
#include "Core/Logger.h"
#include "Core/ProcessInfo.h"
#include "Core/ExtractStats.h"
#include "IO/StreamReader.h"
#include "IO/StreamWriter.h"
#include "Decoder/PkgExtractor.h"
#include "Decoder/TexDecoder.h"
#include "Decoder/DxtDecoder.h"
#include "Decoder/ImageReader.h"
#include "Encoder/ImageEncoder.h"
#include "Pipeline/TaskScheduler.h"
#include "Pipeline/ExtractPipeline.h"
#endif // PKG_EXPKG_H

87
src/main.cpp Normal file
View File

@ -0,0 +1,87 @@
#include "expkg.h"
#include <iomanip>
#include <iostream>
int main(int argc, char **argv) {
if (argc < 2) {
std::cout << "Usage: expkg path/to/file.pkg|file.tex [output_dir]" << std::endl;
return 0;
}
std::filesystem::path inputPath = argv[1];
std::filesystem::path outDir = argc >= 3 ? argv[2] : "out";
PKG::ExtractPipeline pipeline;
PKG::ExtractStats stats;
auto result = pipeline.Run(inputPath, outDir, /*showProgress=*/true, &stats);
std::cout << std::endl; // 进度条换行后空一行
size_t total = stats.totalEntries.load();
size_t ok = stats.succeeded.load();
size_t fail = stats.failed.load();
uint64_t outBytes = stats.totalOutputBytes.load();
// 完全失败(没有任何条目被处理):只打印错误,不显示统计面板
if (!result && ok == 0 && fail == 0) {
std::cerr << "错误: " << result.error.message << std::endl;
return 1;
}
// 成功或部分成功:打印结果与统计
if (result)
std::cout << "提取完成" << std::endl;
else
std::cerr << "错误: " << result.error.message << std::endl;
double elapsed = stats.ElapsedSeconds();
std::cout << "──────────────────────────────" << std::endl;
// 文件总数
std::cout << "文件总数: " << ok << "";
if (fail > 0)
std::cout << " (失败 " << fail << " 个)";
if (total > ok + fail)
std::cout << " / 共 " << total << "";
std::cout << std::endl;
// 输出大小
std::cout << "输出大小: " << PKG::FormatBytes(outBytes) << std::endl;
// 耗时与吞吐(仅在时间有效时打印)
if (elapsed >= 0) {
std::cout << "耗时: " << std::fixed << std::setprecision(2) << elapsed << "";
if (elapsed > 0 && ok > 0)
std::cout << " (" << std::setprecision(1) << (ok / elapsed) << " 个/秒)";
std::cout << std::endl;
}
// 输入 → 输出对比(仅在输入大小有效时打印)
if (stats.inputFileSize > 0 && stats.inputFileSize < 1ULL << 40) {
std::cout << "输入文件: " << PKG::FormatBytes(stats.inputFileSize);
if (outBytes > 0) {
double ratio = static_cast<double>(outBytes) / stats.inputFileSize;
std::cout << " → 输出 " << PKG::FormatBytes(outBytes)
<< " (×" << std::setprecision(2) << ratio << ")";
}
std::cout << std::endl;
}
// 各格式分布
if (!stats.formatCounts.empty()) {
std::cout << "格式分布:" << std::endl;
for (const auto &[ext, count] : stats.formatCounts)
std::cout << " ." << ext << " " << count << "" << std::endl;
}
// 内存占用(峰值)
size_t peakRss = PKG::GetPeakRssBytes();
if (peakRss > 0)
std::cout << "内存占用: " << PKG::FormatBytes(peakRss) << " (峰值)" << std::endl;
std::cout << "──────────────────────────────" << std::endl;
return result ? 0 : 1;
}

View File

@ -1,7 +0,0 @@
set(TARGET demo)
file(GLOB_RECURSE SRC_SOURCE src/**.cpp)
add_executable(${TARGET} ${SRC_SOURCE})
target_link_libraries(${TARGET} expkg-static)

View File

@ -1,10 +0,0 @@
#include "EXPKG/EXPKG.h"
int main(const int argc, char** argv)
{
const CommandArgs args{ argc, argv };
PKG::EXPKG app{ args };
return 0;
}

24
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
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

Some files were not shown because too many files have changed in this diff Show More