Kelsidavis-WoWee/tools/blp_convert/main.cpp
Kelsi aa16a687c2 Replace MPQ runtime with loose file asset system
Extract assets from MPQ archives into organized loose files indexed by
manifest.json, enabling fully parallel reads without StormLib serialization.
Add asset_extract and blp_convert tools, PNG texture override support.
2026-02-12 20:32:14 -08:00

187 lines
5.8 KiB
C++

#include "pipeline/blp_loader.hpp"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
namespace fs = std::filesystem;
using wowee::pipeline::BLPImage;
using wowee::pipeline::BLPLoader;
static std::vector<uint8_t> readFileData(const std::string& path) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f.is_open()) return {};
auto sz = f.tellg();
if (sz <= 0) return {};
std::vector<uint8_t> data(static_cast<size_t>(sz));
f.seekg(0);
f.read(reinterpret_cast<char*>(data.data()), sz);
return data;
}
static bool convertBlpToPng(const std::string& blpPath) {
auto data = readFileData(blpPath);
if (data.empty()) {
std::cerr << "Failed to read: " << blpPath << "\n";
return false;
}
BLPImage img = BLPLoader::load(data);
if (!img.isValid()) {
std::cerr << "Failed to decode BLP: " << blpPath << "\n";
return false;
}
// Output path: same name with .png
fs::path out = fs::path(blpPath);
out.replace_extension(".png");
if (!stbi_write_png(out.string().c_str(), img.width, img.height, 4,
img.data.data(), img.width * 4)) {
std::cerr << "Failed to write PNG: " << out << "\n";
return false;
}
std::cout << blpPath << " -> " << out.string() << " (" << img.width << "x" << img.height << ")\n";
return true;
}
static bool convertPngToBlp(const std::string& pngPath) {
// Load PNG
int w, h, channels;
unsigned char* pixels = stbi_load(pngPath.c_str(), &w, &h, &channels, 4);
if (!pixels) {
std::cerr << "Failed to read PNG: " << pngPath << "\n";
return false;
}
// Write a simple uncompressed BLP2 (ARGB8888)
fs::path out = fs::path(pngPath);
out.replace_extension(".blp");
std::ofstream f(out.string(), std::ios::binary);
if (!f.is_open()) {
stbi_image_free(pixels);
std::cerr << "Failed to open output: " << out << "\n";
return false;
}
// BLP2 header
f.write("BLP2", 4);
uint32_t version = 1;
f.write(reinterpret_cast<const char*>(&version), 4);
uint8_t compression = 3; // uncompressed
uint8_t alphaDepth = 8;
uint8_t alphaEncoding = 0;
uint8_t hasMips = 0;
f.write(reinterpret_cast<const char*>(&compression), 1);
f.write(reinterpret_cast<const char*>(&alphaDepth), 1);
f.write(reinterpret_cast<const char*>(&alphaEncoding), 1);
f.write(reinterpret_cast<const char*>(&hasMips), 1);
uint32_t width = static_cast<uint32_t>(w);
uint32_t height = static_cast<uint32_t>(h);
f.write(reinterpret_cast<const char*>(&width), 4);
f.write(reinterpret_cast<const char*>(&height), 4);
// Mip offsets (16 entries) — only first used
uint32_t dataSize = width * height * 4;
uint32_t headerSize = 4 + 4 + 4 + 4 + 16 * 4 + 16 * 4 + 256 * 4; // magic+version+dims+mips+palette
uint32_t mipOffsets[16] = {};
uint32_t mipSizes[16] = {};
mipOffsets[0] = headerSize;
mipSizes[0] = dataSize;
f.write(reinterpret_cast<const char*>(mipOffsets), sizeof(mipOffsets));
f.write(reinterpret_cast<const char*>(mipSizes), sizeof(mipSizes));
// Empty palette (256 entries)
uint32_t palette[256] = {};
f.write(reinterpret_cast<const char*>(palette), sizeof(palette));
// Convert RGBA → BGRA for BLP
std::vector<uint8_t> bgra(dataSize);
for (int i = 0; i < w * h; ++i) {
bgra[i * 4 + 0] = pixels[i * 4 + 2]; // B
bgra[i * 4 + 1] = pixels[i * 4 + 1]; // G
bgra[i * 4 + 2] = pixels[i * 4 + 0]; // R
bgra[i * 4 + 3] = pixels[i * 4 + 3]; // A
}
f.write(reinterpret_cast<const char*>(bgra.data()), dataSize);
stbi_image_free(pixels);
std::cout << pngPath << " -> " << out.string() << " (" << w << "x" << h << ")\n";
return true;
}
static void printUsage(const char* prog) {
std::cout << "Usage:\n"
<< " " << prog << " --to-png <file.blp> Convert BLP to PNG\n"
<< " " << prog << " --to-blp <file.png> Convert PNG to BLP\n"
<< " " << prog << " --batch <directory> [--recursive] Batch convert BLP->PNG\n";
}
int main(int argc, char** argv) {
if (argc < 3) {
printUsage(argv[0]);
return 1;
}
std::string mode = argv[1];
if (mode == "--to-png") {
return convertBlpToPng(argv[2]) ? 0 : 1;
}
if (mode == "--to-blp") {
return convertPngToBlp(argv[2]) ? 0 : 1;
}
if (mode == "--batch") {
std::string dir = argv[2];
bool recursive = (argc > 3 && std::strcmp(argv[3], "--recursive") == 0);
int count = 0, failed = 0;
auto processEntry = [&](const fs::directory_entry& entry) {
if (!entry.is_regular_file()) return;
std::string ext = entry.path().extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (ext == ".blp") {
if (convertBlpToPng(entry.path().string())) {
count++;
} else {
failed++;
}
}
};
if (recursive) {
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
processEntry(entry);
}
} else {
for (const auto& entry : fs::directory_iterator(dir)) {
processEntry(entry);
}
}
std::cout << "Batch complete: " << count << " converted, " << failed << " failed\n";
return failed > 0 ? 1 : 0;
}
std::cerr << "Unknown mode: " << mode << "\n";
printUsage(argv[0]);
return 1;
}