diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f61f7a8a..bd187258d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2066,6 +2066,7 @@ set_src(TOOLS GLOB src/tools map_convert_07.cpp map_diff.cpp map_extract.cpp + map_optimize.cpp map_replace_image.cpp map_resave.cpp packetgen.cpp @@ -2078,7 +2079,7 @@ foreach(ABS_T ${TOOLS}) string(REGEX REPLACE "\\.cpp$" "" TOOL "${T}") set(TOOL_DEPS ${DEPS}) set(TOOL_LIBS ${LIBS}) - if(TOOL MATCHES "^(tileset_.*|dilate|map_convert_07|map_extract|map_replace_image)$") + if(TOOL MATCHES "^(tileset_.*|dilate|map_convert_07|map_optimize|map_extract|map_replace_image)$") list(APPEND TOOL_DEPS ${PNGLITE_DEP}) list(APPEND TOOL_LIBS ${PNGLITE_LIBRARIES}) list(APPEND TOOL_INCLUDE_DIRS ${PNGLITE_INCLUDE_DIRS}) diff --git a/src/engine/shared/datafile.cpp b/src/engine/shared/datafile.cpp index 1a284a93e..6cddbb871 100644 --- a/src/engine/shared/datafile.cpp +++ b/src/engine/shared/datafile.cpp @@ -10,8 +10,6 @@ #include "uuid_manager.h" -#include - static const int DEBUG = 0; enum @@ -692,7 +690,7 @@ int CDataFileWriter::AddItem(int Type, int ID, int Size, void *pData) return m_NumItems - 1; } -int CDataFileWriter::AddData(int Size, void *pData) +int CDataFileWriter::AddData(int Size, void *pData, int CompressionLevel) { dbg_assert(m_NumDatas < 1024, "too much data"); @@ -700,7 +698,7 @@ int CDataFileWriter::AddData(int Size, void *pData) unsigned long s = compressBound(Size); void *pCompData = malloc(s); // temporary buffer that we use during compression - int Result = compress((Bytef *)pCompData, &s, (Bytef *)pData, Size); // ignore_convention + int Result = compress2((Bytef *)pCompData, &s, (Bytef *)pData, Size, CompressionLevel); // ignore_convention if(Result != Z_OK) { dbg_msg("datafile", "compression error %d", Result); diff --git a/src/engine/shared/datafile.h b/src/engine/shared/datafile.h index 0f3950946..347b7a817 100644 --- a/src/engine/shared/datafile.h +++ b/src/engine/shared/datafile.h @@ -8,6 +8,8 @@ #include #include +#include + // raw datafile access class CDataFileReader { @@ -100,7 +102,7 @@ public: void Init(); bool OpenFile(class IStorage *pStorage, const char *pFilename, int StorageType = IStorage::TYPE_SAVE); bool Open(class IStorage *pStorage, const char *pFilename, int StorageType = IStorage::TYPE_SAVE); - int AddData(int Size, void *pData); + int AddData(int Size, void *pData, int CompressionLevel = Z_DEFAULT_COMPRESSION); int AddDataSwapped(int Size, void *pData); int AddItem(int Type, int ID, int Size, void *pData); int Finish(); diff --git a/src/engine/shared/image_manipulation.cpp b/src/engine/shared/image_manipulation.cpp index 40cd7b456..14c923fba 100644 --- a/src/engine/shared/image_manipulation.cpp +++ b/src/engine/shared/image_manipulation.cpp @@ -2,7 +2,7 @@ #include #include -static void Dilate(int w, int h, int BPP, unsigned char *pSrc, unsigned char *pDest, unsigned char AlphaThreshold = 30) +static void Dilate(int w, int h, int BPP, unsigned char *pSrc, unsigned char *pDest, unsigned char AlphaThreshold = TW_DILATE_ALPHA_THRESHOLD) { int ix, iy; const int aDirX[] = {0, -1, 1, 0}; @@ -67,26 +67,50 @@ static void CopyColorValues(int w, int h, int BPP, unsigned char *pSrc, unsigned } void DilateImage(unsigned char *pImageBuff, int w, int h, int BPP) +{ + DilateImageSub(pImageBuff, w, h, BPP, 0, 0, w, h); +} + +void DilateImageSub(unsigned char *pImageBuff, int w, int h, int BPP, int x, int y, int sw, int sh) { unsigned char *apBuffer[2] = {NULL, NULL}; - apBuffer[0] = (unsigned char *)malloc((size_t)w * h * sizeof(unsigned char) * BPP); - apBuffer[1] = (unsigned char *)malloc((size_t)w * h * sizeof(unsigned char) * BPP); + apBuffer[0] = (unsigned char *)malloc((size_t)sw * sh * sizeof(unsigned char) * BPP); + apBuffer[1] = (unsigned char *)malloc((size_t)sw * sh * sizeof(unsigned char) * BPP); + unsigned char *pBufferOriginal = (unsigned char *)malloc((size_t)sw * sh * sizeof(unsigned char) * BPP); unsigned char *pPixelBuff = (unsigned char *)pImageBuff; - Dilate(w, h, BPP, pPixelBuff, apBuffer[0]); + for(int Y = 0; Y < sh; ++Y) + { + int SrcImgOffset = ((y + Y) * w * BPP) + (x * BPP); + int DstImgOffset = (Y * sw * BPP); + int CopySize = sw * BPP; + mem_copy(&pBufferOriginal[DstImgOffset], &pPixelBuff[SrcImgOffset], CopySize); + } + + Dilate(sw, sh, BPP, pBufferOriginal, apBuffer[0]); for(int i = 0; i < 5; i++) { - Dilate(w, h, BPP, apBuffer[0], apBuffer[1]); - Dilate(w, h, BPP, apBuffer[1], apBuffer[0]); + Dilate(sw, sh, BPP, apBuffer[0], apBuffer[1]); + Dilate(sw, sh, BPP, apBuffer[1], apBuffer[0]); } - CopyColorValues(w, h, BPP, apBuffer[0], pPixelBuff); + CopyColorValues(sw, sh, BPP, apBuffer[0], pBufferOriginal); free(apBuffer[0]); free(apBuffer[1]); + + for(int Y = 0; Y < sh; ++Y) + { + int SrcImgOffset = ((y + Y) * w * BPP) + (x * BPP); + int DstImgOffset = (Y * sw * BPP); + int CopySize = sw * BPP; + mem_copy(&pPixelBuff[SrcImgOffset], &pBufferOriginal[DstImgOffset], CopySize); + } + + free(pBufferOriginal); } static float CubicHermite(float A, float B, float C, float D, float t) diff --git a/src/engine/shared/image_manipulation.h b/src/engine/shared/image_manipulation.h index b6464518a..734d03497 100644 --- a/src/engine/shared/image_manipulation.h +++ b/src/engine/shared/image_manipulation.h @@ -4,7 +4,10 @@ #include #include +#define TW_DILATE_ALPHA_THRESHOLD 30 + void DilateImage(unsigned char *pImageBuff, int w, int h, int BPP); +void DilateImageSub(unsigned char *pImageBuff, int w, int h, int BPP, int x, int y, int sw, int sh); // returned pointer is allocated with malloc uint8_t *ResizeImage(const uint8_t *pImgData, int Width, int Height, int NewWidth, int NewHeight, int BPP); diff --git a/src/tools/map_optimize.cpp b/src/tools/map_optimize.cpp new file mode 100644 index 000000000..ecc7e38fb --- /dev/null +++ b/src/tools/map_optimize.cpp @@ -0,0 +1,322 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void ClearTransparentPixels(uint8_t *pImg, int Width, int Height) +{ + for(int y = 0; y < Height; ++y) + { + for(int x = 0; x < Width; ++x) + { + int Index = y * Width * 4 + x * 4; + if(pImg[Index + 3] == 0) + { + pImg[Index + 0] = 0; + pImg[Index + 1] = 0; + pImg[Index + 2] = 0; + } + } + } +} + +void CopyOpaquePixels(uint8_t *pDestImg, uint8_t *pSrcImg, int Width, int Height) +{ + for(int y = 0; y < Height; ++y) + { + for(int x = 0; x < Width; ++x) + { + int Index = y * Width * 4 + x * 4; + if(pSrcImg[Index + 3] > TW_DILATE_ALPHA_THRESHOLD) + mem_copy(&pDestImg[Index], &pSrcImg[Index], sizeof(uint8_t) * 4); + else + mem_zero(&pDestImg[Index], sizeof(uint8_t) * 4); + } + } +} + +void ClearPixelsTile(uint8_t *pImg, int Width, int Height, int TileIndex) +{ + int WTile = Width / 16; + int HTile = Height / 16; + int xi = (TileIndex % 16) * WTile; + int yi = (TileIndex / 16) * HTile; + + for(int y = yi; y < yi + HTile; ++y) + { + for(int x = xi; x < xi + WTile; ++x) + { + int Index = y * Width * 4 + x * 4; + pImg[Index + 0] = 0; + pImg[Index + 1] = 0; + pImg[Index + 2] = 0; + pImg[Index + 3] = 0; + } + } +} + +void GetImageSHA256(uint8_t *pImgBuff, int ImgSize, int Width, int Height, char *pSHA256Str) +{ + uint8_t *pNewImgBuff = (uint8_t *)malloc(ImgSize); + + // Get all image pixels, that have a alpha threshold over the default threshold, + // all other pixels are cleared to zero. + // This is required since dilate modifies pixels under the alpha threshold, which would alter the SHA. + CopyOpaquePixels(pNewImgBuff, pImgBuff, Width, Height); + SHA256_DIGEST SHAStr = sha256(pNewImgBuff, (size_t)ImgSize); + + sha256_str(SHAStr, pSHA256Str, SHA256_MAXSTRSIZE * sizeof(char)); + + free(pNewImgBuff); +} + +int main(int argc, const char **argv) +{ + dbg_logger_stdout(); + + IStorage *pStorage = CreateStorage("Teeworlds", IStorage::STORAGETYPE_BASIC, argc, argv); + int Index, ID = 0, Type = 0, Size; + void *pPtr; + char aFileName[1024]; + CDataFileReader DataFile; + CDataFileWriter df; + + if(!pStorage || argc <= 1 || argc > 3) + { + dbg_msg("map_optimize", "Invalid parameters or other unknown error."); + dbg_msg("map_optimize", "Usage: map_optimize []"); + return -1; + } + + if(argc == 3) + { + str_format(aFileName, sizeof(aFileName), "out/%s", argv[2]); + + fs_makedir_rec_for(aFileName); + } + else + { + fs_makedir("out"); + char aBuff[MAX_PATH_LENGTH]; + pStorage->StripPathAndExtension(argv[1], aBuff, sizeof(aBuff)); + str_format(aFileName, sizeof(aFileName), "out/%s.map", aBuff); + } + + if(!DataFile.Open(pStorage, argv[1], IStorage::TYPE_ABSOLUTE)) + { + dbg_msg("map_optimize", "Failed to open source file."); + return -1; + } + + if(!df.Open(pStorage, aFileName, IStorage::TYPE_ABSOLUTE)) + { + dbg_msg("map_optimize", "Failed to open target file."); + return -1; + } + + int aImageFlags[64] = { + 0, + }; + + bool aImageTiles[64][256]{ + { + false, + }, + }; + + struct SMapOptimizeItem + { + CMapItemImage *m_pImage; + int m_Index; + int m_Data; + int m_Text; + }; + + std::vector DataFindHelper; + + // add all items + int i = 0; + for(int Index = 0; Index < DataFile.NumItems(); Index++) + { + pPtr = DataFile.GetItem(Index, &Type, &ID); + Size = DataFile.GetItemSize(Index); + // for all layers, check if it uses a image and set the corresponding flag + if(Type == MAPITEMTYPE_LAYER) + { + CMapItemLayer *pLayer = (CMapItemLayer *)pPtr; + if(pLayer->m_Type == LAYERTYPE_TILES) + { + CMapItemLayerTilemap *pTLayer = (CMapItemLayerTilemap *)pLayer; + if(pTLayer->m_Image >= 0 && pTLayer->m_Image < 64 && pTLayer->m_Flags == 0) + { + aImageFlags[pTLayer->m_Image] |= 1; + // check tiles that are used in this image + int DataIndex = pTLayer->m_Data; + unsigned int Size = DataFile.GetDataSize(DataIndex); + void *pTiles = DataFile.GetData(DataIndex); + unsigned int TileSize = sizeof(CTile); + + if(Size >= pTLayer->m_Width * pTLayer->m_Height * TileSize) + { + int x = 0; + int y = 0; + for(y = 0; y < pTLayer->m_Height; ++y) + { + for(x = 0; x < pTLayer->m_Width; ++x) + { + int TileIndex = ((CTile *)pTiles)[y * pTLayer->m_Width + x].m_Index; + if(TileIndex > 0) + { + aImageTiles[pTLayer->m_Image][TileIndex] = true; + } + } + } + } + } + } + else if(pLayer->m_Type == LAYERTYPE_QUADS) + { + CMapItemLayerQuads *pQLayer = (CMapItemLayerQuads *)pLayer; + if(pQLayer->m_Image >= 0 && pQLayer->m_Image < 64) + { + aImageFlags[pQLayer->m_Image] |= 2; + } + } + } + else if(Type == MAPITEMTYPE_IMAGE) + { + CMapItemImage *pImg = (CMapItemImage *)pPtr; + if(!pImg->m_External) + { + SMapOptimizeItem Item; + Item.m_pImage = pImg; + Item.m_Index = i; + Item.m_Data = pImg->m_ImageData; + Item.m_Text = pImg->m_ImageName; + DataFindHelper.push_back(Item); + } + + // found an image + ++i; + } + + df.AddItem(Type, ID, Size, pPtr); + } + + // add all data + for(Index = 0; Index < DataFile.NumData(); Index++) + { + bool DeletePtr = false; + pPtr = DataFile.GetData(Index); + Size = DataFile.GetDataSize(Index); + std::vector::iterator it = std::find_if(DataFindHelper.begin(), DataFindHelper.end(), [Index](const SMapOptimizeItem &Other) -> bool { return Other.m_Data == Index || Other.m_Text == Index; }); + if(it != DataFindHelper.end()) + { + int Width = it->m_pImage->m_Width; + int Height = it->m_pImage->m_Height; + + int ImageIndex = it->m_Index; + if(it->m_Data == Index) + { + DeletePtr = true; + // optimize embedded images + // use a new pointer, to be safe, when using the original image data + void *pNewPtr = malloc(Size); + mem_copy(pNewPtr, pPtr, Size); + pPtr = pNewPtr; + uint8_t *pImgBuff = (uint8_t *)pPtr; + + bool DoClearTransparentPixels = false; + bool DilateAs2DArray = false; + bool DoDilate = false; + + // all tiles that aren't used are cleared(if image was only used by tilemap) + if(aImageFlags[ImageIndex] == 1) + { + for(int i = 0; i < 256; ++i) + { + if(!aImageTiles[ImageIndex][i]) + { + ClearPixelsTile(pImgBuff, Width, Height, i); + } + } + + DoClearTransparentPixels = true; + DilateAs2DArray = true; + DoDilate = true; + } + else if(aImageFlags[ImageIndex] == 0) + { + mem_zero(pImgBuff, Width * Height * 4); + } + else + { + DoClearTransparentPixels = true; + DoDilate = true; + } + + if(DoClearTransparentPixels) + { + // clear unused pixels and make a clean dilate for the compressor + ClearTransparentPixels(pImgBuff, Width, Height); + } + + if(DoDilate) + { + if(DilateAs2DArray) + { + for(int i = 0; i < 256; ++i) + { + int ImgTileW = Width / 16; + int ImgTileH = Height / 16; + int x = (i % 16) * ImgTileW; + int y = (i / 16) * ImgTileH; + DilateImageSub(pImgBuff, Width, Height, 4, x, y, ImgTileW, ImgTileH); + } + } + else + { + DilateImage(pImgBuff, Width, Height, 4); + } + } + } + else if(it->m_Text == Index) + { + char *pImgName = (char *)pPtr; + uint8_t *pImgBuff = (uint8_t *)DataFile.GetData(it->m_Data); + int ImgSize = DataFile.GetDataSize(it->m_Data); + + char aSHA256Str[SHA256_MAXSTRSIZE]; + // This is the important function, that calculates the SHA256 in a special way + // Please read the comments inside the functions to understand it + GetImageSHA256(pImgBuff, ImgSize, Width, Height, aSHA256Str); + + char aNewName[MAX_PATH_LENGTH]; + int StrLen = str_format(aNewName, sizeof(aNewName) / sizeof(aNewName[0]), "%s_cut_%s", pImgName, aSHA256Str); + + DeletePtr = true; + // make the new name ready + char *pNewPtr = (char *)malloc(StrLen + 1); + str_copy(pNewPtr, aNewName, StrLen + 1); + pPtr = pNewPtr; + Size = StrLen + 1; + } + } + + df.AddData(Size, pPtr, Z_BEST_COMPRESSION); + + if(DeletePtr) + free(pPtr); + } + + DataFile.Close(); + df.Finish(); + + return 0; +}