#include #include #include #include #include #include #include #include #include #include #include #include "Image.h" #include "SpriteAssetLoader.h" #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #define NOMINMAX #include #else #include #include #include #endif namespace { enum class TransformMode { None, Resize, Fit, Tom800x480Preset }; struct TransformOptions { TransformMode mode; int32_t width; int32_t height; TransformOptions() : mode(TransformMode::None), width(0), height(0) {} }; static bool IsPathSeparator(char value) { return value == '/' || value == '\\'; } static std::string ToLower(std::string text) { std::transform(text.begin(), text.end(), text.begin(), [](char value) { return static_cast(std::tolower(static_cast(value))); }); return text; } static std::string JoinPath(const std::string& directory, const std::string& fileName) { if (directory.empty()) { return fileName; } if (IsPathSeparator(directory[directory.size() - 1])) { return directory + fileName; } return directory + "/" + fileName; } static std::string GetFileName(const std::string& path) { const size_t slash = path.find_last_of("/\\"); if (slash == std::string::npos) { return path; } return path.substr(slash + 1); } static std::string GetParentPath(const std::string& path) { const size_t slash = path.find_last_of("/\\"); if (slash == std::string::npos) { return std::string(); } if (slash == 0) { return path.substr(0, 1); } return path.substr(0, slash); } static std::string ChangeExtensionToSprite(const std::string& fileName) { const size_t slash = fileName.find_last_of("/\\"); const size_t dot = fileName.find_last_of('.'); if (dot != std::string::npos && (slash == std::string::npos || dot > slash)) { return fileName.substr(0, dot) + Asset::SpriteAssetLoader::GetFileExtension(); } return fileName + Asset::SpriteAssetLoader::GetFileExtension(); } static bool HasPngExtension(const std::string& path) { const std::string lower = ToLower(path); return lower.size() >= 4 && lower.substr(lower.size() - 4) == ".png"; } static bool ParsePositiveInt(const char* text, int32_t& value) { if (text == nullptr || text[0] == '\0') { return false; } errno = 0; char* end = nullptr; const long parsed = std::strtol(text, &end, 10); if (errno != 0 || end == text || *end != '\0' || parsed <= 0 || parsed > INT_MAX) { return false; } value = static_cast(parsed); return true; } static bool DirectoryExists(const std::string& path) { #if defined(_WIN32) const DWORD attrs = GetFileAttributesA(path.c_str()); return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0; #else struct stat info; return stat(path.c_str(), &info) == 0 && S_ISDIR(info.st_mode); #endif } static bool CreateSingleDirectory(const std::string& path) { if (path.empty() || DirectoryExists(path)) { return true; } #if defined(_WIN32) if (CreateDirectoryA(path.c_str(), nullptr) != 0) { return true; } return GetLastError() == ERROR_ALREADY_EXISTS && DirectoryExists(path); #else if (mkdir(path.c_str(), 0755) == 0) { return true; } return errno == EEXIST && DirectoryExists(path); #endif } static bool EnsureDirectoryTree(const std::string& path) { if (path.empty()) { return true; } std::string normalized = path; while (!normalized.empty() && IsPathSeparator(normalized[normalized.size() - 1])) { normalized.erase(normalized.size() - 1); } if (normalized.empty()) { return true; } size_t start = 0; if (normalized.size() >= 2 && normalized[1] == ':') { start = 2; } if (start < normalized.size() && IsPathSeparator(normalized[start])) { ++start; } for (size_t i = start; i <= normalized.size(); ++i) { if (i != normalized.size() && !IsPathSeparator(normalized[i])) { continue; } const std::string part = normalized.substr(0, i); if (part.empty() || (part.size() == 2 && part[1] == ':')) { continue; } if (!CreateSingleDirectory(part)) { std::cerr << "Create directory failed: " << part << std::endl; return false; } } return DirectoryExists(normalized); } static bool EnsureParentDirectory(const std::string& path) { return EnsureDirectoryTree(GetParentPath(path)); } static bool ListPngFiles(const std::string& directory, std::vector& fileNames) { fileNames.clear(); #if defined(_WIN32) const std::string searchPath = JoinPath(directory, "*"); WIN32_FIND_DATAA data; HANDLE handle = FindFirstFileA(searchPath.c_str(), &data); if (handle == INVALID_HANDLE_VALUE) { return false; } do { if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 && HasPngExtension(data.cFileName)) { fileNames.push_back(data.cFileName); } } while (FindNextFileA(handle, &data) != 0); FindClose(handle); #else DIR* dir = opendir(directory.c_str()); if (dir == nullptr) { return false; } dirent* entry = nullptr; while ((entry = readdir(dir)) != nullptr) { const std::string name = entry->d_name; if (name == "." || name == ".." || !HasPngExtension(name)) { continue; } fileNames.push_back(name); } closedir(dir); #endif std::sort(fileNames.begin(), fileNames.end(), [](const std::string& left, const std::string& right) { return ToLower(left) < ToLower(right); }); return true; } } static bool LoadPngImage(const std::string& path, RenderData::Image& image) { SDL_Surface* loadedSurface = IMG_Load(path.c_str()); if (loadedSurface == nullptr) { std::cerr << "IMG_Load failed: " << path << " : " << IMG_GetError() << std::endl; return false; } SDL_Surface* rgbaSurface = SDL_ConvertSurfaceFormat(loadedSurface, SDL_PIXELFORMAT_RGBA8888, 0); SDL_FreeSurface(loadedSurface); if (rgbaSurface == nullptr) { std::cerr << "SDL_ConvertSurfaceFormat failed: " << SDL_GetError() << std::endl; return false; } std::vector pixels(static_cast(rgbaSurface->w) * rgbaSurface->h, 0); if (SDL_LockSurface(rgbaSurface) != 0) { std::cerr << "SDL_LockSurface failed: " << SDL_GetError() << std::endl; SDL_FreeSurface(rgbaSurface); return false; } for (int32_t y = 0; y < rgbaSurface->h; ++y) { const uint8_t* srcRow = static_cast(rgbaSurface->pixels) + y * rgbaSurface->pitch; const uint32_t* srcPixels = reinterpret_cast(srcRow); for (int32_t x = 0; x < rgbaSurface->w; ++x) { pixels[static_cast(y) * rgbaSurface->w + x] = srcPixels[x]; } } SDL_UnlockSurface(rgbaSurface); image = RenderData::Image(rgbaSurface->w, rgbaSurface->h, pixels); SDL_FreeSurface(rgbaSurface); return image.is_valid(); } static RenderData::Image ResizeImageNearest(const RenderData::Image& source, int32_t width, int32_t height) { if (!source.is_valid() || width <= 0 || height <= 0) { return RenderData::Image(); } RenderData::Image result(width, height); for (int32_t y = 0; y < height; ++y) { const int32_t sourceY = y * source.get_height() / height; for (int32_t x = 0; x < width; ++x) { const int32_t sourceX = x * source.get_width() / width; result.set_pixel(x, y, source.get_pixel_fast(sourceX, sourceY)); } } return result; } static RenderData::Image ResizeImageToFit(const RenderData::Image& source, int32_t maxWidth, int32_t maxHeight) { if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0) { return RenderData::Image(); } const float scaleX = static_cast(maxWidth) / source.get_width(); const float scaleY = static_cast(maxHeight) / source.get_height(); const float scale = std::min(scaleX, scaleY); const int32_t width = std::max(1, static_cast(source.get_width() * scale)); const int32_t height = std::max(1, static_cast(source.get_height() * scale)); return ResizeImageNearest(source, width, height); } static TransformOptions ResolveTransformForInput( const std::string& inputPath, const TransformOptions& requested) { if (requested.mode != TransformMode::Tom800x480Preset) { return requested; } TransformOptions resolved; const std::string fileName = ToLower(GetFileName(inputPath)); if (fileName == "background.png") { resolved.mode = TransformMode::Resize; resolved.width = 800; resolved.height = 480; } else if (fileName.find("tom-") == 0) { resolved.mode = TransformMode::Fit; resolved.width = 560; resolved.height = 360; } else if (fileName.find("ui-") == 0) { resolved.mode = TransformMode::Fit; resolved.width = 72; resolved.height = 72; } return resolved; } static bool ApplyTransform(const std::string& inputPath, const TransformOptions& options, RenderData::Image& image) { const TransformOptions resolved = ResolveTransformForInput(inputPath, options); switch (resolved.mode) { case TransformMode::None: return image.is_valid(); case TransformMode::Resize: image = ResizeImageNearest(image, resolved.width, resolved.height); return image.is_valid(); case TransformMode::Fit: image = ResizeImageToFit(image, resolved.width, resolved.height); return image.is_valid(); case TransformMode::Tom800x480Preset: return false; } return false; } static bool ConvertFile( const std::string& inputPath, const std::string& outputPath, const TransformOptions& transformOptions) { RenderData::Image image; if (!LoadPngImage(inputPath, image)) { return false; } if (!ApplyTransform(inputPath, transformOptions, image)) { std::cerr << "Image transform failed: " << inputPath << std::endl; return false; } if (!EnsureParentDirectory(outputPath)) { return false; } if (!Asset::SpriteAssetLoader::Save(outputPath, image)) { std::cerr << "Save failed: " << outputPath << std::endl; return false; } std::cout << inputPath << " -> " << outputPath << " (" << image.get_width() << "x" << image.get_height() << ")" << std::endl; return true; } static bool ConvertBatch( const std::string& inputDirectory, const std::string& outputDirectory, const TransformOptions& transformOptions) { std::vector files; if (!ListPngFiles(inputDirectory, files) || files.empty()) { std::cerr << "No PNG files found: " << inputDirectory << std::endl; return false; } if (!EnsureDirectoryTree(outputDirectory)) { return false; } size_t successCount = 0; for (size_t i = 0; i < files.size(); ++i) { const std::string inputPath = JoinPath(inputDirectory, files[i]); const std::string outputPath = JoinPath(outputDirectory, ChangeExtensionToSprite(files[i])); if (ConvertFile(inputPath, outputPath, transformOptions)) { ++successCount; } } std::cout << "Converted " << successCount << " / " << files.size() << " PNG files." << std::endl; return successCount == files.size(); } static bool ParseTransformOptions(int argc, char* argv[], int startIndex, TransformOptions& options) { if (startIndex >= argc) { options = TransformOptions(); return true; } const std::string mode = argv[startIndex]; if (mode == "--resize" || mode == "--fit") { if (startIndex + 2 >= argc) { return false; } int32_t width = 0; int32_t height = 0; if (!ParsePositiveInt(argv[startIndex + 1], width) || !ParsePositiveInt(argv[startIndex + 2], height) || startIndex + 3 != argc) { return false; } options.mode = mode == "--resize" ? TransformMode::Resize : TransformMode::Fit; options.width = width; options.height = height; return true; } if (mode == "--preset") { if (startIndex + 2 != argc) { return false; } const std::string presetName = argv[startIndex + 1]; if (presetName != "tom-800x480") { std::cerr << "Unknown preset: " << presetName << std::endl; return false; } options.mode = TransformMode::Tom800x480Preset; return true; } return false; } static void PrintUsage() { std::cout << "Usage:" << std::endl; std::cout << " SpriteAssetTool input.png output.sprite [--resize width height]" << std::endl; std::cout << " SpriteAssetTool input.png output.sprite [--fit maxWidth maxHeight]" << std::endl; std::cout << " SpriteAssetTool --batch inputDir outputDir [--resize width height]" << std::endl; std::cout << " SpriteAssetTool --batch inputDir outputDir [--fit maxWidth maxHeight]" << std::endl; std::cout << " SpriteAssetTool --batch game/assets/raw game/assets/sprites --preset tom-800x480" << std::endl; } int main(int argc, char* argv[]) { if (argc < 3) { PrintUsage(); return 1; } const int imageFlags = IMG_INIT_PNG; if ((IMG_Init(imageFlags) & imageFlags) != imageFlags) { std::cerr << "IMG_Init failed: " << IMG_GetError() << std::endl; return 1; } TransformOptions transformOptions; bool ok = false; const std::string command = argv[1]; if (command == "--batch") { if (argc < 4 || !ParseTransformOptions(argc, argv, 4, transformOptions)) { PrintUsage(); IMG_Quit(); return 1; } ok = ConvertBatch(argv[2], argv[3], transformOptions); } else { if (!ParseTransformOptions(argc, argv, 3, transformOptions)) { PrintUsage(); IMG_Quit(); return 1; } ok = ConvertFile(argv[1], argv[2], transformOptions); } IMG_Quit(); return ok ? 0 : 1; }