diff --git a/.gitignore b/.gitignore index b8bd026..01e3e70 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ *.exe *.out *.app + +# Build directories from Cmake +build/ +build-debug/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0c3b96f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,110 @@ +cmake_minimum_required (VERSION 2.6) +project (nin10kit) + +set(CPACK_BINARY_DEB "ON") +set(CPACK_BINARY_RPM "ON") +set(CPACK_BINARY_TGZ "ON") +set(CPACK_BINARY_ZIP "ON") +set(CPACK_GENERATOR "DEB") +set(CPACK_NSIS_DISPLAY_NAME "nin10kit 1.0.0") +set(CPACK_NSIS_PACKAGE_NAME "nin10kit 1.0.0") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Tools for developing on Nintendo handheld systems") +set(CPACK_PACKAGE_INSTALL_DIRECTORY "nin10kit 1.0.0") +set(CPACK_PACKAGE_INSTALL_REGISTRY_KEY "nin10kit 1.0.0") +set(CPACK_PACKAGE_VERSION "1.0.0") +set(CPACK_PACKAGE_VERSION_MAJOR "1") +set(CPACK_PACKAGE_VERSION_MINOR "0") +set(CPACK_PACKAGE_VERSION_PATCH "0") +set(CPACK_SOURCE_GENERATOR "") +set(CPACK_SOURCE_TGZ "ON") + +set(CPACK_DEBIAN_PACKAGE_NAME "nin10kit") +set(CPACK_DEBIAN_PACKAGE_VERSION "1.0") +set(CPACK_DEBIAN_PACKAGE_DEPENDS "") +set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Brandon Whitehead ") +set(CPACK_DEBIAN_PACKAGE_DESCRIPTION ".") + +set(CPACK_PACKAGE_FILE_NAME "nin10kit-1.0.0") +set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE ${ARCH}) + +include(CPack) + +option(ENABLE_DEBUG "Build debug version" OFF) + +IF(ENABLE_DEBUG) + set(CMAKE_CXX_FLAGS "-g -Wall -std=c++11") +ELSE() + set(CMAKE_CXX_FLAGS "-O3 -s -Wall -std=c++11") +ENDIF(ENABLE_DEBUG) + +# Source files definition +set(SRC_SHARED + shared/color.cpp + shared/cpercep.cpp + shared/dither.cpp + shared/exportfile.cpp + shared/fileutils.cpp + shared/headerfile.cpp + shared/histogram.cpp + shared/implementationfile.cpp + shared/Logger.cpp + shared/mediancut.cpp + shared/reductionhelper.cpp + shared/Scanner.cpp + shared/shared.cpp +) + +set(SRC_NIN10KIT + cli/main.cpp + cli/3ds-exporter.cpp + cli/ds-exporter.cpp + cli/gba-exporter.cpp + cli/cmd-line-parser-helper.cpp +) + +#set(SRC_NIN10KITGUI +#) + +find_package(wxWidgets REQUIRED core base) +include(${wxWidgets_USE_FILE}) +find_package(ImageMagick COMPONENTS Magick++ MagickWand MagickCore REQUIRED) + +include_directories(${nin10kit_SOURCE_DIR}/shared) +include_directories(${ImageMagick_INCLUDE_DIRS}) +link_directories(${nin10kit_SOURCE_DIR}/shared) + +add_library( + shared_files + STATIC + ${SRC_SHARED} +) + +target_link_libraries( + shared_files + ${ImageMagick_LIBRARIES} +) + +add_executable( + nin10kit + ${SRC_NIN10KIT} +) + +target_link_libraries( + nin10kit + shared_files + ${wxWidgets_LIBRARIES} +) + +#add_executable( +# nin10gui +# ${SRC_NIN10GUI} +#) + +#target_link_libraries( +# nin10gui +# shared_files +# ${wxWidgets_LIBRARIES} +#) + +install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/nin10kit DESTINATION bin) +#install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/nin10gui DESTINATION bin) diff --git a/cli/3ds-exporter.cpp b/cli/3ds-exporter.cpp new file mode 100644 index 0000000..16a3a2f --- /dev/null +++ b/cli/3ds-exporter.cpp @@ -0,0 +1,17 @@ +#include +#include + +#include "fileutils.hpp" +#include "reductionhelper.hpp" +#include "shared.hpp" + +void Do3DSExport(const std::vector& images, const std::vector& tilesets) +{ + // Add images to header and implementation files + for (const auto& image : images) + { + std::shared_ptr image_ptr(new Image32Bpp(image)); + header.Add(image_ptr); + implementation.Add(image_ptr); + } +} diff --git a/cli/allocator_2d_test.cpp b/cli/allocator_2d_test.cpp new file mode 100644 index 0000000..dbeaab0 --- /dev/null +++ b/cli/allocator_2d_test.cpp @@ -0,0 +1,387 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +int valid_sizes[16] = +{ + 0x0, 0x1, 0x2, -1, + 0x4, 0x5, 0x6, -1, + 0x8, 0x9, 0xA, 0xB, + -1, -1, 0xE, 0xF, +}; + +class Sprite +{ + public: + Sprite() : width(0), height(0) {}; + Sprite(unsigned int w, unsigned int h) : width(w), height(h) {}; + unsigned int Size() const {return width * height;}; + std::string name; + unsigned int width; + unsigned int height; + int offset; +}; + +class BlockSize +{ + public: + BlockSize() : width(0), height(0) {}; + BlockSize(unsigned int _width, unsigned int _height) : width(_width), height(_height) {}; + bool operator==(const BlockSize& rhs) const; + bool operator<(const BlockSize& rhs) const; + unsigned int Size() const {return width * height;}; + bool isBiggestSize() const {return width == 8 and height == 8;}; + static std::vector BiggerSizes(const BlockSize& b); + unsigned int width, height; + +}; + +class Block +{ + public: + Block() : x(0), y(0), sprite_id(-1) {}; + Block(int width, int height) : size(width, height), x(0), y(0), sprite_id(-1) {}; + Block(const BlockSize& _size) : size(_size), x(0), y(0), sprite_id(-1) {}; + Block(int _x, int _y, int width, int height) : size(width, height), x(_x), y(_y), sprite_id(-1) {}; + Block(int _x, int _y, const BlockSize& _size) : size(_size), x(_x), y(_y), sprite_id(-1) {}; + Block HSplit(); + Block VSplit(); + Block Split(const BlockSize& to_this_size); + BlockSize size; + int x; + int y; + int sprite_id; +}; + +class SpriteSheet +{ + public: + SpriteSheet(const std::vector& sprites, unsigned int width, unsigned int height); + void Compile(); + std::map> freeBlocks; + std::vector sprites; + std::list placedBlocks; + unsigned int width, height; + std::vector data; + private: + void PlaceSprites(); + bool AssignBlockIfAvailable(BlockSize& size, Sprite& sprite, unsigned int i); + bool HasAvailableBlock(const BlockSize& size); + void SliceBlock(const BlockSize& size, const std::list& slice); +}; + + +bool BlockSize::operator==(const BlockSize& rhs) const +{ + return width == rhs.width && height == rhs.height; +} + +bool BlockSize::operator<(const BlockSize& rhs) const +{ + if (width != rhs.width) + return width < rhs.width; + else + return height < rhs.height; +} + +std::vector BlockSize::BiggerSizes(const BlockSize& b) +{ + switch(b.Size()) + { + case 1: + return {BlockSize(2, 1), BlockSize(1, 2)}; + case 2: + if (b.width == 2) + return {BlockSize(4, 1), BlockSize(2, 2)}; + else + return {BlockSize(1, 4), BlockSize(2, 2)}; + case 4: + if (b.width == 4) + return {BlockSize(4, 2)}; + else if (b.height == 4) + return {BlockSize(2, 4)}; + else + return {BlockSize(4, 2), BlockSize(2, 4)}; + case 8: + return {BlockSize(4, 4)}; + case 16: + return {BlockSize(8, 4), BlockSize(4, 8)}; + case 32: + return {BlockSize(8, 8)}; + default: + return {}; + } +} + +Block Block::VSplit() +{ + size.height /= 2; + return Block(x, y + size.height, size); +} + +Block Block::HSplit() +{ + size.width /= 2; + return Block(x + size.width, y, size); +} + +Block Block::Split(const BlockSize& to_this_size) +{ + if (size.width == to_this_size.width) + return VSplit(); + else if (size.height == to_this_size.height) + return HSplit(); + else + { + printf("%d %d => %d %d\n", size.width, size.height, to_this_size.width, to_this_size.height); + throw "Error Block::Split"; + } + + return Block(); +} + +SpriteSheet::SpriteSheet(const std::vector& _sprites, unsigned int _width, unsigned int _height) : sprites(_sprites), width(_width), height(_height), data(width * height) +{ +} + +bool SpriteCompare(const Sprite& lhs, const Sprite& rhs) +{ + if (lhs.Size() != rhs.Size()) + return lhs.Size() > rhs.Size(); + else + // Special case 2x2 should be of lesser priority than 4x1/1x4 + return lhs.width + lhs.height > rhs.width + rhs.height; +} + +void SpriteSheet::Compile() +{ + PlaceSprites(); + for (const auto& block : placedBlocks) + { + for (unsigned int i = 0; i < block.size.height; i++) + { + for (unsigned int j = 0; j < block.size.width; j++) + { + int x = block.x + j; + int y = block.y + i; + data[y * width + x] = block.sprite_id; + } + } + } +} + +void SpriteSheet::PlaceSprites() +{ + // Gimme some blocks. + BlockSize size(8, 8); + for (unsigned int y = 0; y < height; y += 8) + { + for (unsigned int x = 0; x < width; x += 8) + { + freeBlocks[size].push_back(Block(x, y, size)); + } + } + // Sort by request size + std::sort(sprites.begin(), sprites.end(), SpriteCompare); + + for (unsigned int i = 0; i < sprites.size(); i++) + { + Sprite& sprite = sprites[i]; + printf("request %d %d\n", sprites[i].width, sprites[i].height); + BlockSize size(sprite.width, sprite.height); + std::list slice; + + // Mother may I have block of this size? + if (AssignBlockIfAvailable(size, sprite, i)) + goto allocated; + + if (size.isBiggestSize()) + { + std::stringstream oss; + oss << "[FATAL] Out of sprite memory could not allocate sprite size (" << sprite.width << "," << sprite.height << ")"; + throw oss.str(); + } + + slice.push_front(size); + while (!HasAvailableBlock(size)) + { + std::vector sizes = BlockSize::BiggerSizes(size); + if (sizes.empty()) + { + std::stringstream oss; + oss << "[FATAL] Out of sprite memory could not allocate sprite size (" << sprite.width << "," << sprite.height << ")"; + throw oss.str(); + } + + // Default next search size will be last. + size = sizes.back(); + for (auto& new_size : sizes) + { + if (HasAvailableBlock(new_size)) + { + size = new_size; + break; + } + } + if (!HasAvailableBlock(size)) + slice.push_front(size); + } + + if (!HasAvailableBlock(size)) + { + std::stringstream oss; + oss << "[FATAL] Out of sprite memory could not allocate sprite size (" << sprite.width << "," << sprite.height << ")"; + throw oss.str(); + } + + printf("BEFORE SPLIT\n"); + for (const auto& size_list : freeBlocks) + { + printf("(%d %d) => ", size_list.first.width, size_list.first.height); + for (const auto& block : size_list.second) + { + printf("(%d %d %d %d), ", block.x, block.y, block.size.width, block.size.height); + } + printf("\n"); + } + SliceBlock(size, slice); + printf("AFTER SPLIT\n"); + for (const auto& size_list : freeBlocks) + { + printf("(%d %d) => ", size_list.first.width, size_list.first.height); + for (const auto& block : size_list.second) + { + printf("(%d %d %d %d), ", block.x, block.y, block.size.width, block.size.height); + } + printf("\n"); + } + + printf("FIN\n"); + size = BlockSize(sprite.width, sprite.height); + // Mother may I have block of this size? + if (AssignBlockIfAvailable(size, sprite, i)) + goto allocated; + else + { + std::stringstream oss; + oss << "[FATAL] Out of sprite memory could not allocate sprite size (" << sprite.width << "," << sprite.height << ")"; + throw oss.str(); + } + + allocated: + for (const auto& size_list : freeBlocks) + { + printf("(%d %d) => ", size_list.first.width, size_list.first.height); + for (const auto& block : size_list.second) + { + printf("(%d %d %d %d), ", block.x, block.y, block.size.width, block.size.height); + } + printf("\n"); + } + } +} + +bool SpriteSheet::AssignBlockIfAvailable(BlockSize& size, Sprite& sprite, unsigned int i) +{ + if (HasAvailableBlock(size)) + { + // Yes you may deary. + Block allocd = freeBlocks[size].front(); + freeBlocks[size].pop_front(); + allocd.sprite_id = i; + sprite.offset = allocd.y * width + allocd.x; + placedBlocks.push_back(allocd); + return true; + } + + return false; +} + +bool SpriteSheet::HasAvailableBlock(const BlockSize& size) +{ + return !freeBlocks[size].empty(); +} + +void SpriteSheet::SliceBlock(const BlockSize& size, const std::list& slice) +{ + Block toSplit = freeBlocks[size].front(); + freeBlocks[size].pop_front(); + + for (const auto& split_size : slice) + { + Block other = toSplit.Split(split_size); + freeBlocks[split_size].push_back(other); + } + + freeBlocks[toSplit.size].push_front(toSplit); +} + +int main(int argc, char** argv) +{ + srand(time(NULL)); + int width = atoi(argv[1]); + int height = atoi(argv[2]); + int total_size = 0; + int max_size = width * height; + + std::vector sprites; + while (total_size < max_size) + { + int i = rand() & 0xF; + + int size = valid_sizes[i]; + if (size == -1) continue; + int sw = 1 << (size >> 2); + int sh = 1 << (size & 3); + + if (total_size + sw * sh > max_size) continue; + + sprites.push_back(Sprite(sw, sh)); + total_size += sw * sh; + } + + for (const auto& sprite : sprites) + { + printf("%d %d\n", sprite.width, sprite.height); + } + + try + { + SpriteSheet sheet(sprites, width, height); + sheet.Compile(); + + for (unsigned int y = 0; y < sheet.height; y++) + { + for (unsigned int x = 0; x < sheet.width; x++) + { + printf("%02d ", sheet.data[y * sheet.width + x]); + } + printf("\n"); + } + } + catch (const std::exception& ex) + { + printf("Image to GBA (sprites) failed! Reason: %s\n", ex.what()); + exit(EXIT_FAILURE); + } + catch (const std::string& ex) + { + printf("Image to GBA (sprites) failed! Reason: %s\n", ex.c_str()); + exit(EXIT_FAILURE); + } + catch (const char* ex) + { + printf("Image to GBA (sprites) failed! Reason: %s\n", ex); + exit(EXIT_FAILURE); + } + catch (...) + { + printf("Image to GBA (sprites) failed!"); + exit(EXIT_FAILURE); + } +} diff --git a/cli/cmd-line-parser-helper.cpp b/cli/cmd-line-parser-helper.cpp new file mode 100644 index 0000000..bfcbaf1 --- /dev/null +++ b/cli/cmd-line-parser-helper.cpp @@ -0,0 +1,92 @@ +#include "cmd-line-parser-helper.hpp" +#include "Scanner.hpp" +#include "Logger.hpp" + +bool CmdLineParserHelper::GetSwitch(const std::string& param) +{ + return parser.Found(param); +} + +int CmdLineParserHelper::GetInt(const std::string& param, int def_value, int min, int max) +{ + long ret; + if (!parser.Found(param, &ret)) return def_value; + + int val = ret; + if (val < min || val > max) + { + WarnLog("Invalid value given for %s range [%d %d] given %d ignoring", param.c_str(), min, max, val); + val = def_value; + } + return val; +} + +int CmdLineParserHelper::GetHexInt(const std::string& param, int def_value, int min, int max) +{ + wxString hex; + if (!parser.Found(param, &hex)) return def_value; + long ret; + if (!hex.ToLong(&ret, 16)) + { + WarnLog("Could not parse %s into hex ignoring", param.c_str()); + return def_value; + } + + int val = ret; + if (val < min || val > max) + { + WarnLog("Invalid value given for %s range [%d %d] given %d ignoring", param.c_str(), min, max, val); + val = def_value; + } + return val; +} + + +long CmdLineParserHelper::GetLong(const std::string& param, long def_value, long min, long max) +{ + long ret = std::max(std::min(def_value, max), min); + parser.Found(param, &ret); + return ret; +} + +std::string CmdLineParserHelper::GetString(const std::string& param, const std::string& def_value) +{ + wxString ret; + return parser.Found(param, &ret) ? ret.ToStdString() : def_value; +} + +std::vector CmdLineParserHelper::GetListInt(const std::string& param, const std::vector& def_value) +{ + wxString list; + if (!parser.Found(param, &list)) return def_value; + + std::vector ret; + Scanner scan(list.ToStdString(), ","); + while (scan.HasMoreTokens()) + { + int32_t var; + if (!scan.Next(var)) + FatalLog("Error parsing param %s", param.c_str()); + ret.push_back(var); + } + + return ret; +} + +std::vector CmdLineParserHelper::GetListString(const std::string& param, const std::vector& def_value) +{ + wxString list; + if (!parser.Found(param, &list)) return def_value; + + std::vector ret; + Scanner scan(list.ToStdString(), ","); + while (scan.HasMoreTokens()) + { + std::string var; + if (!scan.Next(var)) + FatalLog("Error parsing param %s", param.c_str()); + ret.push_back(var); + } + + return ret; +} diff --git a/cli/cmd-line-parser-helper.hpp b/cli/cmd-line-parser-helper.hpp new file mode 100644 index 0000000..92e1c32 --- /dev/null +++ b/cli/cmd-line-parser-helper.hpp @@ -0,0 +1,26 @@ +#ifndef CMD_LINE_PARSER_HELPER_HPP +#define CMD_LINE_PARSER_HELPER_HPP + +#include +#include +#include + +#include + +class CmdLineParserHelper +{ + public: + CmdLineParserHelper(wxCmdLineParser& parser_) : parser(parser_) {} + ~CmdLineParserHelper() {} + bool GetSwitch(const std::string& param); + int GetInt(const std::string& param, int def_value = 0, int min = std::numeric_limits::min(), int max = std::numeric_limits::max()); + int GetHexInt(const std::string& param, int def_value = 0, int min = std::numeric_limits::min(), int max = std::numeric_limits::max()); + long GetLong(const std::string& param, long def_value = 0, long min = std::numeric_limits::min(), long max = std::numeric_limits::max()); + std::string GetString(const std::string& param, const std::string& def_value = ""); + std::vector GetListInt(const std::string& param, const std::vector& def_value = std::vector()); + std::vector GetListString(const std::string& param, const std::vector& def_value = std::vector()); + private: + wxCmdLineParser& parser; +}; + +#endif diff --git a/cli/ds-exporter.cpp b/cli/ds-exporter.cpp new file mode 100644 index 0000000..48c8ddd --- /dev/null +++ b/cli/ds-exporter.cpp @@ -0,0 +1,17 @@ +#include +#include + +#include "fileutils.hpp" +#include "reductionhelper.hpp" +#include "shared.hpp" + +void DoDSExport(const std::vector& images32, const std::vector& tilesets32) +{ + // Add images to header and implementation files + for (const auto& image : images32) + { + std::shared_ptr image_ptr(new Image32Bpp(image)); + header.Add(image_ptr); + implementation.Add(image_ptr); + } +} diff --git a/cli/gba-exporter.cpp b/cli/gba-exporter.cpp new file mode 100644 index 0000000..d74941e --- /dev/null +++ b/cli/gba-exporter.cpp @@ -0,0 +1,143 @@ +#include +#include + +#include "fileutils.hpp" +#include "reductionhelper.hpp" +#include "shared.hpp" + +void DoMode0Export(const std::vector& images); +void DoMode3Export(const std::vector& images); +void DoMode4Export(const std::vector& images); +void DoTilesetExport(const std::vector& images); +void DoMapExport(const std::vector& images, const std::vector& tilesets); +void DoSpriteExport(const std::vector& images); + +void DoGBAExport(const std::vector& images32, const std::vector& tilesets32) +{ + std::vector images; + for (const auto& image : images32) + images.push_back(Image16Bpp(image)); + + std::vector tilesets; + for (const auto& image : tilesets32) + tilesets.push_back(Image16Bpp(image)); + + if (params.transparent_color != -1) + { + int color = params.transparent_color; + char r = (color >> 19) & 0x1F; + char g = (color >> 11) & 0x1F; + char b = (color >> 3) & 0x1F; + unsigned short gba_color = r | g << 5 | b << 10; + header.SetTransparent(gba_color); + implementation.SetTransparent(gba_color); + } + + if (params.mode == "0") + DoMode0Export(images); + else if (params.mode == "3") + DoMode3Export(images); + else if (params.mode == "4") + DoMode4Export(images); + else if (params.mode == "SPRITES") + DoSpriteExport(images); + else if (params.mode == "TILES") + DoTilesetExport(images); + else if (params.mode == "MAP") + DoMapExport(images, tilesets); + else + FatalLog("No/Invalid mode specified image not exported"); +} + +void DoMode0Export(const std::vector& images) +{ + // If split then form several maps + // If !split then start a scene + // Add appropriate object to header/implementation + if (params.split) + { + for (const auto& image : images) + { + std::shared_ptr map_ptr(new Map(image, params.bpp)); + header.Add(map_ptr); + implementation.Add(map_ptr); + } + } + else + { + std::shared_ptr scene(new MapScene(images, params.symbol_base_name, params.bpp)); + header.Add(scene); + implementation.Add(scene); + } +} + +void DoMode3Export(const std::vector& images) +{ + for (const auto& image : images) + { + std::shared_ptr image_ptr(new Image16Bpp(image)); + header.Add(image_ptr); + implementation.Add(image_ptr); + } +} + +void DoMode4Export(const std::vector& images) +{ + // If split then get vector of 8 bit images + // If !split then cause a scene. + // Add appropriate object to header/implementation. + if (params.split) + { + for (const auto& image : images) + { + std::shared_ptr image_ptr(new Image8Bpp(image)); + header.Add(image_ptr); + implementation.Add(image_ptr); + } + } + else + { + std::shared_ptr scene(new Image8BppScene(images, params.symbol_base_name)); + header.Add(scene); + implementation.Add(scene); + } +} + +void DoSpriteExport(const std::vector& images) +{ + // Do the work of sprite conversion. + // Form the sprite scene and then add it to header and implementation + SpriteScene* scene(new SpriteScene(images, params.symbol_base_name, params.export_2d, params.bpp)); + std::shared_ptr exportable(scene); + + // Build the sprite scene and place all sprites. (If applicable) + scene->Build(); + + header.Add(exportable); + implementation.Add(exportable); +} + +void DoTilesetExport(const std::vector& images) +{ + // Form the tileset and then add it to header and implementation + std::shared_ptr tileset(new Tileset(images, params.symbol_base_name, params.bpp)); + + header.Add(tileset); + implementation.Add(tileset); +} + +void DoMapExport(const std::vector& images, const std::vector& tilesets) +{ + if (tilesets.empty()) + FatalLog("Map export specified however -tileset not given"); + + // Form the tileset from the images given this is a dummy + std::shared_ptr tileset(new Tileset(tilesets, "", params.bpp)); + + for (const auto& image : images) + { + std::shared_ptr map_ptr(new Map(image, tileset)); + header.Add(map_ptr); + implementation.Add(map_ptr); + } +} diff --git a/cli/main.cpp b/cli/main.cpp new file mode 100644 index 0000000..88c508f --- /dev/null +++ b/cli/main.cpp @@ -0,0 +1,417 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "cpercep.hpp" +#include "cmd-line-parser-helper.hpp" +#include "headerfile.hpp" +#include "fileutils.hpp" +#include "implementationfile.hpp" +#include "Logger.hpp" +#include "reductionhelper.hpp" +#include "Scanner.hpp" +#include "shared.hpp" +#include "version.h" + +void DoGBAExport(const std::vector& images, const std::vector& tilesets); +void DoDSExport(const std::vector& images, const std::vector& tilesets); +void Do3DSExport(const std::vector& images, const std::vector& tilesets); + +class Nin10KitApp : public wxAppConsole +{ + public: + virtual bool OnInit(); + virtual void OnInitCmdLine(wxCmdLineParser& parser); + virtual bool OnCmdLineParsed(wxCmdLineParser& parser); + bool DoExportImages(); + int OnRun(); +}; + +static const wxCmdLineEntryDesc cmd_descriptions[] = +{ + {wxCMD_LINE_SWITCH, "h", "help", "Displays help on command line parameters", + wxCMD_LINE_VAL_NONE, wxCMD_LINE_OPTION_HELP}, + + // Debugging + {wxCMD_LINE_OPTION, "log", "log", "Debug logging -log=num (0:fatal,1:error,2:warn,3:info,4:verbose) default 3", + wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL}, + + // Modes + {wxCMD_LINE_OPTION, "mode", "mode", "Special Mode String (-mode=(0,3,4,tiles,map,sprites))", + wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_OPTION, "device", "device", "Special Device String (-device=(gba, ds, 3ds))", + wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_OPTION, "bpp", "bpp", "Bits per pixel only for use with -mode0 or -sprites (Default 8).", + wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL}, + + // General helpful options + {wxCMD_LINE_OPTION, "output_dir", "output_dir", "output directory for exported files", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_OPTION, "names", "names", "(Usage -names=name1,name2) Renames output array names to names given. If this is used each image given must be renamed. " + "If not given then the file names of the images will be used to generate the name.", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_OPTION, "resize", "resize", "(Usage -resize=wxh,wxh2) Resize images to wxh must be given for all images " + "(example -resize=10x12,,72x42 first image=10,12, second image is not resized, last image=72x42 )", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_OPTION, "transparent", "transparent", + "(Usage -transparent=RRGGBB in hex) Makes the color RRGGBB transparent The range of r,g,b is [0,255]. " + "In mode 3 using this gives you a #define for the transparent color so you can use it to ignore those pixels when drawing. " + "In mode 4 this color will become palette entry 0 and thus the background color. " + "And in mode0 and sprites it will also be palette entry 0 so the gba will colorkey the color.", + wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_OPTION, "dither", "dither", + "ADVANCED option use at your own risk. (Usage -dither=0 or -dither=1) " + "Apply dithering. Dithering makes the image look better by reducing " + "large areas of one color as a result of reducing the number of colors in the image by adding noise.", + wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_OPTION, "dither_level", "dither_level", + "ADVANCED option use at your own risk. (Usage -dither_level=num) " + "Only applicable if -dither=1. The range of num is [0,100]. This option affects how strong the " + "dithering effect is by default it is set to 80.", + wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL}, + + // Mode 0/4 options + {wxCMD_LINE_OPTION, "start", "start", + "Only for mode4 exports. (Usage -start=X). Starts the palette off at index X. Useful if you " + "have another image already exported that has X entries.", wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_OPTION, "palette", "palette", + "Only for mode4 exports. (Usage -palette=X). Will restrict the palette to X entries rather than 256. " + "Useful when combined with -start.", + wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_SWITCH, "split", "split", + "Only for mode0/4 exports. Exports each individual image with its own palette (and tileset). Useful for sets of screens. " + "Or videos (this program even supports video formats).", + wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL}, + + // Mode 0 exclusive options + {wxCMD_LINE_OPTION, "split_sbb", "split_sbb", "(Usage -split_sbb=1-4) Given a big map image (>1024,1024) split it into multiple maps." + " 1 = (32, 32), 2 = (64, 32), 3 = (32, 64), 4 = (64, 64). Image must be divisible by split size * 8 (NOT IMPLEMENTED).", + wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_OPTION, "tileset", "tileset", "Tileset image(s) to export against when using -map.", + wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_OPTION, "border", "border", "Border around each tile in tileset image", + wxCMD_LINE_VAL_NUMBER, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_SWITCH, "force", "force", + "For mode 0 4bpp export only. If a problem occurs that could result in a major loss of quality, forces the program to export anyway (NOT IMPLEMENTED YET).", + wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL}, + + // Sprite exclusive options + {wxCMD_LINE_SWITCH, "export_2d", "export_2d", + "For sprites export only. Exports sprites for use in sprite 2d mode (Default false).", + wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL}, + {wxCMD_LINE_SWITCH, "for_bitmap", "for_bitmap", + "For sprites export only. Exports sprites for use in modes 3 and 4 (Default false).", + wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL}, + + // Advanced Mode 4 options Use at your own risk. + {wxCMD_LINE_OPTION, "weights", "weights", + "Only for mode0/4 exports. ADVANCED option use at your own risk. (Usage -weights=w1,w2,w3,w4) " + "Fine tune? median cut algorithm. w1 = volume, w2 = population, w3 = volume*population, w4 = error " + "Affects image output quality for mode4, Range of w1-w4 is [0,100] but must sum up to 100 " + "These values affects which colors are chosen by the median cut algorithm used to reduce the number " + "of colors in the image.", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL}, + + {wxCMD_LINE_PARAM, NULL, NULL, "output array name and input file(s)", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_MULTIPLE}, + {wxCMD_LINE_NONE} +}; + +IMPLEMENT_APP(Nin10KitApp); + +// All of the read in command line flags will be in this structure. +ExportParams params; + +/** OnInit + * + * Initializes the program + */ +bool Nin10KitApp::OnInit() +{ + logger->SetLogLevel(LogLevel::INFO); + VerboseLog("Init"); + + if (argc <= 1) + { + wxCmdLineParser parser(argc, argv); + OnInitCmdLine(parser); + parser.Usage(); + return false; + } + + if (!wxAppConsole::OnInit()) + FatalLog("A problem occurred, please report this and give any images the command line that caused this"); + + // Give me the invocation + std::ostringstream out; + for (int i = 1; i < wxAppConsole::argc; i++) + out << wxAppConsole::argv[i] << " "; + + header.SetInvocation(out.str()); + implementation.SetInvocation(out.str()); + + return true; +} + +/** @brief OnInitCmdLine + * + * @todo: document this function + */ +void Nin10KitApp::OnInitCmdLine(wxCmdLineParser& parser) +{ + VerboseLog("OnInitCmdLine"); + parser.SetLogo(wxString::Format(_("nin10kit version %s"), AutoVersion::FULLVERSION_STRING)); + parser.SetDesc(cmd_descriptions); + parser.SetSwitchChars (_("-")); +} + +/** @brief OnCmdLineParsed + * + * @todo: document this function + */ +bool Nin10KitApp::OnCmdLineParsed(wxCmdLineParser& parser) +{ + VerboseLog("OnCmdLineParsed"); + + CmdLineParserHelper parse(parser); + logger->SetLogLevel((LogLevel)parse.GetInt("log", 3, 0, 4)); + + // mode params + params.mode = ToUpper(parse.GetString("mode", "")); + params.device = ToUpper(parse.GetString("device", "GBA")); + params.bpp = parse.GetInt("bpp", 8); + + // Helpful options + params.output_dir = parse.GetString("output_dir"); + params.names = parse.GetListString("names"); + std::vector resizes = parse.GetListString("resize"); + params.transparent_color = parse.GetHexInt("transparent", -1); + params.dither = parse.GetSwitch("dither"); + params.dither_level = parse.GetInt("dither_level", 80, 0, 100) / 100.0f; + + params.offset = parse.GetInt("start", 0, 0, 255); + params.palette = parse.GetInt("palette", 256, 1, 256); + params.split = parse.GetSwitch("split"); + + params.split_sbb = parse.GetSwitch("split_sbb"); + params.tilesets = parse.GetListString("tileset"); + params.border = parse.GetInt("border", 0, 0); + params.force = parse.GetSwitch("force"); + + params.export_2d = parse.GetSwitch("export_2d"); + params.for_bitmap = parse.GetSwitch("for_bitmap"); + + params.weights = parse.GetListInt("weights", {25, 25, 25, 25}); + + std::string export_file = parser.GetParam(0).ToStdString(); + params.filename = params.output_dir.empty() ? export_file : params.output_dir + Chop(export_file); + params.symbol_base_name = Sanitize(export_file); + InfoLog("Exporting to %s symbol base name is %s", params.filename.c_str(), params.symbol_base_name.c_str()); + + // get unnamed parameters + std::set filenames; + for (unsigned int i = 1; i < parser.GetParamCount(); i++) + { + const std::string filename = parser.GetParam(i).ToStdString(); + if (filenames.find(filename) != filenames.end()) + FatalLog("Duplicate image filename given"); + params.files.push_back(filename); + filenames.insert(filename); + } + + if (params.mode.empty()) + FatalLog("No mode set."); + + if (params.files.empty()) + FatalLog("You must specify an output array/filename and a list of image files you want to export."); + + if (params.names.empty()) + { + for (const auto& file : params.files) + params.names.push_back(Format(file)); + } + + if (params.names.size() != params.files.size()) + FatalLog("Incorrect number of override names given %d != %d, this must be equal to the number of images passed in", params.names.size(), params.files.size()); + + if (!resizes.empty() && resizes.size() != params.files.size()) + FatalLog("Incorrect number of resizes given %d != %d, this must be equal to the number of images passed in", resizes.size(), params.files.size()); + + for (const auto& resize_str : resizes) + { + int32_t w = -1, h = -1; + if (!resize_str.empty()) + { + Scanner scan(resize_str, "x"); + if (!scan.Next(w)) + FatalLog("Invalid width given %s", resize_str.c_str()); + if (!scan.Next(h)) + FatalLog("Invalid height given %s", resize_str.c_str()); + } + params.resizes.push_back(resize(w, h)); + } + + if (params.weights.size() != 4) + FatalLog("Error parsing -weights expected 4 elements %d given", params.weights.size()); + int p = params.weights[0]; + int v = params.weights[1]; + int pv = params.weights[2]; + int error = params.weights[3]; + if (p < 0 || v < 0 || pv < 0 || error < 0 || (p+v+pv+error != 100)) + WarnLog("-weights total does not sum up to 100 or invalid value given."); + + if (!params.tilesets.empty()) + { + header.SetTilesets(params.tilesets); + implementation.SetTilesets(params.tilesets); + } + + if (params.mode == "tiles") + { + if (!params.resizes.empty()) + { + WarnLog("Ignoring -resize when in tileset export mode, please resize manually."); + params.resizes.clear(); + } + header.SetTilesets(params.files); + implementation.SetTilesets(params.files); + } + + return true; +} + +bool Nin10KitApp::DoExportImages() +{ + VerboseLog("DoLoadImages"); + std::map> file_images; + std::map> file_tilesets; + for (const auto& filename : params.files) + { + file_images[filename] = std::vector(); + readImages(&file_images[filename], filename); + } + for (const auto& tileset : params.tilesets) + { + file_tilesets[tileset] = std::vector(); + readImages(&file_tilesets[tileset], tileset); + } + + VerboseLog("DoHandleResize"); + for (unsigned int i = 0; i < params.resizes.size(); i++) + { + const std::string& filename = params.files[i]; + const resize& size = params.resizes[i]; + if (!size.IsValid()) continue; + Magick::Geometry geom(size.width, size.height); + geom.aspect(true); + for (auto& image : file_images[filename]) + image.resize(geom); + } + + VerboseLog("DoCheckAndLabelImages"); + for (unsigned int i = 0; i < params.files.size(); i++) + { + const std::string& filename = params.files[i]; + const std::vector images = file_images[filename]; + bool isAnim = images.size() > 1; + for (unsigned int j = 0; j < images.size(); j++) + { + const Magick::Image& image = images[j]; + header.AddImageInfo(filename, j, image.columns(), image.rows(), isAnim); + implementation.AddImageInfo(filename, j, image.columns(), image.rows(), isAnim); + } + } + + VerboseLog("Converting to Image32Bpp"); + for (unsigned int i = 0; i < params.files.size(); i++) + { + const std::string& filename = params.files[i]; + std::vector& images = file_images[filename]; + for (unsigned int j = 0; j < images.size(); j++) + { + const auto& image = images[j]; + params.images.push_back(Image32Bpp(image, params.names[i], filename, j, images.size() > 1)); + } + } + + for (unsigned int i = 0; i < params.tilesets.size(); i++) + { + const std::string& filename = params.tilesets[i]; + std::vector& images = file_tilesets[filename]; + for (unsigned int j = 0; j < images.size(); j++) + { + const auto& image = images[j]; + params.tileset_images.push_back(Image32Bpp(image, params.names[i], filename, j, images.size() > 1)); + } + } + + VerboseLog("DoExportImages"); + InfoLog("Using %s exporter mode %s", params.device.c_str(), params.mode.c_str()); + + header.SetMode(params.mode); + implementation.SetMode(params.mode); + + if (params.device == "GBA") + DoGBAExport(params.images, params.tileset_images); + else if (params.device == "DS") + DoDSExport(params.images, params.tileset_images); + else if (params.device == "3DS") + Do3DSExport(params.images, params.tileset_images); + + InfoLog("Export complete now writing files"); + // Write the files + std::ofstream file_c, file_h; + InitFiles(file_c, file_h, params.filename); + + header.Write(file_h); + implementation.Write(file_c); + + file_h.close(); + file_c.close(); + + return true; +} + +// Do cool things here +int Nin10KitApp::OnRun() +{ + VerboseLog("OnRun"); + try + { + VerboseLog("Init CPERCEP"); + cpercep_init(); + + if (!DoExportImages()) return EXIT_FAILURE; + + InfoLog("File exported successfully as %s.c and %s.h", params.filename.c_str(), params.filename.c_str()); + } + catch(Magick::Exception &error_) + { + FatalLog("Exception occurred!: %s\nPlease check the images you are trying to load into the program.", error_.what()); + } + catch (const std::exception& ex) + { + FatalLog("Exception occurred! Reason: %s", ex.what()); + } + catch (const std::string& ex) + { + FatalLog("Exception occurred! Reason: %s", ex.c_str()); + } + catch (const char* ex) + { + FatalLog("Exception occurred! Reason: %s", ex); + } + catch (...) + { + FatalLog("Uncaught exception occurred!"); + } + + VerboseLog("Done"); + return EXIT_SUCCESS; +} diff --git a/shared/Logger.cpp b/shared/Logger.cpp new file mode 100644 index 0000000..c59295e --- /dev/null +++ b/shared/Logger.cpp @@ -0,0 +1,80 @@ +#include "Logger.hpp" +#include +#include + +std::unique_ptr logger(new Logger()); + +void SetLogger(AbstractLogger* logobj) +{ + logger.reset(logobj); +} + +inline const char* GetLogAbbrev(LogLevel level) +{ + switch(level) + { + case LogLevel::FATAL: + return "F"; + case LogLevel::DEBUG: + return "D"; + case LogLevel::WARNING: + return "W"; + case LogLevel::INFO: + return "I"; + case LogLevel::VERBOSE: + return "V"; + default: + return "?"; + } +} + +inline const char* GetLogColor(LogLevel level) +{ + switch(level) + { + case LogLevel::FATAL: + return "\033[1;31m"; + case LogLevel::DEBUG: + return "\033[1;33m"; + case LogLevel::WARNING: + return "\033[1;33m"; + case LogLevel::INFO: + return ""; + case LogLevel::VERBOSE: + return "\033[2;36"; + default: + return ""; + } +} + +void AbstractLogger::Log(LogLevel level, const std::string& format, ...) +{ + va_list argptr; + va_start(argptr, format); + Log(level, format, argptr); + va_end(argptr); +} + +void AbstractLogger::Log(LogLevel level, const std::string& format, va_list ap) +{ + if (level > log_level) + return; + + timeval curTime; + gettimeofday(&curTime, NULL); + char buffer[128]; + strftime(buffer, 128, "%H:%M:%S", localtime(&curTime.tv_sec)); + char currentTime[128] = ""; + snprintf(currentTime, 128, "%s:%ld", buffer, curTime.tv_usec); + (*out) << GetLogColor(level) << GetLogAbbrev(level) << "[" << currentTime << "] "; + + DoLog(level, format, ap); +} + +void Logger::DoLog(LogLevel level, const std::string& format, va_list ap) +{ + char buffer[1024]; + vsnprintf(buffer, 1024, format.c_str(), ap); + (*out) << buffer << "\n"; + if (level == LogLevel::FATAL) exit(EXIT_FAILURE); +} diff --git a/shared/Logger.hpp b/shared/Logger.hpp new file mode 100644 index 0000000..02bc7de --- /dev/null +++ b/shared/Logger.hpp @@ -0,0 +1,97 @@ +#ifndef LOGGER_HPP +#define LOGGER_HPP + +#include +#include +#include + +enum class LogLevel +{ + FATAL = 0, // printed and stops program + DEBUG = 1, + WARNING = 2, + INFO = 3, + VERBOSE = 4, +}; + +class AbstractLogger +{ + public: + AbstractLogger(std::ostream* target = &std::cerr) : out(target), log_level(LogLevel::INFO) {} + virtual ~AbstractLogger() {} + void Log(LogLevel level, const std::string& format, ...); + void Log(LogLevel level, const std::string& format, va_list ap); + virtual void DoLog(LogLevel level, const std::string& format, va_list ap) {} + void SetLogTarget(std::ostream* stream) {out = stream;} + void SetLogLevel(LogLevel level) {log_level = level;} + protected: + std::ostream* out; + private: + LogLevel log_level; +}; + +class Logger : public AbstractLogger +{ + public: + virtual void DoLog(LogLevel level, const std::string& format, va_list ap); +}; + + +extern std::unique_ptr logger; + +void SetLogger(AbstractLogger* logobj); + +static inline void Log(LogLevel level, const std::string& format, ...) +{ + va_list argptr; + va_start(argptr, format); + logger->Log(level, format, argptr); + va_end(argptr); +} + +static inline void Log(LogLevel level, const std::string& format, va_list arg) +{ + logger->Log(level, format, arg); +} + +static inline void FatalLog(const std::string& format, ...) +{ + va_list argptr; + va_start(argptr, format); + Log(LogLevel::FATAL, format, argptr); + va_end(argptr); +} + +static inline void DebugLog(const std::string& format, ...) +{ + va_list argptr; + va_start(argptr, format); + Log(LogLevel::DEBUG, format, argptr); + va_end(argptr); +} + +static inline void WarnLog(const std::string& format, ...) +{ + va_list argptr; + va_start(argptr, format); + Log(LogLevel::WARNING, format, argptr); + va_end(argptr); +} + +static inline void InfoLog(const std::string& format, ...) +{ + va_list argptr; + va_start(argptr, format); + Log(LogLevel::INFO, format, argptr); + va_end(argptr); +} + +static inline void VerboseLog(const std::string& format, ...) +{ + va_list argptr; + va_start(argptr, format); + Log(LogLevel::VERBOSE, format, argptr); + va_end(argptr); +} + +#endif diff --git a/shared/Scanner.cpp b/shared/Scanner.cpp new file mode 100644 index 0000000..654bcad --- /dev/null +++ b/shared/Scanner.cpp @@ -0,0 +1,61 @@ +#include "Scanner.hpp" +#include "Logger.hpp" + +bool Scanner::Next(int32_t& var, int base) +{ + if (!HasMoreTokens()) return false; + + wxString token = GetNextToken(); + VerboseLog("Read int %s base %d", static_cast(token.c_str()), base); + long ret; + if (!token.ToLong(&ret, base)) return false; + + var = (int32_t) ret; + return true; +} + +bool Scanner::Next(uint32_t& var, int base) +{ + if (!HasMoreTokens()) return false; + + wxString token = GetNextToken(); + VerboseLog("Read unsigned int %s base %d", static_cast(token.c_str()), base); + unsigned long ret; + if (!token.ToULong(&ret, base)) return false; + + var = (uint32_t) ret; + return true; +} + +bool Scanner::Next(float& var) +{ + if (!HasMoreTokens()) return false; + + wxString token = GetNextToken(); + VerboseLog("Read float %s", static_cast(token.c_str())); + double ret; + if (!token.ToDouble(&ret)) return false; + + var = (float) ret; + + return true; +} + +bool Scanner::Next(std::string& var) +{ + if (!HasMoreTokens()) return false; + + var = GetNextToken(); + VerboseLog("Read string %s", static_cast(var.c_str())); + return true; +} + +bool Scanner::NextLine(std::string& var) +{ + if (!HasMoreTokens()) return false; + + var = GetString(); + VerboseLog("Read line %s", static_cast(var.c_str())); + return true; +} + diff --git a/shared/Scanner.hpp b/shared/Scanner.hpp new file mode 100644 index 0000000..9c1103e --- /dev/null +++ b/shared/Scanner.hpp @@ -0,0 +1,18 @@ +#ifndef SCANNER_HPP +#define SCANNER_HPP + +#include + +class Scanner : public wxStringTokenizer +{ + public: + Scanner(const std::string& str, const std::string& delims = " ,\t\r\n") : wxStringTokenizer(str, delims, wxTOKEN_STRTOK) {} + ~Scanner() {} + bool Next(int32_t& var, int base = 10); + bool Next(uint32_t& var, int base = 10); + bool Next(float& var); + bool Next(std::string& var); + bool NextLine(std::string& var); +}; + +#endif diff --git a/shared/color.cpp b/shared/color.cpp new file mode 100644 index 0000000..8cdbb79 --- /dev/null +++ b/shared/color.cpp @@ -0,0 +1,58 @@ +#include "color.hpp" + +#include + +#include "cpercep.hpp" + +Color::Color(unsigned short color_data) +{ + Set(color_data); +} + +bool Color::operator<(const Color& right) const +{ + bool less; + less = x < right.x; + if (x == right.x) less = y < right.y; + if (x == right.x && y == right.y) less = z < right.z; + return less; +} + +void Color::Set(int a, int b, int c) +{ + x = a; + y = b; + z = c; +} + +void Color::Set(unsigned short color_data) +{ + x = color_data & 0x1f; + y = (color_data >> 5) & 0x1f; + z = (color_data >> 10) & 0x1f; +} + +void Color::Get(int& a, int& b, int& c) const +{ + a = x; + b = y; + c = z; +} + +/** Distance in perception. + */ +double Color::Distance(const Color& other) const +{ + double ox, oy, oz; + double l, a, b, ol, oa, ob; + + ox = other.x; + oy = other.y; + oz = other.z; + + cpercep_rgb_to_space(x * 255.0 / 31, y * 255.0 / 31, z * 255.0 / 31, &l, &a, &b); + cpercep_rgb_to_space(ox * 255.0 / 31, oy * 255.0 / 31, oz * 255.0 / 31, &ol, &oa, &ob); + + return cpercep_distance_space(l, a, b, ol, oa, ob); + +} diff --git a/shared/color.hpp b/shared/color.hpp new file mode 100644 index 0000000..a6803c3 --- /dev/null +++ b/shared/color.hpp @@ -0,0 +1,19 @@ +#ifndef COLOR_HPP +#define COLOR_HPP + +///TODO rewrite this entire class. +class Color +{ + public: + Color() : x(0), y(0), z(0) {}; + Color(double a, double b, double c) : x(a), y(b), z(c) {}; + explicit Color(unsigned short color_data); + bool operator<(const Color& rhs) const; + void Set(int a, int b, int c); + void Set(unsigned short color_data); + void Get(int& a, int& b, int& c) const; + double Distance(const Color& other) const; + double x, y, z; +}; + +#endif diff --git a/shared/cpercep.cpp b/shared/cpercep.cpp new file mode 100644 index 0000000..548c382 --- /dev/null +++ b/shared/cpercep.cpp @@ -0,0 +1,628 @@ +/* +Copyright (C) 1999-2002 Adam D. Moss (the "Author"). All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is fur- +nished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FIT- +NESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CON- +NECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of the Author of the +Software shall not be used in advertising or otherwise to promote the sale, +use or other dealings in this Software without prior written authorization +from the Author. +*/ + +/* + cpercep.c: The CPercep Functions v0.9: 2002-02-10 + Adam D. Moss: adam@gimp.org + + This code module concerns itself with conversion from a hard-coded + RGB colour space (sRGB by default) to CIE L*a*b* and back again with + (primarily) precision and (secondarily) speed, oriented largely + towards the purposes of quantifying the PERCEPTUAL difference between + two arbitrary RGB colours with a minimum of fuss. + + Motivation One: The author is disheartened at the amount of graphics + processing software around which uses weighted or non-weighted + Euclidean distance between co-ordinates within a (poorly-defined) RGB + space as the basis of what should really be an estimate of perceptual + difference to the human eye. Certainly it's fast to do it that way, + but please think carefully about whether your particular application + should be tolerating sloppy results for the sake of real-time response. + + Motivation Two: Lack of tested, re-usable and free code available + for this purpose. The difficulty in finding something similar to + CPercep with a free license motivated this project; I hope that this + code also serves to illustrate how to perform the + R'G'B'->XYZ->L*a*b*->XYZ->R'G'B' transformation correctly since I + was distressed to note how many of the equations and code snippets + on the net were omitting the reverse transform and/or were using + incorrectly-derived or just plain wrong constants. + + TODO: document functions, rename erroneously-named arguments +*/ +#include "cpercep.hpp" + +#include +#include + + +/* defines: + + SANITY: emits warnings when passed non-sane colours (and usually + corrects them) -- useful when debugging. + + APPROX: speeds up the conversion from RGB to the colourspace by + assuming that the RGB values passed in are integral and definitely + in the range 0->255 + + SRGB: assumes that the RGB values being passed in (and out) are + destined for an sRGB-alike display device (a typical modern monitor) + -- if you change this then you'll probably want to change ASSUMED_GAMMA, + the phosphor colours and the white point definition. +*/ + +//#define SANITY +#define APPROX +#define SRGB + + +#ifdef SRGB +#define ASSUMED_GAMMA (2.2F) +#else +/*#define ASSUMED_GAMMA (2.591F)*/ +#define ASSUMED_GAMMA (2.2F) +#endif + +#define REV_GAMMA ((1.0F / ASSUMED_GAMMA)) + + +/* define characteristics of the source RGB space (and the space + within which we try to behave linearly). */ + +/* Phosphor colours: */ + +/* sRGB/HDTV phosphor colours */ +static const double pxr = 0.64F; +static const double pyr = 0.33F; +static const double pxg = 0.30F; +static const double pyg = 0.60F; +static const double pxb = 0.15F; +static const double pyb = 0.06F; + +/* White point: */ + +/* D65 (6500K) (recommended but not a common display default) */ +static const double lxn = 0.312713F; +static const double lyn = 0.329016F; + +/* D50 (5000K) */ +/*static const double lxn = 0.3457F; */ +/*static const double lyn = 0.3585F; */ + +/* D55 (5500K) */ +/*static const double lxn = 0.3324F; */ +/*static const double lyn = 0.3474F; */ + +/* D93 (9300K) (a common monitor default, but poor colour reproduction) */ +/* static const double lxn = 0.2848F; */ +/* static const double lyn = 0.2932F; */ + +/* illum E (normalized) */ +/*static const double lxn = 1.0/3.0F; */ +/*static const double lyn = 1.0/3.0F; */ + +/* illum C (average sunlight) */ +/*static const double lxn = 0.3101F; */ +/*static const double lyn = 0.3162F; */ + +/* illum B (direct sunlight) */ +/*static const double lxn = 0.3484F; */ +/*static const double lyn = 0.3516F; */ + +/* illum A (tungsten lamp) */ +/*static const double lxn = 0.4476F; */ +/*static const double lyn = 0.4074F; */ + + +static const double LRAMP = 7.99959199F; + + +static double xnn, znn; + +static double powtable[256]; + + +#ifndef CLAMP +#define CLAMP(x,l,u) ((x)<(l)?(l):((x)>(u)?(u):(x))) +#endif + + +static void +init_powtable(const double gamma) +{ + int i; + +#ifndef SRGB + /* pure gamma function */ + for (i=0; i<256; i++) + { + powtable[i] = pow((i)/255.0F, gamma); + } +#else + /* sRGB gamma curve */ + for (i=0; i<11 /* 0.03928 * 255 */; i++) + { + powtable[i] = (i) / (255.0F * 12.92F); + } + for (; i<256; i++) + { + powtable[i] = pow( (((i) / 255.0F) + 0.055F) / 1.055F, 2.4F); + } +#endif +} + + +typedef double CMatrix[3][3]; +typedef double CVector[3]; + +static CMatrix Mrgb_to_xyz, Mxyz_to_rgb; + +static int +Minvert (CMatrix src, CMatrix dest) +{ + double det; + + dest[0][0] = src[1][1] * src[2][2] - src[1][2] * src[2][1]; + dest[0][1] = src[0][2] * src[2][1] - src[0][1] * src[2][2]; + dest[0][2] = src[0][1] * src[1][2] - src[0][2] * src[1][1]; + dest[1][0] = src[1][2] * src[2][0] - src[1][0] * src[2][2]; + dest[1][1] = src[0][0] * src[2][2] - src[0][2] * src[2][0]; + dest[1][2] = src[0][2] * src[1][0] - src[0][0] * src[1][2]; + dest[2][0] = src[1][0] * src[2][1] - src[1][1] * src[2][0]; + dest[2][1] = src[0][1] * src[2][0] - src[0][0] * src[2][1]; + dest[2][2] = src[0][0] * src[1][1] - src[0][1] * src[1][0]; + + det = + src[0][0] * dest[0][0] + + src[0][1] * dest[1][0] + + src[0][2] * dest[2][0]; + + if (det <= 0.0F) + { +#ifdef SANITY + printf("\n\007 XXXX det: %f\n", det); +#endif + return 0; + } + + dest[0][0] /= det; + dest[0][1] /= det; + dest[0][2] /= det; + dest[1][0] /= det; + dest[1][1] /= det; + dest[1][2] /= det; + dest[2][0] /= det; + dest[2][1] /= det; + dest[2][2] /= det; + + return 1; +} + + +static void +rgbxyzrgb_init(void) +{ + init_powtable (ASSUMED_GAMMA); + + xnn = lxn / lyn; + /* ynn taken as 1.0 */ + znn = (1.0F - (lxn + lyn)) / lyn; + + { + CMatrix MRC, MRCi; + double C1,C2,C3; + + MRC[0][0] = pxr; + MRC[0][1] = pxg; + MRC[0][2] = pxb; + MRC[1][0] = pyr; + MRC[1][1] = pyg; + MRC[1][2] = pyb; + MRC[2][0] = 1.0F - (pxr + pyr); + MRC[2][1] = 1.0F - (pxg + pyg); + MRC[2][2] = 1.0F - (pxb + pyb); + + Minvert (MRC, MRCi); + + C1 = MRCi[0][0]*xnn + MRCi[0][1] + MRCi[0][2]*znn; + C2 = MRCi[1][0]*xnn + MRCi[1][1] + MRCi[1][2]*znn; + C3 = MRCi[2][0]*xnn + MRCi[2][1] + MRCi[2][2]*znn; + + Mrgb_to_xyz[0][0] = MRC[0][0] * C1; + Mrgb_to_xyz[0][1] = MRC[0][1] * C2; + Mrgb_to_xyz[0][2] = MRC[0][2] * C3; + Mrgb_to_xyz[1][0] = MRC[1][0] * C1; + Mrgb_to_xyz[1][1] = MRC[1][1] * C2; + Mrgb_to_xyz[1][2] = MRC[1][2] * C3; + Mrgb_to_xyz[2][0] = MRC[2][0] * C1; + Mrgb_to_xyz[2][1] = MRC[2][1] * C2; + Mrgb_to_xyz[2][2] = MRC[2][2] * C3; + + Minvert (Mrgb_to_xyz, Mxyz_to_rgb); + } +} + + +static void +xyz_to_rgb (double *inx_outr, + double *iny_outg, + double *inz_outb) +{ + const double x = *inx_outr; + const double y = *iny_outg; + const double z = *inz_outb; + + *inx_outr = Mxyz_to_rgb[0][0]*x + Mxyz_to_rgb[0][1]*y + Mxyz_to_rgb[0][2]*z; + *iny_outg = Mxyz_to_rgb[1][0]*x + Mxyz_to_rgb[1][1]*y + Mxyz_to_rgb[1][2]*z; + *inz_outb = Mxyz_to_rgb[2][0]*x + Mxyz_to_rgb[2][1]*y + Mxyz_to_rgb[2][2]*z; +} + + +static void +rgb_to_xyz (double *inr_outx, + double *ing_outy, + double *inb_outz) +{ + const double r = *inr_outx; + const double g = *ing_outy; + const double b = *inb_outz; + + *inr_outx = Mrgb_to_xyz[0][0]*r + Mrgb_to_xyz[0][1]*g + Mrgb_to_xyz[0][2]*b; + *ing_outy = Mrgb_to_xyz[1][0]*r + Mrgb_to_xyz[1][1]*g + Mrgb_to_xyz[1][2]*b; + *inb_outz = Mrgb_to_xyz[2][0]*r + Mrgb_to_xyz[2][1]*g + Mrgb_to_xyz[2][2]*b; +} + + +static inline double +ffunc(const double t) +{ + if (t > 0.008856F) + { + return (cbrt(t)); + } + else + { + return (7.787F * t + 16.0F/116.0F); + } +} + + +static inline double +ffunc_inv(const double t) +{ + if (t > 0.206893F) + { + return (t * t * t); + } + else + { + return ((t - 16.0F/116.0F) / 7.787F); + } +} + + +static void +xyz_to_lab (double *inx, + double *iny, + double *inz) +{ + double L,a,b; + double ffuncY; + const double X = *inx; + const double Y = *iny; + const double Z = *inz; + + if (Y > 0.0F) + { + if (Y > 0.008856F) + { + L = (116.0F * cbrt(Y)) - 16.0F; + } + else + { + L = (Y * 903.3F); + } + +#ifdef SANITY + if (L < 0.0F) + { + printf(" %f \007",(float)L); + } + + if (L > 100.0F) + { + printf(" %f \007",(float)L); + } +#endif + } + else + { + L = 0.0; + } + + ffuncY = ffunc(Y); + a = 500.0F * (ffunc(X/xnn) - ffuncY); + b = 200.0F * (ffuncY - ffunc(Z/znn)); + + *inx = L; + *iny = a; + *inz = b; +} + + +static void +lab_to_xyz (double *inl, + double *ina, + double *inb) +{ + double X,Y,Z; + double P; + const double L = *inl; + const double a = *ina; + const double b = *inb; + + if (L > LRAMP) + { + P = Y = (L + 16.0F) / 116.0F; + Y = Y * Y * Y; + } + else + { + Y = L / 903.3F; + P = 7.787F * Y + 16.0F/116.0F; + } + + X = (P + a / 500.0F); + X = xnn * ffunc_inv(X); + Z = (P - b / 200.0F); + Z = znn * ffunc_inv(Z); + +#ifdef SANITY + if (X<-0.00000F) + { + if (X<-0.0001F) + printf("{badX %f {%f,%f,%f}}",X,L,a,b); + X = 0.0F; + } + if (Y<-0.00000F) + { + if (Y<-0.0001F) + printf("{badY %f}",Y); + Y = 0.0F; + } + if (Z<-0.00000F) + { + if (Z<-0.1F) + printf("{badZ %f}",Z); + Z = 0.0F; + } +#endif + + *inl = X; + *ina = Y; + *inb = Z; +} + + + +/* call this before using the CPercep function */ +void +cpercep_init (void) +{ + static bool initialized = false; + + if (! initialized) + { + rgbxyzrgb_init(); + initialized = true; + } +} + +void +cpercep_rgb_to_space (double inr, + double ing, + double inb, + double *outr, + double *outg, + double *outb) +{ +#ifdef APPROX +#ifdef SANITY + /* ADM extra sanity */ + if ((inr) > 255.0F || + (ing) > 255.0F || + (inb) > 255.0F || + (inr) < -0.0F || + (ing) < -0.0F || + (inb) < -0.0F + ) + //abort(); +#endif /* SANITY */ + inr = powtable[(int)inr]; + ing = powtable[(int)ing]; + inb = powtable[(int)inb]; +#else +#ifdef SRGB + /* sRGB gamma curve */ + if (inr <= (0.03928F * 255.0F)) + inr = inr / (255.0F * 12.92F); + else + inr = pow( (inr + (0.055F * 255.0F)) / (1.055F * 255.0F), 2.4F); + + if (ing <= (0.03928F * 255.0F)) + ing = ing / (255.0F * 12.92F); + else + ing = pow( (ing + (0.055F * 255.0F)) / (1.055F * 255.0F), 2.4F); + + if (inb <= (0.03928F * 255.0F)) + inb = inb / (255.0F * 12.92F); + else + inb = pow( (inb + (0.055F * 255.0F)) / (1.055F * 255.0F), 2.4F); +#else + /* pure gamma function */ + inr = pow((inr)/255.0F, ASSUMED_GAMMA); + ing = pow((ing)/255.0F, ASSUMED_GAMMA); + inb = pow((inb)/255.0F, ASSUMED_GAMMA); +#endif /* SRGB */ +#endif /* APPROX */ + +#ifdef SANITY + /* ADM extra sanity */ + if ((inr) > 1.0F || + (ing) > 1.0F || + (inb) > 1.0F || + (inr) < 0.0F || + (ing) < 0.0F || + (inb) < 0.0F + ) + { + printf("%%"); + /* abort(); */ + } +#endif /* SANITY */ + + rgb_to_xyz(&inr, &ing, &inb); + +#ifdef SANITY + if (inr < 0.0F || ing < 0.0F || inb < 0.0F) + { + printf(" [BAD2 XYZ: %f,%f,%f]\007 ", + inr,ing,inb); + } +#endif /* SANITY */ + + xyz_to_lab(&inr, &ing, &inb); + + *outr = inr; + *outg = ing; + *outb = inb; +} + + +void +cpercep_space_to_rgb (double inr, + double ing, + double inb, + double *outr, + double *outg, + double *outb) +{ + lab_to_xyz(&inr, &ing, &inb); + +#ifdef SANITY + if (inr<-0.0F || ing<-0.0F || inb<-0.0F) + { + printf(" [BAD1 XYZ: %f,%f,%f]\007 ", + inr,ing,inb); + } +#endif + + xyz_to_rgb(&inr, &ing, &inb); + + /* yes, essential. :( */ + inr = CLAMP(inr,0.0F,1.0F); + ing = CLAMP(ing,0.0F,1.0F); + inb = CLAMP(inb,0.0F,1.0F); + +#ifdef SRGB + if (inr <= 0.0030402477F) + inr = inr * (12.92F * 255.0F); + else + inr = pow(inr, 1.0F/2.4F) * (1.055F * 255.0F) - (0.055F * 255.0F); + + if (ing <= 0.0030402477F) + ing = ing * (12.92F * 255.0F); + else + ing = pow(ing, 1.0F/2.4F) * (1.055F * 255.0F) - (0.055F * 255.0F); + + if (inb <= 0.0030402477F) + inb = inb * (12.92F * 255.0F); + else + inb = pow(inb, 1.0F/2.4F) * (1.055F * 255.0F) - (0.055F * 255.0F); +#else + inr = 255.0F * pow(inr, REV_GAMMA); + ing = 255.0F * pow(ing, REV_GAMMA); + inb = 255.0F * pow(inb, REV_GAMMA); +#endif + + *outr = inr; + *outg = ing; + *outb = inb; +} + + +#if 0 +/* EXPERIMENTAL SECTION */ + +const double +xscaler(const double start, const double end, + const double me, const double him) +{ + return start + ((end-start) * him) / (me + him); +} + + +void +mix_colours (const double L1, const double a1, const double b1, + const double L2, const double a2, const double b2, + double *rtnL, double *rtna, double *rtnb, + double mass1, double mass2) +{ + double w1, w2; + +#if 0 + *rtnL = xscaler (L1, L2, mass1, mass2); + *rtna = xscaler (a1, a2, mass1, mass2); + *rtnb = xscaler (b1, b2, mass1, mass2); +#else + +#if 1 + w1 = mass1 * L1; + w2 = mass2 * L2; +#else + w1 = mass1 * (L1*L1*L1); + w2 = mass2 * (L2*L2*L2); +#endif + + *rtnL = xscaler (L1, L2, mass1, mass2); + + if (w1 <= 0.0 && + w2 <= 0.0) + { + *rtna = + *rtnb = 0.0; +#ifdef SANITY + /* printf("\007OUCH. "); */ +#endif + } + else + { + *rtna = xscaler(a1, a2, w1, w2); + *rtnb = xscaler(b1, b2, w1, w2); + } +#endif +} +#endif /* EXPERIMENTAL SECTION */ diff --git a/shared/cpercep.hpp b/shared/cpercep.hpp new file mode 100644 index 0000000..2a69a24 --- /dev/null +++ b/shared/cpercep.hpp @@ -0,0 +1,69 @@ +/* +Copyright (C) 1997-2002 Adam D. Moss (the "Author"). All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is fur- +nished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FIT- +NESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CON- +NECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of the Author of the +Software shall not be used in advertising or otherwise to promote the sale, +use or other dealings in this Software without prior written authorization +from the Author. +*/ + +/* + cpercep.c: The CPercep Functions v0.9: 2002-02-10 + Adam D. Moss: adam@gimp.org + + TODO: document functions, rename erroneously-named arguments +*/ + +#ifndef __CPERCEP_H__ +#define __CPERCEP_H__ + + +void cpercep_init (void); + +void cpercep_rgb_to_space (double inr, + double ing, + double inb, + double *outr, + double *outg, + double *outb); + +void cpercep_space_to_rgb (double inr, + double ing, + double inb, + double *outr, + double *outg, + double *outb); + + +/* This is in the header so that it can potentially be inlined. */ +static inline const double +cpercep_distance_space (const double L1, const double a1, const double b1, + const double L2, const double a2, const double b2) +{ + const double Ld = L1 - L2; + const double ad = a1 - a2; + const double bd = b1 - b2; + + return (Ld*Ld + ad*ad + bd*bd); +} + + +#endif /* __CPERCEP_H__ */ + diff --git a/shared/dither.cpp b/shared/dither.cpp new file mode 100644 index 0000000..a76467a --- /dev/null +++ b/shared/dither.cpp @@ -0,0 +1,165 @@ +#include "dither.hpp" + +#include +#include + +#ifndef CLAMP +#define CLAMP(x) (((x) < 0.0) ? 0.0 : (((x) > 31) ? 31 : (x))) +#endif + +struct DitherImage +{ + DitherImage(const Image16Bpp& _inImage, Image8Bpp& _outImage, unsigned short _transparent, int _dither, float _ditherlevel) : + inImage(_inImage), outImage(_outImage), transparent(_transparent), x(0), y(0), dither(_dither), ditherlevel(_ditherlevel) {}; + const Image16Bpp& inImage; + Image8Bpp& outImage; + unsigned short transparent; + unsigned int x, y; + int dither; + float ditherlevel; +}; + +enum +{ + NONE, + UP, + LEFT, + DOWN, + RIGHT, +}; + +int Dither(const unsigned short& data, std::shared_ptr palette, unsigned short transparent, int dither, float ditherlevel) +{ + static int ex = 0, ey = 0, ez = 0; + if (data == transparent) return 0; + + Color color(data); + Color newColor(CLAMP(color.x + ex), CLAMP(color.y + ey), CLAMP(color.z + ez)); + int index = palette->Search(newColor); + newColor = palette->At(index); + + if (dither) + { + ex += color.x - newColor.x; + ey += color.y - newColor.y; + ez += color.z - newColor.z; + ex *= ditherlevel; + ey *= ditherlevel; + ez *= ditherlevel; + } + + return index; +} + +static void move(DitherImage& dither, int direction) +{ + const Image16Bpp& image = dither.inImage; + Image8Bpp& indexedImage = dither.outImage; + int x = dither.x; + int y = dither.y; + int width = indexedImage.width; + int height = indexedImage.height; + /* dither the current pixel */ + if (x >= 0 && x < width && y >= 0 && y < height) + { + int index = Dither(image.pixels[x + y * width], dither.outImage.palette, dither.transparent, dither.dither, dither.ditherlevel); + indexedImage.pixels[x + y * width] = index; + } + + /* move to the next pixel */ + switch (direction) + { + case LEFT: + dither.x -= 1; + break; + case RIGHT: + dither.x += 1; + break; + case UP: + dither.y -= 1; + break; + case DOWN: + dither.y += 1; + break; + } +} + +void Hilbert(DitherImage& dither, int level, int direction) +{ + if (level == 1) + { + switch (direction) + { + case LEFT: + move(dither, RIGHT); + move(dither, DOWN); + move(dither, LEFT); + break; + case RIGHT: + move(dither, LEFT); + move(dither, UP); + move(dither, RIGHT); + break; + case UP: + move(dither, DOWN); + move(dither, RIGHT); + move(dither, UP); + break; + case DOWN: + move(dither, UP); + move(dither, LEFT); + move(dither, DOWN); + break; + } + } + else + { + switch (direction) + { + case LEFT: + Hilbert(dither, level - 1, UP); + move(dither, RIGHT); + Hilbert(dither, level - 1, LEFT); + move(dither, DOWN); + Hilbert(dither, level - 1, LEFT); + move(dither, LEFT); + Hilbert(dither, level - 1, DOWN); + break; + case RIGHT: + Hilbert(dither, level - 1, DOWN); + move(dither, LEFT); + Hilbert(dither, level - 1, RIGHT); + move(dither, UP); + Hilbert(dither, level - 1, RIGHT); + move(dither, RIGHT); + Hilbert(dither, level - 1, UP); + break; + case UP: + Hilbert(dither, level - 1, LEFT); + move(dither, DOWN); + Hilbert(dither, level - 1, UP); + move(dither, RIGHT); + Hilbert(dither, level - 1, UP); + move(dither, UP); + Hilbert(dither, level - 1, RIGHT); + break; + case DOWN: + Hilbert(dither, level - 1, RIGHT); + move(dither, UP); + Hilbert(dither, level - 1, DOWN); + move(dither, LEFT); + Hilbert(dither, level - 1, DOWN); + move(dither, DOWN); + Hilbert(dither, level - 1, LEFT); + break; + } + } +} + +void RiemersmaDither(const Image16Bpp& inImage, Image8Bpp& outImage, unsigned short transparent, int dither, float ditherlevel) +{ + DitherImage dimage(inImage, outImage, transparent, dither, ditherlevel); + int size = ceil(log2(std::max(inImage.width, inImage.height))); + if (size > 0) Hilbert(dimage, size, UP); + move(dimage, NONE); +} diff --git a/shared/dither.hpp b/shared/dither.hpp new file mode 100644 index 0000000..be3704e --- /dev/null +++ b/shared/dither.hpp @@ -0,0 +1,13 @@ +#ifndef DITHER_HPP +#define DITHER_HPP + +#include +#include +#include + +#include "color.hpp" +#include "reductionhelper.hpp" + +void RiemersmaDither(const Image16Bpp& inImage, Image8Bpp& outimage, unsigned short transparent, int dither, float ditherlevel); + +#endif diff --git a/shared/exportfile.cpp b/shared/exportfile.cpp new file mode 100644 index 0000000..0c378c3 --- /dev/null +++ b/shared/exportfile.cpp @@ -0,0 +1,82 @@ +#include "exportfile.hpp" + +#include +#include +#include + +#include "shared.hpp" +#include "version.h" + +/** @brief Write + * + * @todo: document this function + */ +void ExportFile::Write(std::ostream& file) +{ + char str[1024]; + time_t aclock; + struct tm *newtime; + + time(&aclock); + newtime = localtime( &aclock ); + strftime(str, 96, "%A %m/%d/%Y, %H:%M:%S", newtime); + + file << "/*\n"; + file << " * Exported with nin10kit v" << AutoVersion::MAJOR << "." << AutoVersion::MINOR << "\n"; + if (!invocation.empty()) + file << " * Invocation command was nin10kit " << invocation << "\n"; + file << " * Time-stamp: " << str << "\n"; + file << " * \n"; + file << " * Image Information\n"; + file << " * -----------------\n"; + for (unsigned int i = 0; i < imageInfos.size() ; i++) + file << " * " << imageInfos[i] << "\n"; + if (!tilesets.empty()) + { + file << " * \n"; + file << " * Using tilesets\n"; + file << " * --------------\n"; + for (unsigned int i = 0; i < tilesets.size(); i++) + { + file << " * " << tilesets[i] << "\n"; + } + } + if (params.transparent_color != -1) + file << " * Transparent color: 0x" << std::hex << transparent_color << std::dec << "\n"; + file << " * \n"; + file << " * Quote/Fortune of the Day!\n"; + file << " * -------------------------\n"; + file << " * \n"; + file << " * All bug reports / feature requests are to be sent to Brandon (bwhitehead0308@gmail.com)\n"; + for (unsigned int i = 0; i < lines.size() ; i++) + file << " * " << lines[i] << "\n"; + file << " */\n\n"; +} + +/** @brief AddLine + * + * @todo: document this function + */ +void ExportFile::AddLine(const std::string& line) +{ + lines.push_back(line); +} + +/** @brief AddImageInfo + * + * @todo: document this function + */ +void ExportFile::AddImageInfo(const std::string& filename, int scene, int width, int height, bool frame) +{ + char buffer[1024]; + if (frame) + snprintf(buffer, 1024, "%s (frame %d) %d@%d", filename.c_str(), scene, width, height); + else + snprintf(buffer, 1024, "%s %d@%d", filename.c_str(), width, height); + imageInfos.push_back(buffer); +} + +void ExportFile::Add(std::shared_ptr& image) +{ + exportables.push_back(image); +} diff --git a/shared/exportfile.hpp b/shared/exportfile.hpp new file mode 100644 index 0000000..bb0ed31 --- /dev/null +++ b/shared/exportfile.hpp @@ -0,0 +1,41 @@ +#ifndef EXPORT_FILE_HPP +#define EXPORT_FILE_HPP + +#include +#include +#include +#include +#include + +#include "reductionhelper.hpp" + +class ExportFile +{ + public: + ExportFile(const std::string& _invocation = "") : invocation(_invocation), transparent_color(-1), mode("3") {}; + virtual ~ExportFile() {}; + + void SetInvocation(const std::string& invo) {invocation = invo;}; + void SetTransparent(int color) {transparent_color = color;}; + void SetMode(const std::string& _mode) {mode = _mode;}; + void SetTilesets(const std::vector& _tilesets) {tilesets = _tilesets;}; + + void AddLine(const std::string& line); + void AddImageInfo(const std::string& filename, int scene, int width, int height, bool frame); + void Add(std::shared_ptr& image); + + virtual void Write(std::ostream& file); + + private: + std::string invocation; + std::vector lines; + std::vector imageInfos; + std::vector tilesets; + + protected: + int transparent_color; + std::string mode; + std::vector> exportables; +}; + +#endif diff --git a/shared/fileutils.cpp b/shared/fileutils.cpp new file mode 100644 index 0000000..a16de26 --- /dev/null +++ b/shared/fileutils.cpp @@ -0,0 +1,174 @@ +#include "fileutils.hpp" + +#include +#include + +#include "shared.hpp" + +void InitFiles(std::ofstream& file_c, std::ofstream& file_h, const std::string& name) +{ + VerboseLog("Init Files"); + std::string filename_c = name + ".c"; + std::string filename_h = name + ".h"; + + file_c.open(filename_c.c_str()); + file_h.open(filename_h.c_str()); + + if (!file_c.good() || !file_h.good()) + FatalLog("Could not open files for writing"); +} + +void WriteShortArray(std::ostream& file, const std::string& name, const std::string& append, const std::vector& data, unsigned int items_per_row) +{ + VerboseLog("Writing short array %s%s size %zd", name.c_str(), append.c_str(), data.size()); + char buffer[7]; + file << "const unsigned short " << name << append << "[" << data.size() << "] =\n{\n\t"; + for (unsigned int i = 0; i < data.size(); i++) + { + snprintf(buffer, 7, "0x%04x", data[i]); + WriteElement(file, buffer, data.size(), i, items_per_row); + } + file << "\n};\n"; +} + +void WriteShortArray(std::ostream& file, const std::string& name, const std::string& append, const std::vector& data, unsigned int items_per_row) +{ + VerboseLog("Writing short array (from char) %s%s size %zd", name.c_str(), append.c_str(), data.size() / 2); + char buffer[7]; + unsigned int size = data.size() / 2; + file << "const unsigned short " << name << append << "[" << size << "] =\n{\n\t"; + for (unsigned int i = 0; i < size; i++) + { + snprintf(buffer, 7, "0x%02x%02x", data[2 * i + 1], data[2 * i]); + WriteElement(file, buffer, size, i, items_per_row); + } + file << "\n};\n"; +} + +void WriteShortArray4Bit(std::ostream& file, const std::string& name, const std::string& append, const std::vector& data, unsigned int items_per_row) +{ + VerboseLog("Writing short array (from 4 bits) %s%s size %zd", name.c_str(), append.c_str(), data.size()); + char buffer[7]; + unsigned int size = data.size() / 4; + file << "const unsigned short " << name << append << "[" << size << "] =\n{\n\t"; + for (unsigned int i = 0; i < size; i++) + { + snprintf(buffer, 7, "0x%01x%01x%01x%01x", data[4 * i + 3], data[4 * i + 2], data[4 * i + 1], data[4 * i]); + WriteElement(file, buffer, size, i, items_per_row); + } + file << "\n};\n"; +} + +void WriteShortArray(std::ostream& file, const std::string& name, const std::string& append, const std::vector& data, unsigned int items_per_row) +{ + VerboseLog("Writing short array (from Colors) %s%s size %zd", name.c_str(), append.c_str(), data.size()); + char buffer[7]; + int x, y, z; + file << "const unsigned short " << name << append << "[" << data.size() << "] =\n{\n\t"; + for (unsigned int i = 0; i < data.size(); i++) + { + const Color& color = data[i]; + color.Get(x, y, z); + unsigned short data_read = x | (y << 5) | (z << 10); + snprintf(buffer, 7, "0x%04x", data_read); + WriteElement(file, buffer, data.size(), i, items_per_row); + } + file << "\n};\n"; +} + +void WriteCharArray(std::ostream& file, const std::string& name, const std::string& append, const std::vector& data, unsigned int items_per_row) +{ + VerboseLog("Writing char array %s%s size %zd", name.c_str(), append.c_str(), data.size()); + char buffer[5]; + file << "const unsigned char " << name << append << "[" << data.size() << "] =\n{\n\t"; + for (unsigned int i = 0; i < data.size(); i++) + { + snprintf(buffer, 5, "0x%02x", data[i]); + WriteElement(file, buffer, data.size(), i, items_per_row); + } + file << "\n};\n"; +} + + +void WriteElement(std::ostream& file, const std::string& data, unsigned int size, unsigned int counter, + unsigned int items_per_row) +{ + file << data; + if (counter != size - 1) + { + file << ","; + if (counter % items_per_row == items_per_row - 1) + file << "\n\t"; + } +} + +void WriteShortPtrArray(std::ostream& file, const std::string& name, const std::string& append, const std::vector& names, unsigned short items_per_row) +{ + VerboseLog("Writing short array %s%s size %zd", name.c_str(), append.c_str(), names.size()); + file << "const unsigned short* " << name << append << "[" << names.size() << "] =\n{\n\t"; + for (unsigned int i = 0; i < names.size(); i++) + { + const std::string& data_read = names[i]; + WriteElement(file, data_read, names.size(), i, items_per_row); + } + file << "\n};\n"; +} + +void WriteShortPtrArray(std::ostream& file, const std::string& name, const std::string& append, const std::vector& names, const std::string& name_append, + unsigned short items_per_row) +{ + VerboseLog("Writing short ptr array %s%s size %zd", name.c_str(), append.c_str(), names.size()); + file << "const unsigned short* " << name << append << "[" << names.size() << "] =\n{\n\t"; + for (unsigned int i = 0; i < names.size(); i++) + { + const std::string data_read = names[i] + name_append; + WriteElement(file, data_read, names.size(), i, items_per_row); + } + file << "\n};\n"; +} + +void WriteExtern(std::ostream& file, const std::string& type, const std::string& name, const std::string& append, unsigned int size) +{ + VerboseLog("Writing extern %s %s%s size %zd", type.c_str(), name.c_str(), append.c_str(), size); + file << "extern " << type << " " << name << append << "[" << size << "];\n"; +} + +void WriteDefine(std::ostream& file, const std::string& name, const std::string& append, int value) +{ + std::string name_cap = ToUpper(name); + VerboseLog("Writing define %s%s value %d", name.c_str(), append.c_str(), value); + file << "#define " << name_cap << append << " " << value << "\n"; +} + +void WriteDefine(std::ostream& file, const std::string& name, const std::string& append, int value, int shift) +{ + std::string name_cap = ToUpper(name); + VerboseLog("Writing define %s%s value %d<<%d", name.c_str(), append.c_str(), value, shift); + file << "#define " << name_cap << append << " (" << value << " << " << shift << ")\n"; +} + +void WriteDefine(std::ostream& file, const std::string& name, const std::string& append, const std::string& value) +{ + std::string name_cap = ToUpper(name); + VerboseLog("Writing define %s%s value %s", name_cap.c_str(), append.c_str(), value.c_str()); + file << "#define " << name_cap << append << " " << value << "\n"; +} + +void WriteHeaderGuard(std::ostream& file, const std::string& name, const std::string& append) +{ + std::string name_cap = ToUpper(name); + VerboseLog("Writing header guard %s%s", name_cap.c_str(), append.c_str()); + file << "#ifndef " << name_cap << append << "\n"; + file << "#define " << name_cap << append << "\n\n"; +} + +void WriteEndHeaderGuard(std::ostream& file) +{ + VerboseLog("Writing end of header guard"); + file << "#endif\n"; +} + +void WriteNewLine(std::ostream& file) +{ + file << "\n"; +} diff --git a/shared/fileutils.hpp b/shared/fileutils.hpp new file mode 100644 index 0000000..0e11df5 --- /dev/null +++ b/shared/fileutils.hpp @@ -0,0 +1,42 @@ +#ifndef FILEUTILS_HPP +#define FILEUTILS_HPP + +#include +#include +#include +#include + +#include "color.hpp" + +void InitFiles(std::ofstream& c_file, std::ofstream& h_file, const std::string& name); +void WriteElement(std::ostream& file, const std::string& data, unsigned int size, unsigned int counter, + unsigned int items_per_row); + +/// TODO rewrite this. +void WriteShortArray(std::ostream& file, const std::string& name, const std::string& append, + const std::vector& data, unsigned int items_per_row); +void WriteShortArray(std::ostream& file, const std::string& name, const std::string& append, + const std::vector& data, unsigned int items_per_row); +void WriteShortArray(std::ostream& file, const std::string& name, const std::string& append, + const std::vector& data, unsigned int items_per_row); +void WriteShortArray4Bit(std::ostream& file, const std::string& name, const std::string& append, + const std::vector& data, unsigned int items_per_row); +void WriteShortPtrArray(std::ostream& file, const std::string& name, const std::string& append, + const std::vector& names, unsigned short items_per_row); +void WriteShortPtrArray(std::ostream& file, const std::string& name, const std::string& append, + const std::vector& names, const std::string& name_append, + unsigned short items_per_row); + + +void WriteCharArray(std::ostream& file, const std::string& name, const std::string& append, + const std::vector& data, unsigned int items_per_row); + +void WriteExtern(std::ostream& file, const std::string& type, const std::string& name, const std::string& append, unsigned int size); +void WriteHeaderGuard(std::ostream& file, const std::string& name, const std::string& append); +void WriteEndHeaderGuard(std::ostream& file); +void WriteNewLine(std::ostream& file); +void WriteDefine(std::ostream& file, const std::string& name, const std::string& append, int value); +void WriteDefine(std::ostream& file, const std::string& name, const std::string& append, int value, int shift); +void WriteDefine(std::ostream& file, const std::string& name, const std::string& append, const std::string& value); + +#endif diff --git a/shared/headerfile.cpp b/shared/headerfile.cpp new file mode 100644 index 0000000..8123451 --- /dev/null +++ b/shared/headerfile.cpp @@ -0,0 +1,39 @@ +#include "headerfile.hpp" + +#include +#include +#include + +#include "fileutils.hpp" +#include "shared.hpp" + +HeaderFile header; + +void HeaderFile::Write(std::ostream& file) +{ + ExportFile::Write(file); + + WriteHeaderGuard(file, params.symbol_base_name, "_H"); + + /// TODO This needs to change + bool ok_newline = false; + if (transparent_color != -1) + { + char buffer[7]; + sprintf(buffer, "0x%04x", transparent_color); + WriteDefine(file, params.symbol_base_name, "_TRANSPARENT", (mode == "3") ? buffer : "0x00"); + ok_newline = true; + } + if (params.offset) + { + WriteDefine(file, params.symbol_base_name, "_PALETTE_OFFSET ", params.offset); + ok_newline = true; + } + if (ok_newline) WriteNewLine(file); + + for (const auto& exportable : exportables) + exportable->WriteExport(file); + + WriteEndHeaderGuard(file); + WriteNewLine(file); +} diff --git a/shared/headerfile.hpp b/shared/headerfile.hpp new file mode 100644 index 0000000..02f5a8d --- /dev/null +++ b/shared/headerfile.hpp @@ -0,0 +1,19 @@ +#ifndef HEADER_FILE_HPP +#define HEADER_FILE_HPP + +#include "exportfile.hpp" + +#include +#include + +class HeaderFile : public ExportFile +{ + public: + HeaderFile(const std::string& invocation = "") : ExportFile(invocation) {}; + ~HeaderFile() {}; + virtual void Write(std::ostream& file); +}; + +extern HeaderFile header; + +#endif diff --git a/shared/histogram.cpp b/shared/histogram.cpp new file mode 100644 index 0000000..9712bb7 --- /dev/null +++ b/shared/histogram.cpp @@ -0,0 +1,172 @@ +#include "histogram.hpp" +#include +#include + +bool ColorCompare::operator()(Color left, Color right) +{ + bool less; + switch(index) + { + case 0: + less = left.x < right.x; + if (left.x == right.x) less = left.y < right.y; + if (left.x == right.x && left.y == right.y) less = left.z < right.z; + break; + case 1: + less = left.y < right.y; + if (left.y == right.y) less = left.x < right.x; + if (left.x == right.x && left.y == right.y) less = left.z < right.z; + break; + case 2: + less = left.z < right.z; + if (left.z == right.z) less = left.x < right.x; + if (left.x == right.x && left.z == right.z) less = left.y < right.y; + break; + default: + less = false; + } + return less; +} + +/** Histogram + * + * Creates a histogram from the image. + */ +Histogram::Histogram(const std::vector& image) +{ + std::vector::const_iterator i; + std::map::const_iterator j; + for (i = image.begin(); i != image.end(); ++i) + data[*i] += 1; + for (j = data.begin(); j != data.end(); ++j) + colors.push_back(j->first); +} + +/** Histogram + * + * Creates a histogram + */ +Histogram::Histogram(Histogram& hist, const std::vector& keys) +{ + std::copy(keys.begin(), keys.end(), colors.begin()); + std::vector::const_iterator i; + for (i = colors.begin(); i != colors.end(); ++i) + data[*i] = hist.data[*i]; + +} + +/** @brief Histogram + * + * @todo: document this function + */ +Histogram::Histogram(const std::map& hist, const std::vector& keys) : data(hist), colors(keys) +{ +} + +/** @brief GetColors + * + * Gets the colors associated with this histogram + */ +const std::vector& Histogram::GetColors() const +{ + return colors; +} + +/** @brief GetData + * + * Gets the data associated with this histogram. + */ +const std::map& Histogram::GetData() const +{ + return data; +} + +/** Population + * + * Gets the population of his histogram. + */ +size_t Histogram::Population() const +{ + size_t total = 0; + std::map::const_iterator i; + for (i = data.begin(); i != data.end(); ++i) + total += i->second; + return total; +} + +/** @brief Size + * + * @todo: document this function + */ +size_t Histogram::Size() const +{ + return colors.size(); +} + +/** GetAverageColor + * + * Gets the average color represented by this histogram. + */ +Color Histogram::GetAverageColor() const +{ + Color out; + double sumx = 0, sumy = 0, sumz = 0, sump = 0; + std::map::const_iterator i; + for (i = data.begin(); i != data.end(); ++i) + { + Color current = i->first; + size_t population = i->second; + sumx += current.x * population; + sumy += current.y * population; + sumz += current.z * population; + sump += population; + } + + out.x = sumx / sump; + out.y = sumy / sump; + out.z = sumz / sump; + return out; +} + +/** @brief Split + * + * @todo: document this function + */ +void Histogram::Split(std::map& otherData, std::vector& otherColors, ColorCompare& comp) +{ + size_t population = Population(); + + std::sort(colors.begin(), colors.end(), comp); + + // Perform Split finding the median color + size_t median = population / 2; + size_t fill = 0; + while (fill < median) + { + Color current = colors[0]; + if (fill + data[current] <= median) + { + fill += data[current]; + otherColors.push_back(current); + otherData[current] = data[current]; + data.erase(current); + colors.erase(colors.begin()); + } + else + { + // If we can get more than half of this color take all of it. But only if it is not the only color remaining. + if (median - fill > median / 2 && colors.size() > 1) + { + fill += data[current]; + otherColors.push_back(current); + otherData[current] = data[current]; + data.erase(current); + colors.erase(colors.begin()); + } + else + { + fill = median; + } + } + } +} diff --git a/shared/histogram.hpp b/shared/histogram.hpp new file mode 100644 index 0000000..2548261 --- /dev/null +++ b/shared/histogram.hpp @@ -0,0 +1,35 @@ +#ifndef HISTOGRAM_HPP +#define HISTOGRAM_HPP + +#include +#include +#include + +#include "color.hpp" + +class ColorCompare +{ + public: + ColorCompare(int _index) : index(_index) {}; + bool operator()(Color left, Color right); + int index; +}; + +class Histogram +{ + public: + Histogram(const std::vector& image); + Histogram(Histogram& hist, const std::vector& keys); + Histogram(const std::map& hist, const std::vector& keys); + size_t Population() const; + size_t Size() const; + const std::map& GetData() const; + const std::vector& GetColors() const; + Color GetAverageColor() const; + void Split(std::map& otherData, std::vector& otherColors, ColorCompare& comp); + private: + std::map data; + std::vector colors; +}; + +#endif diff --git a/shared/implementationfile.cpp b/shared/implementationfile.cpp new file mode 100644 index 0000000..611e9cd --- /dev/null +++ b/shared/implementationfile.cpp @@ -0,0 +1,17 @@ +#include "implementationfile.hpp" + +#include +#include + +#include "fileutils.hpp" +#include "shared.hpp" + +ImplementationFile implementation; + +void ImplementationFile::Write(std::ostream& file) +{ + ExportFile::Write(file); + + for (const auto& exportable : exportables) + exportable->WriteData(file); +} diff --git a/shared/implementationfile.hpp b/shared/implementationfile.hpp new file mode 100644 index 0000000..6899d16 --- /dev/null +++ b/shared/implementationfile.hpp @@ -0,0 +1,20 @@ +#ifndef IMPLEMENTATION_FILE_HPP +#define IMPLEMENTATION_FILE_HPP + +#include "exportfile.hpp" + +#include +#include + +class ImplementationFile : public ExportFile +{ + public: + ImplementationFile(const std::string& invocation = "") : ExportFile(invocation) {}; + ~ImplementationFile() {}; + virtual void Write(std::ostream& file); +}; + +extern ImplementationFile implementation; + +#endif + diff --git a/shared/mediancut.cpp b/shared/mediancut.cpp new file mode 100644 index 0000000..25b9e42 --- /dev/null +++ b/shared/mediancut.cpp @@ -0,0 +1,268 @@ +#include "mediancut.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +class BoxCompare; + +static void CutBoxes(std::priority_queue, BoxCompare>& queue, std::list& removed, unsigned int desiredColors); +static void SwapQueues(std::priority_queue, BoxCompare>& q1, std::priority_queue, BoxCompare >& q2); + +class BoxCompare +{ + public: + BoxCompare(int mode) + { + this->mode = mode; + } + bool operator()(Box lhs, Box rhs) + { + ///TODO verify this + bool less; + switch(mode) + { + case 0: + less = lhs.Volume() < rhs.Volume(); + break; + case 1: + less = lhs.Population() * lhs.Size() < rhs.Population() * lhs.Size(); + break; + case 2: + less = lhs.Population() * lhs.Volume() < rhs.Population() * rhs.Volume(); + break; + case 3: + less = lhs.Error() < rhs.Error(); + break; + default: + less = false; + } + return less; + } + int mode; +}; + +/** Box + * + * Creates a new Box + */ +Box::Box(const Histogram& hist) : data(hist), min(DBL_MAX, DBL_MAX, DBL_MAX), max(DBL_MIN, DBL_MIN, DBL_MIN) +{ +} + +/** ~Box + * + * Destructor + */ +Box::~Box() +{ +} + +/** @brief GetData + * + * @todo: document this function + */ +const Histogram& Box::GetData() const +{ + return data; +} + +/** @brief Split + * + * @todo: document this function + */ +Box Box::Split() +{ + std::vector otherColors; + std::map otherHist; + + double x, y, z; + ColorCompare comp(0); + x = max.x - min.x; + y = max.y - min.y; + z = max.z - min.z; + + if (x > y && x > z) + comp = ColorCompare(0); + else if (y > x && y > z) + comp = ColorCompare(1); + else + comp = ColorCompare(2); + + data.Split(otherHist, otherColors, comp); + + return Box(Histogram(otherHist, otherColors)); +} + +/** @brief Shrink + * + * Shrinks the Box + */ +void Box::Shrink() +{ + std::vector::const_iterator i; + const std::vector& colors = data.GetColors(); + for (i = colors.begin(); i != colors.end(); ++i) + { + Color c = *i; + + min.x = std::min(c.x, min.x); + max.x = std::max(c.x, max.x); + min.y = std::min(c.y, min.y); + max.y = std::max(c.y, max.y); + min.z = std::min(c.z, min.z); + max.z = std::max(c.z, max.z); + } +} + +/** Population + * + * Gets the Population of this box + */ +size_t Box::Population() const +{ + return data.Population(); +} + +/** @brief Size + * + * @todo: document this function + */ +size_t Box::Size() const +{ + return data.Size(); +} + +/** @brief Volume + * + * @todo: document this function + */ +double Box::Volume() const +{ + return (max.x - min.x) * (max.y - min.y) * (max.z - min.z); +} + +/** @brief Error + * + * Calculates the Error associated with this box. + */ +double Box::Error() const +{ + Color average = data.GetAverageColor(); + const std::map& hist = data.GetData(); + const std::vector colors = data.GetColors(); + std::vector::const_iterator i; + + double error = 0; + for (i = colors.begin(); i != colors.end(); ++i) + { + Color color = *i; + size_t population = hist.find(color)->second; + error += (color.x - average.x) * (color.x - average.x) * population; + error += (color.y - average.y) * (color.y - average.y) * population; + error += (color.z - average.z) * (color.z - average.z) * population; + } + + return error; +} + +/** @brief GetAverageColor + * + * @todo: document this function + */ +Color Box::GetAverageColor() const +{ + return data.GetAverageColor(); +} + +bool MedianCut(const std::vector& image, unsigned int desiredColors, std::vector& palette, const int weights[4]) +{ + Histogram hist(image); + // If we have fewer colors than desired + if (hist.Size() <= desiredColors) + { + const std::vector& colors = hist.GetColors(); + for (const auto& color : colors) + palette.push_back(color); + return false; + } + // Volume Queue + std::priority_queue, BoxCompare> queue0(BoxCompare(0)); + // Population Queue + std::priority_queue, BoxCompare> queue1(BoxCompare(1)); + // Popular Volume Queue + std::priority_queue, BoxCompare> queue2(BoxCompare(2)); + // Error Queue + std::priority_queue, BoxCompare> queue3(BoxCompare(3)); + std::list removed; + + Box box(hist); + box.Shrink(); + queue0.push(box); + + + + CutBoxes(queue0, removed, weights[0] * desiredColors / 100); + SwapQueues(queue0, queue1); + CutBoxes(queue1, removed, (weights[0] + weights[1]) * desiredColors / 100); + SwapQueues(queue1, queue2); + CutBoxes(queue2, removed, (weights[0] + weights[1] + weights[2]) * desiredColors / 100); + SwapQueues(queue2, queue3); + CutBoxes(queue3, removed, desiredColors); + + while (!queue3.empty()) + { + Box current = queue3.top(); + queue3.pop(); + Color color = current.GetAverageColor(); + palette.push_back(color); + } + + while (!removed.empty()) + { + Box current = removed.front(); + removed.pop_front(); + + Color color = current.GetAverageColor(); + palette.push_back(color); + } + + return true; +} + + +void CutBoxes(std::priority_queue, BoxCompare>& queue, + std::list& removed, unsigned int desiredColors) +{ + while (queue.size() + removed.size() < desiredColors && !queue.empty()) + { + Box current = queue.top(); + Histogram histogram = current.GetData(); + queue.pop(); + + Box other = current.Split(); + + other.Shrink(); + current.Shrink(); + + if (current.Size() > 1) queue.push(current); + if (other.Size() > 1) queue.push(other); + if (current.Size() == 1) removed.push_back(current); + if (other.Size() == 1) removed.push_back(other); + } +} + +void SwapQueues(std::priority_queue, BoxCompare>& q1, + std::priority_queue, BoxCompare>& q2) +{ + while (!q1.empty()) + { + q2.push(q1.top()); + q1.pop(); + } +} diff --git a/shared/mediancut.hpp b/shared/mediancut.hpp new file mode 100644 index 0000000..b622516 --- /dev/null +++ b/shared/mediancut.hpp @@ -0,0 +1,34 @@ +#ifndef MEDIAN_CUT_HPP +#define MEDIAN_CUT_HPP + +#include +#include +#include + +#include "color.hpp" +#include "histogram.hpp" + +class Box +{ + public: + Box(const Histogram& hist); + ~Box(); + double Error() const; + double Volume() const; + size_t Population() const; + size_t Size() const; + void Shrink(); + Box Split(); + Color GetAverageColor() const; + const Histogram& GetData() const; + bool operator<(const Box& rhs); + private: + Histogram data; + Color min, max; + +}; + +bool MedianCut(const std::vector& image, unsigned int desiredColors, std::vector& palette, const int weights[4]); + +#endif + diff --git a/shared/reductionhelper.cpp b/shared/reductionhelper.cpp new file mode 100644 index 0000000..edc6fb9 --- /dev/null +++ b/shared/reductionhelper.cpp @@ -0,0 +1,1572 @@ +#include "reductionhelper.hpp" + +#include +#include +#include +#include +#include + +#include "cpercep.hpp" +#include "dither.hpp" +#include "fileutils.hpp" +#include "mediancut.hpp" +#include "shared.hpp" + +const int sprite_shapes[16] = +{ +//h = 1, 2, 4, 8 + 0, 2, 2, -1, // width = 1 + 1, 0, 2, -1, // width = 2 + 1, 1, 0, 2, // width = 4 + -1, -1, 1, 0 // width = 8 +}; + +const int sprite_sizes[16] = +{ +//h = 1, 2, 4, 8 + 0, 0, 1, -1, // width = 1 + 0, 1, 2, -1, // width = 2 + 1, 2, 2, 3, // width = 4 + -1, -1, 3, 3 // width = 8 +}; + +Image32Bpp::Image32Bpp(const Magick::Image& image, const std::string& name, const std::string& filename, unsigned int frame, bool animated) : + Image(image.columns(), image.rows(), name, filename, frame, animated), has_alpha(false), pixels(3 * width * height) +{ + unsigned int num_pixels = width * height; + const Magick::PixelPacket* imageData = image.getConstPixels(0, 0, image.columns(), image.rows()); + + size_t depth; + MagickCore::GetMagickQuantumDepth(&depth); + for (unsigned int i = 0; i < num_pixels; i++) + { + const Magick::PixelPacket& packet = imageData[i]; + int r, g, b; + if (depth == 8) + { + r = packet.red; + g = packet.green; + b = packet.blue; + } + else if (depth == 16) + { + r = (packet.red >> 8) & 0xFF; + g = (packet.green >> 8) & 0xFF; + b = (packet.blue >> 8) & 0xFF; + } + else + { + // To get rid of warning + b = g = r = 0; + FatalLog("Image quantum not supported"); + } + + pixels[3 * i] = b; + pixels[3 * i + 1] = g; + pixels[3 * i + 2] = r; + } +} + +void Image32Bpp::WriteData(std::ostream& file) const +{ + WriteCharArray(file, export_name, "", pixels, 10); + WriteNewLine(file); +} + +void Image32Bpp::WriteExport(std::ostream& file) const +{ + WriteExtern(file, "const unsigned char", export_name, "", pixels.size()); + WriteDefine(file, export_name, "_SIZE", pixels.size()); + WriteDefine(file, export_name, "_WIDTH", width); + WriteDefine(file, export_name, "_HEIGHT", height); + WriteNewLine(file); +} + +Image16Bpp::Image16Bpp(const Image32Bpp& image) : Image(image), pixels(width * height) +{ + unsigned int num_pixels = width * height; + const std::vector& pixels32 = image.pixels; + + for (unsigned int i = 0; i < num_pixels; i++) + { + char r, g, b; + r = (pixels32[3 * i + 2] >> 3) & 0x1F; + g = (pixels32[3 * i + 1] >> 3) & 0x1F; + b = (pixels32[3 * i] >> 3) & 0x1F; + pixels[i] = r | g << 5 | b << 10; + } +} + +void Image16Bpp::GetColors(std::vector& colors) const +{ + for (unsigned int i = 0; i < width * height; i++) + { + short pix = pixels[i]; + if (pix != params.transparent_color) + { + colors.push_back(Color(pix)); + } + } +} + +Image16Bpp Image16Bpp::SubImage(unsigned int x, unsigned int y, unsigned int swidth, unsigned int sheight) const +{ + Image16Bpp sub(swidth, sheight); + for (unsigned int i = 0; i < sheight; i++) + { + for (unsigned int j = 0; j < swidth; j++) + { + sub.pixels[i * swidth + j] = pixels[(i + y) * width + j + x]; + } + } + return sub; +} + +void Image16Bpp::WriteData(std::ostream& file) const +{ + WriteShortArray(file, export_name, "", pixels, 10); + WriteNewLine(file); +} + +void Image16Bpp::WriteExport(std::ostream& file) const +{ + WriteExtern(file, "const unsigned short", export_name, "", pixels.size()); + WriteDefine(file, export_name, "_SIZE", pixels.size()); + WriteDefine(file, export_name, "_WIDTH", width); + WriteDefine(file, export_name, "_HEIGHT", height); + WriteNewLine(file); +} + +void ColorArray::Set(const std::vector& _colors) +{ + colors = _colors; + colorSet.clear(); + colorSet.insert(colors.begin(), colors.end()); +} + +int ColorArray::Search(const Color& a) const +{ + register double bestd = DBL_MAX; + int index = -1; + + if (colorIndexCache.find(a) != colorIndexCache.end()) + return colorIndexCache[a]; + + for (unsigned int i = 0; i < colors.size(); i++) + { + double dist = 0; + const Color& b = colors[i]; + dist = a.Distance(b); + if (dist < bestd) + { + index = i; + bestd = dist; + } + } + + colorIndexCache[a] = index; + + if (bestd != 0) + { + int x, y, z; + a.Get(x, y, z); + unsigned short c = x | (y << 5) | (z << 10); + VerboseLog("Color remap: Color %d given to palette bank not an exact match.", c); + } + + return index; +} + +int ColorArray::Search(unsigned short color_data) const +{ + return Search(Color(color_data)); +} + +bool ColorArray::Contains(const ColorArray& palette) const +{ + for (const auto& color : palette.colors) + { + if (colorSet.find(color) == colorSet.end()) + return false; + } + return true; +} + +void ColorArray::Add(const Color& c) +{ + if (colorSet.find(c) == colorSet.end()) + { + colorSet.insert(c); + colors.push_back(c); + } +} + +Palette::Palette(const std::vector& colors, const std::string& _name) : ColorArray(colors), name(_name) +{ + if (colors.size() + params.offset > 256) + FatalLog("Too many colors in palette. Found %d colors, offset is %d.", colors.size() + params.offset, params.offset); +} + +void Palette::WriteData(std::ostream& file) const +{ + WriteShortArray(file, name, "_palette", colors, 10); + WriteNewLine(file); +} + +void Palette::WriteExport(std::ostream& file) const +{ + WriteExtern(file, "const unsigned short", name, "_palette", colors.size()); + WriteDefine(file, name, "_PALETTE_SIZE", colors.size()); + WriteNewLine(file); +} + +Image8Bpp::Image8Bpp(const Image16Bpp& image) : Image(image), pixels(width * height), palette(NULL), export_shared_info(true) +{ + // If the image width is odd error out + if (width & 1) + FatalLog("Image: %s width is not a multiple of 2. Please fix", name.c_str()); + + std::vector pixels16; + image.GetColors(pixels16); + + // If transparent color is present add to palette, but only if offset is not 0 + int palette_size = params.palette; + std::vector paletteColors(palette_size); + if (params.offset == 0) + { + palette_size--; + paletteColors.push_back(Color(params.transparent_color)); + } + + MedianCut(pixels16, palette_size, paletteColors, params.weights.data()); + + palette.reset(new Palette(paletteColors, export_name)); + RiemersmaDither(image, *this, params.transparent_color, params.dither, params.dither_level); + if (params.offset > 0) + { + for (unsigned char& pix : pixels) + if (pix) + pix += params.offset; + } +} + +Image8Bpp::Image8Bpp(const Image16Bpp& image, std::shared_ptr& global_palette) : Image(image), pixels(width * height), palette(global_palette), export_shared_info(false) +{ + if (width & 1) + FatalLog("Image: %s width is not a multiple of 2. Please fix", name.c_str()); + + RiemersmaDither(image, *this, params.transparent_color, params.dither, params.dither_level); + if (params.offset > 0) + { + for (unsigned char& pix : pixels) + if (pix) + pix += params.offset; + } +} + +void Image8Bpp::WriteData(std::ostream& file) const +{ + // Sole owner of palette + if (export_shared_info) + palette->WriteData(file); + WriteShortArray(file, export_name, "", pixels, 10); + WriteNewLine(file); +} + +void Image8Bpp::WriteExport(std::ostream& file) const +{ + // Sole owner of palette + if (export_shared_info) + palette->WriteExport(file); + WriteExtern(file, "const unsigned short", export_name, "", pixels.size()/2); + WriteDefine(file, export_name, "_SIZE", pixels.size()/2); + WriteDefine(file, export_name, "_WIDTH", width); + WriteDefine(file, export_name, "_HEIGHT", height); + WriteNewLine(file); +} + +Image8BppScene::Image8BppScene(const std::vector& images16, const std::string& _name) : Scene(_name), palette(NULL) +{ + unsigned int total_pixels = 0; + for (unsigned int i = 0; i < images16.size(); i++) + total_pixels += images16[i].width * images16[i].height; + + std::vector pixels(total_pixels); + for (unsigned int i = 0; i < images16.size(); i++) + images16[i].GetColors(pixels); + + // If transparent color is present add to palette + std::vector paletteColors; + paletteColors.reserve(params.palette); + if (params.offset == 0) + { + params.palette -= 1; + paletteColors.push_back(Color(params.transparent_color)); + } + + MedianCut(pixels, params.palette, paletteColors, params.weights.data()); + palette.reset(new Palette(paletteColors, name)); + + images.reserve(images16.size()); + for (unsigned int i = 0; i < images.size(); i++) + images.emplace_back(new Image8Bpp(images16[i], palette)); +} + +const Image8Bpp& Image8BppScene::GetImage(int index) const +{ + const Image* image = images[index].get(); + const Image8Bpp* image8 = dynamic_cast(image); + if (!image8) FatalLog("Could not cast Image to Image8Bpp. This shouldn't happen"); + return *image8; +} + +void Image8BppScene::WriteData(std::ostream& file) const +{ + palette->WriteData(file); + Scene::WriteData(file); +} + +void Image8BppScene::WriteExport(std::ostream& file) const +{ + palette->WriteExport(file); + Scene::WriteExport(file); +} + +int PaletteBank::CanMerge(const ColorArray& palette) const +{ + std::set tmpset(colorSet); + for (const auto& color : palette.colors) + tmpset.insert(color); + + return 16 - tmpset.size(); +} + +void PaletteBank::Merge(const ColorArray& palette) +{ + for (const auto& color : palette.colors) + Add(color); +} + +std::ostream& operator<<(std::ostream& file, const PaletteBank& bank) +{ + char buffer[7]; + std::vector colors = bank.colors; + colors.resize(16); + for (unsigned int i = 0; i < colors.size(); i++) + { + int x, y, z; + const Color& color = colors[i]; + color.Get(x, y, z); + unsigned short data_read = x | (y << 5) | (z << 10); + snprintf(buffer, 7, "0x%04x", data_read); + WriteElement(file, buffer, colors.size(), i, 8); + } + + return file; +} + +PaletteBankManager::PaletteBankManager(const std::string& _name) : name(_name), banks(16) +{ + for (unsigned int i = 0; i < banks.size(); i++) + banks[i].id = i; +} + +PaletteBankManager::PaletteBankManager(const std::string& _name, const std::vector& paletteBanks) : name(_name), banks(paletteBanks) +{ +} + +void PaletteBankManager::WriteData(std::ostream& file) const +{ + file << "const unsigned short " << name << "_palette[256] =\n{\n\t"; + for (unsigned int i = 0; i < 16; i++) + { + file << banks[i]; + if (i != 16 - 1) + file << ",\n\t"; + } + file << "\n};\n"; + WriteNewLine(file); +} + +void PaletteBankManager::WriteExport(std::ostream& file) const +{ + WriteExtern(file, "const unsigned short", name, "_palette", 256); + WriteDefine(file, name, "_PALETTE_SIZE", 256); + WriteNewLine(file); +} + +template <> +Tile::Tile() : id(0), data(TILE_SIZE), bpp(16), palette_bank(0) +{ +} + +template <> +Tile::Tile(const std::vector& image, int pitch, int tilex, int tiley, int border, int ignored) : data(TILE_SIZE), palette_bank(0) +{ + Set(image, pitch, tilex, tiley, border, 16); +} + +template <> +Tile::Tile(const unsigned short* image, int pitch, int tilex, int tiley, int border, int ignored) : data(TILE_SIZE), palette_bank(0) +{ + Set(image, pitch, tilex, tiley, border, 16); +} + +template <> +Tile::Tile(std::shared_ptr& imageTile, int _bpp) : data(TILE_SIZE), bpp(_bpp) +{ + // bpp reduce minus one for the transparent color + int num_colors = (1 << bpp) - 1; + const unsigned short* imgdata = imageTile->data.data(); + int weights[4] = {25, 25, 25, 25}; + + std::vector pixels; + pixels.reserve(TILE_SIZE); + + std::vector paletteArray; + paletteArray.reserve(1 << bpp); + + for (unsigned int i = 0; i < TILE_SIZE; i++) + { + unsigned short pix = imgdata[i]; + if (pix != params.transparent_color) + pixels.push_back(Color(pix)); + } + paletteArray.push_back(Color(params.transparent_color)); + MedianCut(pixels, num_colors, paletteArray, weights); + + palette.Set(paletteArray); + + for (unsigned int i = 0; i < TILE_SIZE; i++) + { + unsigned short pix = imgdata[i]; + data[i] = (pix != params.transparent_color) ? palette.Search(pix) : 0; + } + + sourceTile = imageTile; +} + +template <> +void Tile::Set(const Image16Bpp& image, const Palette& global_palette, int tilex, int tiley) +{ + bpp = 8; + for (unsigned int i = 0; i < TILE_SIZE; i++) + { + int x = i % 8; + int y = i / 8; + data[i] = global_palette.Search(image.pixels[(tiley * 8 + y) * image.width + tilex * 8 + x]); + } +} + +template <> +void Tile::UsePalette(const PaletteBank& bank) +{ + if (bpp == 8) + FatalLog("Code error not implemented"); + + std::map remapping; + for (unsigned int i = 0; i < palette.Size(); i++) + { + Color old = palette.At(i); + remapping[i] = bank.Search(old); + } + + for (unsigned int i = 0; i < TILE_SIZE; i++) + { + if (remapping.find(data[i]) == remapping.end()) + FatalLog("Somehow tile contains invalid indicies. This shouldn't happen"); + + data[i] = remapping[data[i]]; + } + + palette.Set(bank.colors); +} + +template <> +std::ostream& operator<<(std::ostream& file, const Tile& tile) +{ + char buffer[7]; + const std::vector& data = tile.data; + if (tile.bpp == 8) + { + for (unsigned int i = 0; i < TILE_SIZE_SHORTS_8BPP; i++) + { + snprintf(buffer, 7, "0x%02x%02x", data[2 * i + 1], data[2 * i]); + WriteElement(file, buffer, TILE_SIZE_SHORTS_8BPP, i, 8); + } + } + else + { + for (unsigned int i = 0; i < TILE_SIZE_SHORTS_4BPP; i++) + { + snprintf(buffer, 7, "0x%01x%01x%01x%01x", data[4 * i + 3], data[4 * i + 2], data[4 * i + 1], data[4 * i]); + WriteElement(file, buffer, TILE_SIZE_SHORTS_4BPP, i, 8); + } + } + return file; +} + +bool TilesPaletteSizeComp(const GBATile& i, const GBATile& j) +{ + return i.palette.Size() > j.palette.Size(); +} + +Tileset::Tileset(const std::vector& images, const std::string& _name, int _bpp) : name(_name), bpp(_bpp), paletteBanks(name) +{ + switch(bpp) + { + case 4: + Init4bpp(images); + break; + case 8: + Init8bpp(images); + break; + case 16: + Init16bpp(images); + break; + } +} + +Tileset::Tileset(const Image16Bpp& image, int _bpp) : name(image.name), bpp(_bpp), paletteBanks(name) +{ + std::vector images; + images.push_back(image); + switch(bpp) + { + case 4: + Init4bpp(images); + break; + case 8: + Init8bpp(images); + break; + case 16: + Init16bpp(images); + break; + } +} + +int Tileset::Search(const GBATile& tile) const +{ + const std::set::const_iterator foundTile = tiles.find(tile); + if (foundTile != tiles.end()) + { + std::cout << "found\n" << *foundTile << "\n"; + return foundTile->id; + } + + return -1; +} + +int Tileset::Search(const ImageTile& tile) const +{ + const std::set::const_iterator foundTile = itiles.find(tile); + if (foundTile != itiles.end()) + return foundTile->id; + + return -1; +} + +bool Tileset::Match(const ImageTile& tile, int& tile_id, int& pal_id) const +{ + const std::map::const_iterator foundTile = matcher.find(tile); + if (foundTile != matcher.end()) + { + const GBATile& tile = foundTile->second; + tile_id = tile.id; + pal_id = tile.palette_bank; + return true; + } + + return false; +} + +void Tileset::WriteData(std::ostream& file) const +{ + if (bpp == 8) + palette->WriteData(file); + else + paletteBanks.WriteData(file); + + std::vector::const_iterator tile_ptr = tilesExport.begin(); + file << "const unsigned short " << name << "_tiles[" << Size() << "] =\n{\n\t"; + for (unsigned int i = 0; i < tilesExport.size(); i++) + { + file << *tile_ptr; + if (i != tilesExport.size() - 1) + file << ",\n\t"; + ++tile_ptr; + } + file << "\n};\n"; + WriteNewLine(file); +} + +void Tileset::WriteExport(std::ostream& file) const +{ + if (bpp == 8) + palette->WriteExport(file); + else + paletteBanks.WriteExport(file); + + WriteDefine(file, name, "_PALETTE_TYPE", (bpp == 8), 7); + WriteNewLine(file); + + WriteExtern(file, "const unsigned short", name, "_tiles", Size()); + WriteDefine(file, name, "_TILES", tiles.size()); + WriteDefine(file, name, "_TILES_SIZE", Size()); + WriteNewLine(file); +} + +void Tileset::Init4bpp(const std::vector& images) +{ + // Tile image into 16 bit tiles + Tileset tileset16bpp(images, name, 16); + std::set imageTiles = tileset16bpp.itiles; + + GBATile nullTile; + nullTile.id = 0; + nullTile.bpp = 4; + ImageTile nullImageTile; + tiles.insert(nullTile); + tilesExport.push_back(nullTile); + matcher[nullImageTile] = nullTile; + + // Reduce each tile to 4bpp + std::vector gbaTiles; + for (std::set::const_iterator i = imageTiles.begin(); i != imageTiles.end(); ++i) + { + std::shared_ptr src(new ImageTile(*i)); + GBATile tile(src, 4); + gbaTiles.push_back(tile); + } + + // Ensure image contains < 256 colors + std::set bigPalette; + for (const auto& tile : gbaTiles) + { + const std::vector& tile_palette = tile.palette.colors; + bigPalette.insert(tile_palette.begin(), tile_palette.end()); + } + + if (bigPalette.size() > 256 && !params.force) + FatalLog("Image after reducing tiles to 4bpp still contains more than 256 distinct colors. Found %d colors. Please fix.", bigPalette.size()); + + // Greedy approach deal with tiles with largest palettes first. + std::sort(gbaTiles.begin(), gbaTiles.end(), TilesPaletteSizeComp); + + // But deal with nulltile + for (unsigned int i = 0; i < paletteBanks.Size(); i++) + paletteBanks[i].Add(Color()); + nullTile.palette_bank = 0; + + + // Construct palette banks, assign bank id to tile, remap tile to palette bank given, assign tile ids + for (auto& tile : gbaTiles) + { + int pbank = -1; + // Fully contains checks + for (unsigned int i = 0; i < paletteBanks.Size(); i++) + { + PaletteBank& bank = paletteBanks[i]; + if (bank.Contains(tile.palette)) + pbank = i; + } + + // Ok then find least affected bank + if (pbank == -1) + { + int max_colors_left = -1; + for (unsigned int i = 0; i < paletteBanks.Size(); i++) + { + PaletteBank& bank = paletteBanks[i]; + int colors_left = bank.CanMerge(tile.palette); + if (colors_left != 0 && max_colors_left < colors_left) + { + max_colors_left = colors_left; + pbank = i; + } + } + } + + // Cry and die for now. Unless you tell me to keep going. + if (pbank == -1 && !params.force) + FatalLog("More than 16 distinct palettes found, please use 8bpp mode."); + + // Merge step and assign palette bank + paletteBanks[pbank].Merge(tile.palette); + tile.palette_bank = pbank; + tile.UsePalette(paletteBanks[pbank]); + + // Assign tile id + std::set::const_iterator it = tiles.find(tile); + if (it == tiles.end()) + { + tile.id = tiles.size(); + tiles.insert(tile); + tilesExport.push_back(tile); + } + else + { + tile.id = it->id; + } + + // Form mapping from ImageTile to GBATile + matcher[*tile.sourceTile] = tile; + } + + int tile_size = TILE_SIZE_BYTES_4BPP; + int memory_b = tiles.size() * tile_size; + if (tiles.size() >= 1024) + FatalLog("Too many tiles found %d tiles. Please make the image simpler.", tiles.size()); + + // Delicious infos + int cbbs = tiles.size() * tile_size / SIZE_CBB_BYTES; + int sbbs = (int) ceil(tiles.size() * tile_size % SIZE_CBB_BYTES / ((double)SIZE_SBB_BYTES)); + InfoLog("Tiles found %zu.", tiles.size()); + InfoLog("Tiles uses %d charblocks and %d screenblocks.", cbbs, sbbs); + InfoLog("Total utilization %.2f/4 charblocks or %d/32 screenblocks, %d/65536 bytes.", + memory_b / ((double)SIZE_CBB_BYTES), (int) ceil(memory_b / ((double)SIZE_SBB_BYTES)), memory_b); +} + +void Tileset::Init8bpp(const std::vector& images16) +{ + int tile_width = 8 + params.border; + + // Reduce all and get the global palette and reduced images. + Image8BppScene scene(images16, name); + palette = scene.palette; + + const std::vector>& images = scene.GetImages(); + + GBATile nullTile; + ImageTile nullImageTile; + tiles.insert(nullTile); + tilesExport.push_back(nullTile); + matcher[nullImageTile] = nullTile; + + for (unsigned int k = 0; k < images.size(); k++) + { + bool disjoint_error = false; + const Image8Bpp& image = scene.GetImage(k); + const Image16Bpp& image16 = images16[k]; + + offsets.push_back(tiles.size()); + unsigned int tilesX = image.width / tile_width; + unsigned int tilesY = image.height / tile_width; + unsigned int totalTiles = tilesX * tilesY; + + // Perform reduce. + for (unsigned int i = 0; i < totalTiles; i++) + { + int tilex = i % tilesX; + int tiley = i / tilesX; + GBATile tile(image.pixels, image.width, tilex, tiley, params.border); + std::set::iterator foundTile = tiles.find(tile); + if (foundTile == tiles.end()) + { + tile.id = tiles.size(); + tiles.insert(tile); + tilesExport.push_back(tile); + // Add matcher data + ImageTile imageTile(image16.pixels, image.width, tilex, tiley, params.border); + matcher[imageTile] = tile; + } + else if (offsets.size() > 1 && !disjoint_error) + { + WarnLog("Tiles found in tileset image %s are not disjoint, offset calculations may be off", image.name.c_str()); + disjoint_error = true; + } + } + } + + // Checks + int tile_size = TILE_SIZE_BYTES_8BPP; + int memory_b = tiles.size() * tile_size; + if (tiles.size() >= 1024) + FatalLog("Too many tiles found %d tiles. Please make the image simpler.", tiles.size()); + + + // Delicious infos + int cbbs = tiles.size() * tile_size / SIZE_CBB_BYTES; + int sbbs = (int) ceil(tiles.size() * tile_size % SIZE_CBB_BYTES / ((double)SIZE_SBB_BYTES)); + InfoLog("Tiles found %zu.", tiles.size()); + InfoLog("Tiles uses %d charblocks and %d screenblocks.", cbbs, sbbs); + InfoLog("Total utilization %.2f/4 charblocks or %d/32 screenblocks, %d/65536 bytes.", + memory_b / ((double)SIZE_CBB_BYTES), (int) ceil(memory_b / ((double)SIZE_SBB_BYTES)), memory_b); +} + +void Tileset::Init16bpp(const std::vector& images) +{ + int tile_width = 8 + params.border; + ImageTile nullTile; + itiles.insert(nullTile); + + for (unsigned int k = 0; k < images.size(); k++) + { + const Image16Bpp& image = images[k]; + const std::vector& pixels = image.pixels; + + unsigned int tilesX = image.width / tile_width; + unsigned int tilesY = image.height / tile_width; + unsigned int totalTiles = tilesX * tilesY; + + // Perform reduce. + for (unsigned int i = 0; i < totalTiles; i++) + { + int tilex = i % tilesX; + int tiley = i / tilesX; + ImageTile tile(pixels, image.width, tilex, tiley, params.border); + std::set::iterator foundTile = itiles.find(tile); + if (foundTile == itiles.end()) + { + tile.id = itiles.size(); + itiles.insert(tile); + } + } + } +} + +Map::Map(const Image16Bpp& image, int bpp) : Image(image.width / 8, image.height / 8, image.name, image.filename, image.frame, image.animated), + data(width * height), tileset(NULL), export_shared_info(true) +{ + // Create tileset according to bpp + tileset.reset(new Tileset(image, bpp)); + + // Tile match each tile in image + switch(bpp) + { + case 4: + Init4bpp(image); + break; + default: + Init8bpp(image); + break; + } +} + +Map::Map(const Image16Bpp& image, std::shared_ptr& global_tileset) : Image(image.width / 8, image.height / 8, image.name, image.filename, image.frame, image.animated), + data(width * height), tileset(global_tileset), export_shared_info(false) +{ + switch(tileset->bpp) + { + case 4: + Init4bpp(image); + break; + default: + Init8bpp(image); + break; + } +} + +void Map::Init4bpp(const Image16Bpp& image) +{ + const std::vector& pixels = image.pixels; + + for (unsigned int i = 0; i < data.size(); i++) + { + int tilex = i % width; + int tiley = i / width; + ImageTile imageTile(pixels, image.width, tilex, tiley); + int tile_id = 0; + int pal_id = 0; + + if (!tileset->Match(imageTile, tile_id, pal_id)) + { + WarnLog("Image: %s No match for tile starting at (%d %d) px, using empty tile instead.", image.name.c_str(), tilex * 8, tiley * 8); + WarnLog("Image: %s No match for palette for tile starting at (%d %d) px, using palette 0 instead.", image.name.c_str(), tilex * 8, tiley * 8); + } + data[i] = pal_id << 12 | tile_id; + + } +} + +void Map::Init8bpp(const Image16Bpp& image) +{ + const std::vector& pixels = image.pixels; + + for (unsigned int i = 0; i < data.size(); i++) + { + int tilex = i % width; + int tiley = i / width; + ImageTile tile(pixels, image.width, tilex, tiley); + int tile_id = 0; + int pal_id = 0; + + if (!tileset->Match(tile, tile_id, pal_id)) + { + WarnLog("Image: %s No match for tile starting at (%d %d) px, using empty tile instead.", image.name.c_str(), tilex * 8, tiley * 8); + } + + data[i] = tile_id; + } +} + +void Map::WriteData(std::ostream& file) const +{ + // Sole owner of tileset. + if (export_shared_info) + tileset->WriteData(file); + + char buffer[7]; + int type = (width > 32 ? 1 : 0) | (height > 32 ? 1 : 0) << 1; + int num_blocks = (type == 0 ? 1 : (type < 3 ? 2 : 4)); + + file << "const unsigned short " << export_name << "_map[" << num_blocks * 32 * 32 << "] =\n{\n\t"; + for (int i = 0; i < num_blocks; i++) + { + // Case for each possible value of num_blocks + // 1: 0 + // 2: type is 1 - 0, 1 + // type is 2 - 0, 2 + // 4: 0, 1, 2, 3 + int sbb = (i == 0 ? 0 : (i == 1 && type == 2 ? 2 : i)); + unsigned int sx, sy; + sx = ((sbb & 1) != 0) * 32; + sy = ((sbb & 2) != 0) * 32; + for (unsigned int y = 0; y < 32; y++) + { + for (unsigned int x = 0; x < 32; x++) + { + // Read tile if outside bounds replace with null tile + unsigned short tile_id; + if (x + sx > width || y + sy > height) + tile_id = 0; + else + tile_id = data[(y + sy) * width + (x + sx)]; + + snprintf(buffer, 7, "0x%04x", tile_id); + // Write it. + WriteElement(file, buffer, num_blocks * 32 * 32, (y + sy) * width + (x + sx), 8); + } + } + } + file << "\n};\n"; + WriteNewLine(file); +} + +void Map::WriteExport(std::ostream& file) const +{ + // Sole owner of tileset. + if (export_shared_info) + tileset->WriteExport(file); + + WriteExtern(file, "const unsigned short", export_name, "_map", Size()); + WriteDefine(file, export_name, "_WIDTH", width); + WriteDefine(file, export_name, "_HEIGHT", height); + WriteDefine(file, export_name, "_MAP_SIZE", Size()); + WriteDefine(file, export_name, "_MAP_TYPE", Type(), 14); + WriteNewLine(file); +} + +MapScene::MapScene(const std::vector& images16, const std::string& _name, int bpp) : Scene(_name), tileset(NULL) +{ + tileset.reset(new Tileset(images16, name, bpp)); + + for (const auto& image : images16) + images.emplace_back(new Map(image, tileset)); +} + +MapScene::MapScene(const std::vector& images16, const std::string& _name, std::shared_ptr& _tileset) : Scene(_name), tileset(_tileset) +{ + for (const auto& image : images16) + images.emplace_back(new Map(image, tileset)); +} + +void MapScene::WriteData(std::ostream& file) const +{ + tileset->WriteData(file); + Scene::WriteData(file); +} + +void MapScene::WriteExport(std::ostream& file) const +{ + tileset->WriteExport(file); + Scene::WriteExport(file); +} + +Sprite::Sprite(const Image16Bpp& image, std::shared_ptr& global_palette) : + Image(image.width / 8, image.height / 8, image.name, image.filename, image.frame, image.animated), data(width * height), palette(global_palette), + palette_bank(-1), size(0), shape(0), offset(0) +{ + unsigned int key = (log2(width) << 2) | log2(height); + shape = sprite_shapes[key]; + size = sprite_sizes[key]; + if (size == -1) + FatalLog("Invalid sprite size, (%d %d) Please fix", width, height); + + for (unsigned int i = 0; i < data.size(); i++) + { + int tilex = i % width; + int tiley = i / width; + + data[i].Set(image, *global_palette, tilex, tiley); + } +} + +Sprite::Sprite(const Image16Bpp& image, int bpp) : Image(image.width / 8, image.height / 8, image.name, image.filename, image.frame, image.animated), + data(width * height), palette_bank(-1), size(0), shape(0), offset(0) +{ + unsigned int key = (log2(width) << 2) | log2(height); + shape = sprite_shapes[key]; + size = sprite_sizes[key]; + if (size == -1) + FatalLog("Invalid sprite size, (%d %d) Please fix", width, height); + + // bpp reduce minus one for the transparent color + int num_colors = (1 << bpp) - 1; + const unsigned short* imgdata = image.pixels.data(); + unsigned int size = image.pixels.size(); + + std::vector pixels(size); + std::vector paletteArray(num_colors + 1); + + for (unsigned int i = 0; i < size; i++) + { + unsigned short pix = imgdata[i]; + if (pix != params.transparent_color) + pixels.push_back(Color(pix)); + } + paletteArray.push_back(Color(params.transparent_color)); + MedianCut(pixels, num_colors, paletteArray, params.weights.data()); + + palette.reset(new Palette(paletteArray, "")); + + std::vector data4bpp(size); + for (unsigned int i = 0; i < size; i++) + { + unsigned short pix = imgdata[i]; + data4bpp[i] = (pix != params.transparent_color) ? palette->Search(pix) : 0; + } + + for (unsigned int i = 0; i < data.size(); i++) + { + int tilex = i % width; + int tiley = i / width; + + data[i].Set(data4bpp, image.width, tilex, tiley, 0, 4); + data[i].palette = *palette; + } +} + +void Sprite::UsePalette(const PaletteBank& bank) +{ + for (auto& tile : data) + tile.UsePalette(bank); + palette->Set(bank.colors); +} + +void Sprite::WriteTile(unsigned char* arr, int x, int y) const +{ + int index = y * width + x; + const GBATile& tile = data[index]; + for (unsigned int i = 0; i < TILE_SIZE; i++) + { + arr[i] = tile.data[i]; + } +} + +void Sprite::WriteExport(std::ostream& file) const +{ + if (params.bpp == 4) + WriteDefine(file, export_name, "_PALETTE", palette_bank, 12); + WriteDefine(file, export_name, "_SHAPE", shape, 14); + WriteDefine(file, export_name, "_SIZE", size, 14); + WriteDefine(file, export_name, "_ID", offset | (params.for_bitmap ? 512 : 0)); + WriteNewLine(file); +} + +void Sprite::WriteData(std::ostream& file) const +{ + +} + +std::ostream& operator<<(std::ostream& file, const Sprite& sprite) +{ + for (unsigned int i = 0; i < sprite.data.size(); i++) + { + file << sprite.data[i]; + if (i != sprite.data.size() - 1) + file << ",\n\t"; + } + return file; +} + +bool BlockSize::operator==(const BlockSize& rhs) const +{ + return width == rhs.width && height == rhs.height; +} + +bool BlockSize::operator<(const BlockSize& rhs) const +{ + if (width != rhs.width) + return width < rhs.width; + else + return height < rhs.height; +} + +std::vector BlockSize::BiggerSizes(const BlockSize& b) +{ + switch(b.Size()) + { + case 1: + return {BlockSize(2, 1), BlockSize(1, 2)}; + case 2: + if (b.width == 2) + return {BlockSize(4, 1), BlockSize(2, 2)}; + else + return {BlockSize(1, 4), BlockSize(2, 2)}; + case 4: + if (b.width == 4) + return {BlockSize(4, 2)}; + else if (b.height == 4) + return {BlockSize(2, 4)}; + else + return {BlockSize(4, 2), BlockSize(2, 4)}; + case 8: + return {BlockSize(4, 4)}; + case 16: + return {BlockSize(8, 4), BlockSize(4, 8)}; + case 32: + return {BlockSize(8, 8)}; + default: + return {}; + } +} + +Block Block::VSplit() +{ + size.height /= 2; + return Block(x, y + size.height, size); +} + +Block Block::HSplit() +{ + size.width /= 2; + return Block(x + size.width, y, size); +} + +Block Block::Split(const BlockSize& to_this_size) +{ + if (size.width == to_this_size.width) + return VSplit(); + else if (size.height == to_this_size.height) + return HSplit(); + else + FatalLog("Error Block::Split this shouldn't happen"); + + return Block(); +} + +bool SpriteCompare(const Image* l, const Image* r) +{ + const Sprite* lhs = dynamic_cast(l); + const Sprite* rhs = dynamic_cast(r); + if (lhs->Size() != rhs->Size()) + return lhs->Size() > rhs->Size(); + else + // Special case 2x2 should be of lesser priority than 4x1/1x4 + // This is because 2x2's don't care how a 4x4 block is split they are happy with a 4x2 or 2x4 + // Whereas 4x1 must get 4x2 and 1x4 must get 2x4. + return lhs->width + lhs->height > rhs->width + rhs->height; +} + +SpriteSheet::SpriteSheet(const std::vector& _sprites, const std::string& _name, int _bpp, bool _spriteSheetGiven) : + name(_name), bpp(_bpp), spriteSheetGiven(_spriteSheetGiven), sprites(_sprites) +{ + width = bpp == 4 ? 32 : 16; + height = !params.for_bitmap ? 32 : 16; + data.resize(width * 8 * height * 8); +} + +void SpriteSheet::Compile() +{ + PlaceSprites(); + for (const auto& block : placedBlocks) + { + for (unsigned int i = 0; i < block.size.height; i++) + { + for (unsigned int j = 0; j < block.size.width; j++) + { + int x = block.x + j; + int y = block.y + i; + int index = y * width + x; + const Sprite* sprite = sprites[block.sprite_id]; + sprite->WriteTile(data.data() + index * 64, j, i); + } + } + } +} + +void SpriteSheet::WriteData(std::ostream& file) const +{ + if (bpp == 8) + WriteShortArray(file, name, "", data, 8); + else + WriteShortArray4Bit(file, name, "", data, 8); + WriteNewLine(file); +} + +void SpriteSheet::WriteExport(std::ostream& file) const +{ + unsigned int size = data.size() / (bpp == 4 ? 4 : 2); + WriteExtern(file, "const unsigned short", name, "", size); + WriteDefine(file, name, "_SIZE", size); + WriteNewLine(file); + + if (!spriteSheetGiven) + { + for (const auto& sprite : sprites) + sprite->WriteExport(file); + } +} + +void SpriteSheet::PlaceSprites() +{ + if (spriteSheetGiven) + { + for (unsigned int i = 0; i < sprites.size(); i++) + { + Sprite& sprite = *sprites[i]; + Block allocd(i % width, i / width, BlockSize(1, 1)); + allocd.sprite_id = i; + sprite.offset = (allocd.y * width + allocd.x) * (params.bpp == 4 ? 1 : 2); + placedBlocks.push_back(allocd); + } + return; + } + + // Gimme some blocks. + BlockSize size(8, 8); + for (unsigned int y = 0; y < height; y += 8) + { + for (unsigned int x = 0; x < width; x += 8) + { + freeBlocks[size].push_back(Block(x, y, size)); + } + } + + // Sort by request size + std::sort(sprites.begin(), sprites.end(), SpriteCompare); + + for (unsigned int i = 0; i < sprites.size(); i++) + { + Sprite& sprite = *sprites[i]; + BlockSize size(sprite.width, sprite.height); + std::list slice; + + // Mother may I have block of this size? + if (AssignBlockIfAvailable(size, sprite, i)) + continue; + + if (size.isBiggestSize()) + FatalLog("Out of sprite memory could not allocate sprite %s size (%d,%d)", sprite.name.c_str(), sprite.width, sprite.height); + + slice.push_front(size); + while (!HasAvailableBlock(size)) + { + std::vector sizes = BlockSize::BiggerSizes(size); + if (sizes.empty()) + FatalLog("Out of sprite memory could not allocate sprite %s size (%d,%d)", sprite.name.c_str(), sprite.width, sprite.height); + + // Default next search size will be last. + size = sizes.back(); + for (auto& new_size : sizes) + { + if (HasAvailableBlock(new_size)) + { + size = new_size; + break; + } + } + if (!HasAvailableBlock(size)) + slice.push_front(size); + } + + if (!HasAvailableBlock(size)) + FatalLog("Out of sprite memory could not allocate sprite %s size (%d,%d)", sprite.name.c_str(), sprite.width, sprite.height); + + SliceBlock(size, slice); + + size = BlockSize(sprite.width, sprite.height); + // Mother may I have block of this size? + if (AssignBlockIfAvailable(size, sprite, i)) + continue; + else + FatalLog("Out of sprite memory could not allocate sprite %s size (%d,%d)", sprite.name.c_str(), sprite.width, sprite.height); + } +} + +bool SpriteSheet::AssignBlockIfAvailable(BlockSize& size, Sprite& sprite, unsigned int i) +{ + if (HasAvailableBlock(size)) + { + // Yes you may deary. + Block allocd = freeBlocks[size].front(); + freeBlocks[size].pop_front(); + allocd.sprite_id = i; + sprite.offset = (allocd.y * width + allocd.x) * (params.bpp == 4 ? 1 : 2); + placedBlocks.push_back(allocd); + return true; + } + + return false; +} + +bool SpriteSheet::HasAvailableBlock(const BlockSize& size) +{ + return !freeBlocks[size].empty(); +} + +void SpriteSheet::SliceBlock(const BlockSize& size, const std::list& slice) +{ + Block toSplit = freeBlocks[size].front(); + freeBlocks[size].pop_front(); + + for (const auto& split_size : slice) + { + Block other = toSplit.Split(split_size); + freeBlocks[split_size].push_back(other); + } + + freeBlocks[toSplit.size].push_front(toSplit); +} + +SpriteScene::SpriteScene(const std::vector& images, const std::string& _name, bool _is2d, int _bpp) : Scene(_name), bpp(_bpp), paletteBanks(name), + is2d(_is2d), spriteSheetGiven(false) +{ + if (images.size() == 1 && (images[0].width > 64 || images[0].height > 64)) + { + spriteSheetGiven = true; + is2d = !(images[0].width == 8 || images[0].height == 8); + WarnLog("[WARNING] Spritesheet detected.\n" + "If you formed your sprites in a single sprite sheet please note that \n" + "the program will automatically build the spritesheet for you and export.\n" + "Just pass in the images you want to use as sprites and let me do the rest.\n" + "Override: using sprite mode %s.", (is2d ? "2D" : "1D")); + InitSpriteSheet(images[0]); + return; + } + + switch(bpp) + { + case 4: + Init4bpp(images); + break; + default: + Init8bpp(images); + break; + } +} + +SpriteScene::SpriteScene(const std::vector& images, const std::string& _name, bool _is2d, std::shared_ptr& _palette) : + Scene(_name), bpp(8), palette(_palette), paletteBanks(name), is2d(_is2d), spriteSheetGiven(false) +{ + Init8bpp(images); +} + +SpriteScene::SpriteScene(const std::vector& images, const std::string& _name, bool _is2d, const std::vector& _paletteBanks) : + Scene(_name), bpp(4), paletteBanks(name, _paletteBanks), is2d(_is2d), spriteSheetGiven(false) +{ + Init4bpp(images); +} + +void SpriteScene::Build() +{ + if (is2d) + { + std::vector sprites; + for (const auto& image : images) + { + Sprite* sprite = dynamic_cast(image.get()); + if (!sprite) FatalLog("Could not cast Image to Sprite. This shouldn't happen"); + sprites.push_back(sprite); + } + spriteSheet.reset(new SpriteSheet(sprites, name, bpp, spriteSheetGiven)); + spriteSheet->Compile(); + } + else + { + unsigned int offset = 0; + for (unsigned int k = 0; k < images.size(); k++) + { + Sprite* sprite = dynamic_cast(images[k].get()); + if (!sprite) FatalLog("Could not cast Image to Sprite. This shouldn't happen"); + sprite->offset = offset; + offset += sprite->width * sprite->height * (bpp == 8 ? 2 : 1); + } + } +} + +const Sprite& SpriteScene::GetSprite(int index) const +{ + const Sprite* sprite = dynamic_cast(images[index].get()); + if (!sprite) FatalLog("Could not cast Image to Sprite. This shouldn't happen"); + return *sprite; +} + +unsigned int SpriteScene::Size() const +{ + unsigned int total = 0; + for (const auto& image : images) + total += image->width * image->height; + + return total * (bpp == 4 ? TILE_SIZE_SHORTS_4BPP : TILE_SIZE_SHORTS_8BPP); +} + +void SpriteScene::WriteData(std::ostream& file) const +{ + if (bpp == 4) + paletteBanks.WriteData(file); + else + palette->WriteData(file); + + if (is2d) + { + spriteSheet->WriteData(file); + } + else + { + file << "const unsigned short " << name << "[" << Size() << "] =\n{\n\t"; + for (unsigned int i = 0; i < images.size(); i++) + { + file << GetSprite(i); + if (i != images.size() - 1) + file << ",\n\t"; + } + file << "\n};\n"; + WriteNewLine(file); + } +} + +void SpriteScene::WriteExport(std::ostream& file) const +{ + WriteDefine(file, name, "_PALETTE_TYPE", bpp == 8, 13); + WriteDefine(file, name, "_DIMENSION_TYPE", !is2d, 6); + WriteNewLine(file); + + if (bpp == 4) + paletteBanks.WriteExport(file); + else + palette->WriteExport(file); + + + if (is2d) + { + spriteSheet->WriteExport(file); + } + else + { + WriteExtern(file, "const unsigned short", name, "", Size()); + WriteDefine(file, name, "_SIZE", Size()); + WriteNewLine(file); + + if (!spriteSheetGiven) + { + for (const auto& sprite : images) + sprite->WriteExport(file); + } + } +} + +bool SpritePaletteSizeComp(const Sprite* i, const Sprite* j) +{ + return i->palette->Size() > j->palette->Size(); +} + +void SpriteScene::Init4bpp(const std::vector& images16) +{ + // Form sprites + std::vector sprites; + for (const auto& image : images16) + sprites.push_back(new Sprite(image, bpp)); + + // Palette bank selection time + // Ensure image contains < 256 colors + std::set bigPalette; + for (const auto& sprite : sprites) + { + const std::vector& sprite_palette = sprite->palette->colors; + bigPalette.insert(sprite_palette.begin(), sprite_palette.end()); + } + + if (bigPalette.size() > 256 && !params.force) + FatalLog("Image after reducing sprites to 4bpp still contains more than 256 distinct colors. Found %d colors. Please fix.", bigPalette.size()); + + // Greedy approach deal with tiles with largest palettes first. + std::sort(sprites.begin(), sprites.end(), SpritePaletteSizeComp); + + // But deal with transparent color + for (unsigned int i = 0; i < paletteBanks.Size(); i++) + paletteBanks[i].Add(Color(params.transparent_color)); + + // Construct palette banks, assign bank id to tile, remap sprite to palette bank given, assign tile ids + for (auto& sprite : sprites) + { + int pbank = -1; + // Fully contains checks + for (unsigned int i = 0; i < paletteBanks.Size(); i++) + { + PaletteBank& bank = paletteBanks[i]; + if (bank.Contains(*sprite->palette)) + pbank = i; + } + + // Ok then find least affected bank + if (pbank == -1) + { + int max_colors_left = -1; + for (unsigned int i = 0; i < paletteBanks.Size(); i++) + { + PaletteBank& bank = paletteBanks[i]; + int colors_left = bank.CanMerge(*sprite->palette); + if (colors_left != 0 && max_colors_left < colors_left) + { + max_colors_left = colors_left; + pbank = i; + } + } + } + // Cry and die for now. Unless you tell me to keep going. + if (pbank == -1 && !params.force) + FatalLog("More than 16 distinct palettes found, please use 8bpp mode."); + + // Merge step and assign palette bank + paletteBanks[pbank].Merge(*sprite->palette); + sprite->palette_bank = pbank; + sprite->UsePalette(paletteBanks[pbank]); + + // Add to images list + images.emplace_back(sprite); + } +} + +void SpriteScene::Init8bpp(const std::vector& images16) +{ + if (!palette) + { + Image8BppScene scene(images16, name); + palette = scene.palette; + } + + for (const auto& image : images16) + images.emplace_back(new Sprite(image, palette)); +} + +void SpriteScene::InitSpriteSheet(const Image16Bpp& sheet) +{ + std::vector tiles; + int tilesX = sheet.width / 8; + int tilesY = sheet.height / 8; + tiles.reserve(tilesX * tilesY); + for (int y = 0; y < tilesY; y++) + { + for (int x = 0; x < tilesX; x++) + { + tiles.push_back(sheet.SubImage(x * 8, y * 8, 8, 8)); + } + } + + if (bpp == 4) + Init4bpp(tiles); + else + Init8bpp(tiles); +} diff --git a/shared/reductionhelper.hpp b/shared/reductionhelper.hpp new file mode 100644 index 0000000..31e0508 --- /dev/null +++ b/shared/reductionhelper.hpp @@ -0,0 +1,509 @@ +#ifndef REDUCTION_HELPER_HPP +#define REDUCTION_HELPER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define TILE_SIZE 64 +#define PALETTE_SIZE 16 +#define TILE_SIZE_BYTES_8BPP 64 +#define TILE_SIZE_BYTES_4BPP 32 +#define TILE_SIZE_SHORTS_8BPP 32 +#define TILE_SIZE_SHORTS_4BPP 16 +#define TOTAL_TILE_MEMORY_BYTES 65536 +#define SIZE_CBB_BYTES (8192 * 2) +#define SIZE_SBB_BYTES (1024 * 2) +#define SIZE_SBB_SHORTS 1024 + +#include "color.hpp" +#include "Logger.hpp" + +class Exportable +{ + public: + Exportable() {} + virtual ~Exportable() {}; + virtual void WriteData(std::ostream& file) const = 0; + virtual void WriteExport(std::ostream& file) const = 0; +}; + +class Image : public Exportable +{ + public: + Image(unsigned int _width, unsigned int _height, const std::string& _name = "", const std::string& _filename = "", unsigned int _frame = 0, bool _animated = false) : + width(_width), height(_height), name(_name), filename(_filename), frame(_frame), animated(_animated) + { + std::stringstream oss(name); + if (animated) oss << frame; + export_name = oss.str(); + } + Image(const Image& image) : width(image.width), height(image.height), name(image.name), filename(image.filename), frame(image.frame), + animated(image.animated), export_name(image.export_name) {} + virtual ~Image() {} + unsigned int width; + unsigned int height; + std::string name; + std::string filename; + unsigned int frame; + bool animated; + std::string export_name; + ///TODO include pixels array +}; + +class Scene : public Exportable +{ + public: + Scene(const std::string& _name) : name(_name) {} + virtual ~Scene() {} + const std::vector>& GetImages() const {return images;} + virtual void WriteData(std::ostream& file) const + { + for (const auto& image : images) + image->WriteData(file); + } + virtual void WriteExport(std::ostream& file) const + { + for (const auto& image : images) + image->WriteExport(file); + } + std::string name; + protected: + std::vector> images; +}; + +class Image32Bpp : public Image +{ + public: + Image32Bpp(const Magick::Image& image, const std::string& name, const std::string& filename, unsigned int frame, bool animated); + unsigned int Size() const {return width * height;}; + void WriteData(std::ostream& file) const; + void WriteExport(std::ostream& file) const; + bool has_alpha; + std::vector pixels; +}; + +class Image16Bpp : public Image +{ + public: + Image16Bpp(const Image32Bpp& image); + Image16Bpp(unsigned int width, unsigned int height) : Image(width, height) {} + void GetColors(std::vector& colors) const; + void GetColors(std::vector::iterator& color_ptr) const; + unsigned int Size() const {return width * height;}; + void WriteData(std::ostream& file) const; + void WriteExport(std::ostream& file) const; + Image16Bpp SubImage(unsigned int x, unsigned int y, unsigned int width, unsigned int height) const; + std::vector pixels; +}; + +class ColorArray +{ + public: + ColorArray(const std::vector& _colors = std::vector()) : colors(_colors), colorSet(colors.begin(), colors.end()) {} + void Set(const std::vector& _colors); + Color At(int index) const {return colors[index];} + int Search(const Color& color) const; + int Search(unsigned short color) const; + bool Contains(const ColorArray& palette) const; + void Add(const Color& c); + unsigned int Size() const {return colors.size();}; + std::vector colors; + std::set colorSet; + protected: + mutable std::map colorIndexCache; +}; + +class Palette : public ColorArray, public Exportable +{ + public: + Palette() {} // Used by Tile constructor + Palette(const std::vector& _colors, const std::string& _name); + void WriteData(std::ostream& file) const; + void WriteExport(std::ostream& file) const; + std::string name; +}; + +class Image8Bpp : public Image +{ + public: + Image8Bpp(const Image16Bpp& image); + Image8Bpp(const Image16Bpp& image, std::shared_ptr& global_palette); + unsigned int Size() const {return width * height / 2;}; + void WriteData(std::ostream& file) const; + void WriteExport(std::ostream& file) const; + std::vector pixels; + std::shared_ptr palette; + private: + bool export_shared_info; +}; + +class Image8BppScene : public Scene +{ + public: + Image8BppScene(const std::vector& images, const std::string& name); + const Image8Bpp& GetImage(int index) const; + void WriteData(std::ostream& file) const; + void WriteExport(std::ostream& file) const; + std::shared_ptr palette; +}; + +class PaletteBank : public ColorArray +{ + public: + PaletteBank(int _id = -1) : id(_id) {}; + PaletteBank(const std::vector& _colors, int id); + int CanMerge(const ColorArray& palette) const; + void Merge(const ColorArray& palette); + int id; +}; + +class PaletteBankManager : public Exportable +{ + public: + PaletteBankManager(const std::string& _name); + PaletteBankManager(const std::string& _name, const std::vector& paletteBanks); + PaletteBank& operator[](int i) {return banks[i];} + const PaletteBank& operator[](int i) const {return banks[i];} + unsigned int Size() const {return banks.size();} + void WriteData(std::ostream& file) const; + void WriteExport(std::ostream& file) const; + std::string name; + std::vector banks; +}; + +// TODO rewrite class passing in images to clean up. +template +class Tile +{ + public: + typedef Tile GBATile; + typedef Tile ImageTile; + + Tile(); + Tile(const Tile& tile); + Tile(const std::vector& image, int pitch, int tilex, int tiley, int border = 0, int bpp = 8); + Tile(const T* image, int pitch, int tilex, int tiley, int border = 0, int bpp = 8); + Tile(std::shared_ptr& imageTile, int bpp); + // Constructs tile from image using global palette. + Tile(const Image16Bpp& image, const Palette& global_palette, int tilex, int tiley); + ~Tile() {}; + void Set(const std::vector& image, int pitch, int tilex, int tiley, int border = 0, int bpp = 8); + void Set(const T* image, int pitch, int tilex, int tiley, int border = 0, int bpp = 8); + // 8bpp Set. Constructs tile from image using global palette. + void Set(const Image16Bpp& image, const Palette& global_palette, int tilex, int tiley); + void UsePalette(const PaletteBank& bank); + bool IsEqual(const Tile& other) const; + bool IsSameAs(const Tile& other) const; + bool operator<(const Tile& other) const; + bool operator==(const Tile& other) const; + int id; + std::vector data; + int bpp; + unsigned char palette_bank; + Palette palette; + std::shared_ptr sourceTile; + + template + friend std::ostream& operator<<(std::ostream& file, const Tile& tile); +}; + +typedef Tile GBATile; +typedef Tile ImageTile; + +class Tileset : public Exportable +{ + public: + Tileset(const std::vector& images, const std::string& name, int bpp); + Tileset(const Image16Bpp& image, int bpp); + int Search(const GBATile& tile) const; + int Search(const ImageTile& tile) const; + // Match Imagetile to GBATile (only for bpp = 4) + bool Match(const ImageTile& tile, int& tile_id, int& pal_id) const; + unsigned int Size() const {return tiles.size() * ((bpp == 4) ? TILE_SIZE_SHORTS_4BPP : (bpp == 8) ? TILE_SIZE_SHORTS_8BPP : 1);}; + void WriteData(std::ostream& file) const; + void WriteExport(std::ostream& file) const; + std::string name; + int bpp; + // Only one of two will be used bpp = 4 or 8: tiles 16: itiles + std::set tiles; + std::set itiles; + // Bookkeeping matcher used when bpp = 4 or 8 + std::map matcher; + // Tiles sorted by id for export. + std::vector tilesExport; + // Only one max will be used bpp = 4: paletteBanks 8: palette 16: neither + std::shared_ptr palette; + PaletteBankManager paletteBanks; + // Only valid for bpp = 4 and 8 + std::vector offsets; + private: + void Init4bpp(const std::vector& images); + void Init8bpp(const std::vector& images); + void Init16bpp(const std::vector& images); +}; + +class Map : public Image +{ + public: + Map(const Image16Bpp& image, int bpp); + Map(const Image16Bpp& image, std::shared_ptr& global_tileset); + unsigned int Size() const {return data.size();}; + unsigned int Type() const {return (width > 32 ? 1 : 0) | (height > 32 ? 1 : 0) << 1;}; + void WriteData(std::ostream& file) const; + void WriteExport(std::ostream& file) const; + std::vector data; + std::shared_ptr tileset; + private: + void Init4bpp(const Image16Bpp& image); + void Init8bpp(const Image16Bpp& image); + bool export_shared_info; +}; + +class MapScene : public Scene +{ + public: + MapScene(const std::vector& images, const std::string& name, int bpp); + MapScene(const std::vector& images, const std::string& name, std::shared_ptr& tileset); + void WriteData(std::ostream& file) const; + void WriteExport(std::ostream& file) const; + std::shared_ptr tileset; +}; + +class Sprite : public Image +{ + public: + Sprite(const Image16Bpp& image, std::shared_ptr& global_palette); + Sprite(const Image16Bpp& image, int bpp); + unsigned int Size() const {return width * height;}; + void UsePalette(const PaletteBank& bank); + void WriteData(std::ostream& file) const; + void WriteExport(std::ostream& file) const; + void WriteTile(unsigned char* arr, int x, int y) const; + std::vector data; + std::shared_ptr palette; + int palette_bank; + int size; + int shape; + int offset; + + friend std::ostream& operator<<(std::ostream& file, const Sprite& sprite); +}; + +class BlockSize +{ + public: + BlockSize() : width(0), height(0) {}; + BlockSize(unsigned int _width, unsigned int _height) : width(_width), height(_height) {}; + bool operator==(const BlockSize& rhs) const; + bool operator<(const BlockSize& rhs) const; + unsigned int Size() const {return width * height;}; + bool isBiggestSize() const {return width == 8 and height == 8;}; + static std::vector BiggerSizes(const BlockSize& b); + unsigned int width, height; + +}; + +class Block +{ + public: + Block() : x(0), y(0), sprite_id(-1) {}; + Block(int width, int height) : size(width, height), x(0), y(0), sprite_id(-1) {}; + Block(const BlockSize& _size) : size(_size), x(0), y(0), sprite_id(-1) {}; + Block(int _x, int _y, int width, int height) : size(width, height), x(_x), y(_y), sprite_id(-1) {}; + Block(int _x, int _y, const BlockSize& _size) : size(_size), x(_x), y(_y), sprite_id(-1) {}; + Block HSplit(); + Block VSplit(); + Block Split(const BlockSize& to_this_size); + BlockSize size; + int x; + int y; + int sprite_id; +}; + +class SpriteSheet +{ + public: + SpriteSheet(const std::vector& sprites, const std::string& name, int bpp, bool spriteSheetGiven); + void Compile(); + void WriteData(std::ostream& file) const; + void WriteExport(std::ostream& file) const; + std::map> freeBlocks; + std::list placedBlocks; + std::vector data; + unsigned int width, height; + std::string name; + int bpp; + bool spriteSheetGiven; + private: + void PlaceSprites(); + bool AssignBlockIfAvailable(BlockSize& size, Sprite& sprite, unsigned int i); + bool HasAvailableBlock(const BlockSize& size); + void SliceBlock(const BlockSize& size, const std::list& slice); + // Not owned by this object, but SpriteScene. + std::vector sprites; +}; + +class SpriteScene : public Scene +{ + public: + SpriteScene(const std::vector& images, const std::string& name, bool is2d, int bpp); + SpriteScene(const std::vector& images, const std::string& name, bool is2d, std::shared_ptr& tileset); + SpriteScene(const std::vector& images, const std::string& name, bool is2d, const std::vector& paletteBanks); + void Build(); + const Sprite& GetSprite(int index) const; + void WriteData(std::ostream& file) const; + void WriteExport(std::ostream& file) const; + unsigned int Size() const; + int bpp; + // Only one max will be used bpp = 4: paletteBanks 8: palette + std::shared_ptr palette; + PaletteBankManager paletteBanks; + // Used if is2d is true + std::unique_ptr spriteSheet; + bool is2d; + // Special for use spritesheet mode + bool spriteSheetGiven; + private: + void Init4bpp(const std::vector& images); + void Init8bpp(const std::vector& images); + void InitSpriteSheet(const Image16Bpp& image); +}; + +template +Tile::Tile() : id(0), data(TILE_SIZE), bpp(8), palette_bank(0) +{ +} + +template +Tile::Tile(const Tile& tile) : id(tile.id), data(tile.data), bpp(tile.bpp), palette_bank(tile.palette_bank), palette(tile.palette), sourceTile(tile.sourceTile) +{ +} + +template +Tile::Tile(std::shared_ptr& imageTile, int bpp) +{ + FatalLog("Code error not implemented"); +} + +template +Tile::Tile(const Image16Bpp& image, const Palette& global_palette, int tilex, int tiley) : data(TILE_SIZE), bpp(8) +{ + Set(image, global_palette, tilex, tiley); +} + +template +Tile::Tile(const std::vector& image, int pitch, int tilex, int tiley, int border, int bpp) : data(TILE_SIZE) +{ + Set(image, pitch, tilex, tiley, border, bpp); +} + +template +Tile::Tile(const T* image, int pitch, int tilex, int tiley, int border, int bpp) : data(TILE_SIZE) +{ + Set(image, pitch, tilex, tiley, border, bpp); +} + +template +void Tile::Set(const std::vector& image, int pitch, int tilex, int tiley, int border, int bpp) +{ + this->bpp = bpp; + T* ptr = data.data(); + + for (int i = 0; i < 8; i++) + { + for (int j = 0; j < 8; j++) + { + ptr[i * 8 + j] = image[(tiley * (8+border) + i) * pitch + tilex * (8+border) + j]; + } + } +} + +template +void Tile::Set(const T* image, int pitch, int tilex, int tiley, int border, int bpp) +{ + this->bpp = bpp; + T* ptr = data.data(); + + for (int i = 0; i < 8; i++) + { + for (int j = 0; j < 8; j++) + { + ptr[i * 8 + j] = image[(tiley * (8+border) + i) * pitch + tilex * (8+border) + j]; + } + } +} + +template +void Tile::Set(const Image16Bpp& image, const Palette& global_palette, int tilex, int tiley) +{ + FatalLog("Code error not implemented"); +} + +template +void Tile::UsePalette(const PaletteBank& bank) +{ + FatalLog("Code error not implemented"); +} + +template +bool Tile::IsEqual(const Tile& other) const +{ + return data == other.data; +} + +template +bool Tile::operator==(const Tile& other) const +{ + return IsEqual(other); +} + +template +bool Tile::operator<(const Tile& other) const +{ + return data < other.data; +} + +template +bool Tile::IsSameAs(const Tile& other) const +{ + bool same, sameh, samev, samevh; + same = sameh = samev = samevh = true; + for (int i = 0; i < TILE_SIZE; i++) + { + int x = i % 8; + int y = i / 8; + same = same && data[i] == other.data[i]; + samev = samev && data[i] == other.data[(7 - y) * 8 + x]; + sameh = sameh && data[i] == other.data[y * 8 + (7 - x)]; + samevh = samevh && data[i] == other.data[(7 - y) * 8 + (7 - x)]; + } + return same || samev || sameh || samevh; +} + +template +std::ostream& operator<<(std::ostream& file, const Tile& tile) +{ + + file << std::hex; + for (unsigned int i = 0; i < 8; i++) + { + for (unsigned int j = 0; j < 8; j++) + file << tile.data[i * 8 + j] << " "; + file << std::endl; + } + file << std::dec; + return file; +} + +bool TilesPaletteSizeComp(const GBATile& i, const GBATile& j); + +#endif diff --git a/shared/shared.cpp b/shared/shared.cpp new file mode 100644 index 0000000..1ac3c4e --- /dev/null +++ b/shared/shared.cpp @@ -0,0 +1,52 @@ +#include "shared.hpp" + +#include +#include +#include +#include + +std::string ToUpper(const std::string& str) +{ + std::string cap = str; + transform(cap.begin(), cap.end(), cap.begin(), (int(*)(int)) std::toupper); + return cap; +} + +void split(const std::string& s, char delimiter, std::vector& tokens) +{ + std::stringstream ss(s); + std::string item; + while(std::getline(ss, item, delimiter)) + tokens.push_back(item); +} + +std::string Chop(const std::string& filename) +{ + return wxFileName(filename).GetName().ToStdString(); +} + +std::string Sanitize(const std::string& filename) +{ + std::stringstream out; + for (unsigned int i = 0; i < filename.size(); i++) + { + if ((filename[i] >= 'A' && filename[i] <= 'Z') || + (filename[i] >= 'a' && filename[i] <= 'z') || + (filename[i] >= '0' && filename[i] <= '9' && i != 0) || + filename[i] == '_') + out.put(filename[i]); + } + return out.str(); +} + +std::string Format(const std::string& file) +{ + return Sanitize(Chop(file)); +} + +unsigned int log2(unsigned int x) +{ + unsigned int result = 0; + while (x >>= 1) result++; + return result; +} diff --git a/shared/shared.hpp b/shared/shared.hpp new file mode 100644 index 0000000..f664bb8 --- /dev/null +++ b/shared/shared.hpp @@ -0,0 +1,71 @@ +#ifndef SHARED_HPP +#define SHARED_HPP + +#include +#include + +#include + +#include "headerfile.hpp" +#include "implementationfile.hpp" +#include "reductionhelper.hpp" + +struct resize +{ + resize(int w = -1, int h = -1) : width(w), height(h) {} + int width; + int height; + bool IsValid() const {return width > 0 && height > 0;} +}; + +struct ExportParams +{ + // General Export stuff + std::string mode; + std::string device; + std::string output_dir; + std::string filename; // Full path to exported file. + std::string symbol_base_name; // base name of generated symbols _palette, _map etc. + + std::vector files; + std::vector tilesets; + std::vector images; + std::vector tileset_images; + std::vector names; // In batch names of the arrays for the images. if -names is given then it becomes those. + + // Optional stuff + std::vector resizes; + int transparent_color; + + // Palette options + unsigned int offset; + std::vector weights; + bool dither; + float dither_level; + unsigned int palette; + bool split; + int bpp; + + // Tile/map stuff + int split_sbb; + int border; + bool force; + bool reduce; + + // Sprite stuff + bool for_bitmap; + bool export_2d; + + std::vector GetImages() const; + std::vector GetTilesets() const; +}; + +std::string ToUpper(const std::string& str); +std::string Chop(const std::string& filename); +std::string Sanitize(const std::string& filename); +std::string Format(const std::string& filename); +unsigned int log2(unsigned int x); + +extern ExportParams params; + +#endif diff --git a/shared/version.h b/shared/version.h new file mode 100644 index 0000000..e7a1f1e --- /dev/null +++ b/shared/version.h @@ -0,0 +1,33 @@ +#ifndef VERSION_H +#define VERSION_H + +namespace AutoVersion{ + + //Date Version Types + static const char DATE[] = "1"; + static const char MONTH[] = "1"; + static const char YEAR[] = "2014"; + static const char UBUNTU_VERSION_STYLE[] = "14.01"; + + //Software Status + static const char STATUS[] = "Alpha"; + static const char STATUS_SHORT[] = "a"; + + //Standard Version Type + static const long MAJOR = 1; + static const long MINOR = 0; + static const long BUILD = 0; + static const long REVISION = 0; + + //Miscellaneous Version Types + static const long BUILDS_COUNT = 0; + #define RC_FILEVERSION 1,0,0,0 + #define RC_FILEVERSION_STRING "1, 0, 0, 0\0" + static const char FULLVERSION_STRING[] = "1.0.0.0"; + + //These values are to keep track of your versioning state, don't modify them. + static const long BUILD_HISTORY = 0; + + +} +#endif //VERSION_H