diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d1ebde9c39..899ad43c633 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2033,6 +2033,7 @@ set_src(ENGINE_INTERFACE GLOB src/engine ghost.h graphics.h http.h + image.h input.h kernel.h keys.h @@ -2139,6 +2140,7 @@ set_src(ENGINE_SHARED GLOB_RECURSE src/engine/shared websockets.h ) set_src(ENGINE_GFX GLOB src/engine/gfx + image.cpp image_loader.cpp image_loader.h image_manipulation.cpp diff --git a/src/engine/client/graphics_threaded.cpp b/src/engine/client/graphics_threaded.cpp index 21e5687febb..8dc82c34ae2 100644 --- a/src/engine/client/graphics_threaded.cpp +++ b/src/engine/client/graphics_threaded.cpp @@ -300,54 +300,6 @@ void CGraphics_Threaded::UnloadTexture(CTextureHandle *pIndex) FreeTextureIndex(pIndex); } -bool ConvertToRGBA(uint8_t *pDest, const CImageInfo &SrcImage) -{ - if(SrcImage.m_Format == CImageInfo::FORMAT_RGBA) - { - mem_copy(pDest, SrcImage.m_pData, SrcImage.DataSize()); - return true; - } - else - { - const size_t SrcChannelCount = CImageInfo::PixelSize(SrcImage.m_Format); - const size_t DstChannelCount = CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA); - for(size_t Y = 0; Y < SrcImage.m_Height; ++Y) - { - for(size_t X = 0; X < SrcImage.m_Width; ++X) - { - size_t ImgOffsetSrc = (Y * SrcImage.m_Width * SrcChannelCount) + (X * SrcChannelCount); - size_t ImgOffsetDest = (Y * SrcImage.m_Width * DstChannelCount) + (X * DstChannelCount); - size_t CopySize = SrcChannelCount; - if(SrcImage.m_Format == CImageInfo::FORMAT_RGB) - { - mem_copy(&pDest[ImgOffsetDest], &SrcImage.m_pData[ImgOffsetSrc], CopySize); - pDest[ImgOffsetDest + 3] = 255; - } - else if(SrcImage.m_Format == CImageInfo::FORMAT_SINGLE_COMPONENT) - { - pDest[ImgOffsetDest + 0] = 255; - pDest[ImgOffsetDest + 1] = 255; - pDest[ImgOffsetDest + 2] = 255; - pDest[ImgOffsetDest + 3] = SrcImage.m_pData[ImgOffsetSrc]; - } - } - } - return false; - } -} - -IGraphics::CTextureHandle CGraphics_Threaded::LoadSpriteTextureImpl(const CImageInfo &FromImageInfo, int x, int y, size_t w, size_t h, const char *pName) -{ - m_vSpriteHelper.resize(w * h * FromImageInfo.PixelSize()); - CopyTextureFromTextureBufferSub(m_vSpriteHelper.data(), w, h, FromImageInfo, x, y, w, h); - CImageInfo SpriteInfo; - SpriteInfo.m_Width = w; - SpriteInfo.m_Height = h; - SpriteInfo.m_Format = FromImageInfo.m_Format; - SpriteInfo.m_pData = m_vSpriteHelper.data(); - return LoadTextureRaw(SpriteInfo, 0, pName); -} - IGraphics::CTextureHandle CGraphics_Threaded::LoadSpriteTexture(const CImageInfo &FromImageInfo, const CDataSprite *pSprite) { int ImageGridX = FromImageInfo.m_Width / pSprite->m_pSet->m_Gridx; @@ -356,12 +308,19 @@ IGraphics::CTextureHandle CGraphics_Threaded::LoadSpriteTexture(const CImageInfo int y = pSprite->m_Y * ImageGridY; int w = pSprite->m_W * ImageGridX; int h = pSprite->m_H * ImageGridY; - return LoadSpriteTextureImpl(FromImageInfo, x, y, w, h, pSprite->m_pName); + + CImageInfo SpriteInfo; + SpriteInfo.m_Width = w; + SpriteInfo.m_Height = h; + SpriteInfo.m_Format = FromImageInfo.m_Format; + SpriteInfo.m_pData = static_cast(malloc(SpriteInfo.DataSize())); + SpriteInfo.CopyRectFrom(FromImageInfo, x, y, w, h, 0, 0); + return LoadTextureRawMove(SpriteInfo, 0, pSprite->m_pName); } bool CGraphics_Threaded::IsImageSubFullyTransparent(const CImageInfo &FromImageInfo, int x, int y, int w, int h) { - if(FromImageInfo.m_Format == CImageInfo::FORMAT_SINGLE_COMPONENT || FromImageInfo.m_Format == CImageInfo::FORMAT_RGBA) + if(FromImageInfo.m_Format == CImageInfo::FORMAT_R || FromImageInfo.m_Format == CImageInfo::FORMAT_RA || FromImageInfo.m_Format == CImageInfo::FORMAT_RGBA) { const uint8_t *pImgData = FromImageInfo.m_pData; const size_t PixelSize = FromImageInfo.PixelSize(); @@ -435,8 +394,8 @@ IGraphics::CTextureHandle CGraphics_Threaded::LoadTextureRaw(const CImageInfo &I CCommandBuffer::SCommand_Texture_Create Cmd = LoadTextureCreateCommand(TextureHandle.Id(), Image.m_Width, Image.m_Height, Flags); // Copy texture data and convert if necessary - uint8_t *pTmpData = static_cast(malloc(Image.m_Width * Image.m_Height * CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA))); - if(!ConvertToRGBA(pTmpData, Image)) + uint8_t *pTmpData; + if(!ConvertToRgbaAlloc(pTmpData, Image)) { dbg_msg("graphics", "converted image '%s' to RGBA, consider making its file format RGBA", pTexName ? pTexName : "(no name)"); } @@ -472,7 +431,6 @@ IGraphics::CTextureHandle CGraphics_Threaded::LoadTextureRawMove(CImageInfo &Ima return TextureHandle; } -// simple uncompressed RGBA loaders IGraphics::CTextureHandle CGraphics_Threaded::LoadTexture(const char *pFilename, int StorageType, int Flags) { dbg_assert(pFilename[0] != '\0', "Cannot load texture from file with empty filename"); // would cause Valgrind to crash otherwise @@ -544,81 +502,56 @@ bool CGraphics_Threaded::UpdateTextTexture(CTextureHandle TextureId, int x, int return true; } -bool CGraphics_Threaded::LoadPng(CImageInfo &Image, const char *pFilename, int StorageType) +static SWarning FormatPngliteIncompatibilityWarning(int PngliteIncompatible, const char *pContextName) { - char aCompleteFilename[IO_MAX_PATH_LENGTH]; - IOHANDLE File = m_pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType, aCompleteFilename, sizeof(aCompleteFilename)); - if(File) + SWarning Warning; + str_format(Warning.m_aWarningMsg, sizeof(Warning.m_aWarningMsg), Localize("\"%s\" is not compatible with pnglite and cannot be loaded by old DDNet versions: "), pContextName); + static const int FLAGS[] = {CImageLoader::PNGLITE_COLOR_TYPE, CImageLoader::PNGLITE_BIT_DEPTH, CImageLoader::PNGLITE_INTERLACE_TYPE, CImageLoader::PNGLITE_COMPRESSION_TYPE, CImageLoader::PNGLITE_FILTER_TYPE}; + static const char *const EXPLANATION[] = {"color type", "bit depth", "interlace type", "compression type", "filter type"}; + + bool First = true; + for(size_t i = 0; i < std::size(FLAGS); ++i) { - io_seek(File, 0, IOSEEK_END); - long int FileSize = io_tell(File); - if(FileSize <= 0) + if((PngliteIncompatible & FLAGS[i]) != 0) { - io_close(File); - log_error("game/png", "failed to get file size (%ld). filename='%s'", FileSize, pFilename); - return false; + if(!First) + { + str_append(Warning.m_aWarningMsg, ", "); + } + str_append(Warning.m_aWarningMsg, EXPLANATION[i]); + First = false; } - io_seek(File, 0, IOSEEK_START); + } + str_append(Warning.m_aWarningMsg, " unsupported"); + return Warning; +} - TImageByteBuffer ByteBuffer; - SImageByteBuffer ImageByteBuffer(&ByteBuffer); +bool CGraphics_Threaded::LoadPng(CImageInfo &Image, const char *pFilename, int StorageType) +{ + IOHANDLE File = m_pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType); - ByteBuffer.resize(FileSize); - io_read(File, &ByteBuffer.front(), FileSize); + int PngliteIncompatible; + if(!CImageLoader::LoadPng(File, pFilename, Image, PngliteIncompatible)) + return false; - io_close(File); + if(m_WarnPngliteIncompatibleImages && PngliteIncompatible != 0) + { + m_vWarnings.emplace_back(FormatPngliteIncompatibilityWarning(PngliteIncompatible, pFilename)); + } - uint8_t *pImgBuffer = NULL; - EImageFormat ImageFormat; - int PngliteIncompatible; - if(::LoadPng(ImageByteBuffer, pFilename, PngliteIncompatible, Image.m_Width, Image.m_Height, pImgBuffer, ImageFormat)) - { - if(ImageFormat == IMAGE_FORMAT_RGB) - Image.m_Format = CImageInfo::FORMAT_RGB; - else if(ImageFormat == IMAGE_FORMAT_RGBA) - Image.m_Format = CImageInfo::FORMAT_RGBA; - else - { - free(pImgBuffer); - log_error("game/png", "image had unsupported image format. filename='%s' format='%d'", pFilename, (int)ImageFormat); - return false; - } - Image.m_pData = pImgBuffer; + return true; +} - if(m_WarnPngliteIncompatibleImages && PngliteIncompatible != 0) - { - SWarning Warning; - str_format(Warning.m_aWarningMsg, sizeof(Warning.m_aWarningMsg), Localize("\"%s\" is not compatible with pnglite and cannot be loaded by old DDNet versions: "), pFilename); - static const int FLAGS[] = {PNGLITE_COLOR_TYPE, PNGLITE_BIT_DEPTH, PNGLITE_INTERLACE_TYPE, PNGLITE_COMPRESSION_TYPE, PNGLITE_FILTER_TYPE}; - static const char *const EXPLANATION[] = {"color type", "bit depth", "interlace type", "compression type", "filter type"}; +bool CGraphics_Threaded::LoadPng(CImageInfo &Image, const uint8_t *pData, size_t DataSize, const char *pContextName) +{ + CByteBufferReader Reader(pData, DataSize); + int PngliteIncompatible; + if(!CImageLoader::LoadPng(Reader, pContextName, Image, PngliteIncompatible)) + return false; - bool First = true; - for(size_t i = 0; i < std::size(FLAGS); ++i) - { - if((PngliteIncompatible & FLAGS[i]) != 0) - { - if(!First) - { - str_append(Warning.m_aWarningMsg, ", "); - } - str_append(Warning.m_aWarningMsg, EXPLANATION[i]); - First = false; - } - } - str_append(Warning.m_aWarningMsg, " unsupported"); - m_vWarnings.emplace_back(Warning); - } - } - else - { - log_error("game/png", "failed to load file. filename='%s'", pFilename); - return false; - } - } - else + if(m_WarnPngliteIncompatibleImages && PngliteIncompatible != 0) { - log_error("game/png", "failed to open file. filename='%s'", pFilename); - return false; + m_vWarnings.emplace_back(FormatPngliteIncompatibilityWarning(PngliteIncompatible, pContextName)); } return true; @@ -655,12 +588,7 @@ bool CGraphics_Threaded::CheckImageDivisibility(const char *pContextName, CImage NewHeight = maximum(HighestBit(Image.m_Height), DivY); NewWidth = (NewHeight / DivY) * DivX; } - - uint8_t *pNewImage = ResizeImage(Image.m_pData, Image.m_Width, Image.m_Height, NewWidth, NewHeight, Image.PixelSize()); - free(Image.m_pData); - Image.m_pData = pNewImage; - Image.m_Width = NewWidth; - Image.m_Height = NewHeight; + ResizeImage(Image, NewWidth, NewHeight); ImageIsValid = true; } @@ -682,29 +610,6 @@ bool CGraphics_Threaded::IsImageFormatRgba(const char *pContextName, const CImag return true; } -void CGraphics_Threaded::CopyTextureBufferSub(uint8_t *pDestBuffer, const CImageInfo &SourceImage, size_t SubOffsetX, size_t SubOffsetY, size_t SubCopyWidth, size_t SubCopyHeight) -{ - const size_t PixelSize = SourceImage.PixelSize(); - for(size_t Y = 0; Y < SubCopyHeight; ++Y) - { - const size_t ImgOffset = ((SubOffsetY + Y) * SourceImage.m_Width * PixelSize) + (SubOffsetX * PixelSize); - const size_t CopySize = SubCopyWidth * PixelSize; - mem_copy(&pDestBuffer[ImgOffset], &SourceImage.m_pData[ImgOffset], CopySize); - } -} - -void CGraphics_Threaded::CopyTextureFromTextureBufferSub(uint8_t *pDestBuffer, size_t DestWidth, size_t DestHeight, const CImageInfo &SourceImage, size_t SrcSubOffsetX, size_t SrcSubOffsetY, size_t SrcSubCopyWidth, size_t SrcSubCopyHeight) -{ - const size_t PixelSize = SourceImage.PixelSize(); - for(size_t Y = 0; Y < SrcSubCopyHeight; ++Y) - { - const size_t SrcImgOffset = ((SrcSubOffsetY + Y) * SourceImage.m_Width * PixelSize) + (SrcSubOffsetX * PixelSize); - const size_t DstImgOffset = (Y * DestWidth * PixelSize); - const size_t CopySize = SrcSubCopyWidth * PixelSize; - mem_copy(&pDestBuffer[DstImgOffset], &SourceImage.m_pData[SrcImgOffset], CopySize); - } -} - void CGraphics_Threaded::KickCommandBuffer() { m_pBackend->RunBuffer(m_pCommandBuffer); @@ -731,24 +636,14 @@ class CScreenshotSaveJob : public IJob IStorage *m_pStorage; IConsole *m_pConsole; char m_aName[IO_MAX_PATH_LENGTH]; - int m_Width; - int m_Height; - uint8_t *m_pData; + CImageInfo m_Image; void Run() override { char aWholePath[IO_MAX_PATH_LENGTH]; char aBuf[64 + IO_MAX_PATH_LENGTH]; - IOHANDLE File = m_pStorage->OpenFile(m_aName, IOFLAG_WRITE, IStorage::TYPE_SAVE, aWholePath, sizeof(aWholePath)); - if(File) + if(CImageLoader::SavePng(m_pStorage->OpenFile(m_aName, IOFLAG_WRITE, IStorage::TYPE_SAVE, aWholePath, sizeof(aWholePath)), m_aName, m_Image)) { - TImageByteBuffer ByteBuffer; - SImageByteBuffer ImageByteBuffer(&ByteBuffer); - - if(SavePng(IMAGE_FORMAT_RGBA, m_pData, ImageByteBuffer, m_Width, m_Height)) - io_write(File, &ByteBuffer.front(), ByteBuffer.size()); - io_close(File); - str_format(aBuf, sizeof(aBuf), "saved screenshot to '%s'", aWholePath); } else @@ -759,19 +654,17 @@ class CScreenshotSaveJob : public IJob } public: - CScreenshotSaveJob(IStorage *pStorage, IConsole *pConsole, const char *pName, int Width, int Height, uint8_t *pData) : + CScreenshotSaveJob(IStorage *pStorage, IConsole *pConsole, const char *pName, CImageInfo Image) : m_pStorage(pStorage), m_pConsole(pConsole), - m_Width(Width), - m_Height(Height), - m_pData(pData) + m_Image(Image) { str_copy(m_aName, pName); } ~CScreenshotSaveJob() override { - free(m_pData); + m_Image.Free(); } }; @@ -795,7 +688,7 @@ void CGraphics_Threaded::ScreenshotDirect(bool *pSwapped) if(Image.m_pData) { - m_pEngine->AddJob(std::make_shared(m_pStorage, m_pConsole, m_aScreenshotName, Image.m_Width, Image.m_Height, Image.m_pData)); + m_pEngine->AddJob(std::make_shared(m_pStorage, m_pConsole, m_aScreenshotName, Image)); } } diff --git a/src/engine/client/graphics_threaded.h b/src/engine/client/graphics_threaded.h index dd1d4ce65ef..01774d58fc2 100644 --- a/src/engine/client/graphics_threaded.h +++ b/src/engine/client/graphics_threaded.h @@ -798,8 +798,6 @@ class CGraphics_Threaded : public IEngineGraphics size_t m_FirstFreeTexture; int m_TextureMemoryUsage; - std::vector m_vSpriteHelper; - bool m_WarnPngliteIncompatibleImages = false; std::vector m_vWarnings; @@ -954,7 +952,6 @@ class CGraphics_Threaded : public IEngineGraphics bool UnloadTextTextures(CTextureHandle &TextTexture, CTextureHandle &TextOutlineTexture) override; bool UpdateTextTexture(CTextureHandle TextureId, int x, int y, size_t Width, size_t Height, const uint8_t *pData) override; - CTextureHandle LoadSpriteTextureImpl(const CImageInfo &FromImageInfo, int x, int y, size_t w, size_t h, const char *pName); CTextureHandle LoadSpriteTexture(const CImageInfo &FromImageInfo, const struct CDataSprite *pSprite) override; bool IsImageSubFullyTransparent(const CImageInfo &FromImageInfo, int x, int y, int w, int h) override; @@ -963,13 +960,11 @@ class CGraphics_Threaded : public IEngineGraphics // simple uncompressed RGBA loaders IGraphics::CTextureHandle LoadTexture(const char *pFilename, int StorageType, int Flags = 0) override; bool LoadPng(CImageInfo &Image, const char *pFilename, int StorageType) override; + bool LoadPng(CImageInfo &Image, const uint8_t *pData, size_t DataSize, const char *pContextName) override; bool CheckImageDivisibility(const char *pContextName, CImageInfo &Image, int DivX, int DivY, bool AllowResize) override; bool IsImageFormatRgba(const char *pContextName, const CImageInfo &Image) override; - void CopyTextureBufferSub(uint8_t *pDestBuffer, const CImageInfo &SourceImage, size_t SubOffsetX, size_t SubOffsetY, size_t SubCopyWidth, size_t SubCopyHeight) override; - void CopyTextureFromTextureBufferSub(uint8_t *pDestBuffer, size_t DestWidth, size_t DestHeight, const CImageInfo &SourceImage, size_t SrcSubOffsetX, size_t SrcSubOffsetY, size_t SrcSubCopyWidth, size_t SrcSubCopyHeight) override; - void TextureSet(CTextureHandle TextureId) override; void Clear(float r, float g, float b, bool ForceClearNow = false) override; diff --git a/src/engine/gfx/image.cpp b/src/engine/gfx/image.cpp new file mode 100644 index 00000000000..78475aae065 --- /dev/null +++ b/src/engine/gfx/image.cpp @@ -0,0 +1,120 @@ +#include + +#include + +void CImageInfo::Free() +{ + m_Width = 0; + m_Height = 0; + m_Format = FORMAT_UNDEFINED; + free(m_pData); + m_pData = nullptr; +} + +size_t CImageInfo::PixelSize(EImageFormat Format) +{ + dbg_assert(Format != FORMAT_UNDEFINED, "Format undefined"); + static const size_t s_aSizes[] = {3, 4, 1, 2}; + return s_aSizes[(int)Format]; +} + +const char *CImageInfo::FormatName(EImageFormat Format) +{ + static const char *s_apNames[] = {"UNDEFINED", "RGBA", "RGB", "R", "RA"}; + return s_apNames[(int)Format + 1]; +} + +size_t CImageInfo::PixelSize() const +{ + return PixelSize(m_Format); +} + +const char *CImageInfo::FormatName() const +{ + return FormatName(m_Format); +} + +size_t CImageInfo::DataSize() const +{ + return m_Width * m_Height * PixelSize(m_Format); +} + +bool CImageInfo::DataEquals(const CImageInfo &Other) const +{ + if(m_Width != Other.m_Width || m_Height != Other.m_Height || m_Format != Other.m_Format) + return false; + if(m_pData == nullptr && Other.m_pData == nullptr) + return true; + if(m_pData == nullptr || Other.m_pData == nullptr) + return false; + return mem_comp(m_pData, Other.m_pData, DataSize()) == 0; +} + +ColorRGBA CImageInfo::PixelColor(size_t x, size_t y) const +{ + const size_t PixelStartIndex = (x + m_Width * y) * PixelSize(); + + ColorRGBA Color; + if(m_Format == FORMAT_R) + { + Color.r = Color.g = Color.b = 1.0f; + Color.a = m_pData[PixelStartIndex] / 255.0f; + } + else if(m_Format == FORMAT_RA) + { + Color.r = Color.g = Color.b = m_pData[PixelStartIndex] / 255.0f; + Color.a = m_pData[PixelStartIndex + 1] / 255.0f; + } + else + { + Color.r = m_pData[PixelStartIndex + 0] / 255.0f; + Color.g = m_pData[PixelStartIndex + 1] / 255.0f; + Color.b = m_pData[PixelStartIndex + 2] / 255.0f; + if(m_Format == FORMAT_RGBA) + { + Color.a = m_pData[PixelStartIndex + 3] / 255.0f; + } + else + { + Color.a = 1.0f; + } + } + return Color; +} + +void CImageInfo::SetPixelColor(size_t x, size_t y, ColorRGBA Color) const +{ + const size_t PixelStartIndex = (x + m_Width * y) * PixelSize(); + + if(m_Format == FORMAT_R) + { + m_pData[PixelStartIndex] = round_to_int(Color.a * 255.0f); + } + else if(m_Format == FORMAT_RA) + { + m_pData[PixelStartIndex] = round_to_int((Color.r + Color.g + Color.b) / 3.0f * 255.0f); + m_pData[PixelStartIndex + 1] = round_to_int(Color.a * 255.0f); + } + else + { + m_pData[PixelStartIndex + 0] = round_to_int(Color.r * 255.0f); + m_pData[PixelStartIndex + 1] = round_to_int(Color.g * 255.0f); + m_pData[PixelStartIndex + 2] = round_to_int(Color.b * 255.0f); + if(m_Format == FORMAT_RGBA) + { + m_pData[PixelStartIndex + 3] = round_to_int(Color.a * 255.0f); + } + } +} + +void CImageInfo::CopyRectFrom(const CImageInfo &SrcImage, size_t SrcX, size_t SrcY, size_t Width, size_t Height, size_t DestX, size_t DestY) const +{ + const size_t PixelSize = SrcImage.PixelSize(); + const size_t CopySize = Width * PixelSize; + for(size_t Y = 0; Y < Height; ++Y) + { + const size_t SrcOffset = ((SrcY + Y) * SrcImage.m_Width + SrcX) * PixelSize; + const size_t DestOffset = ((DestY + Y) * m_Width + DestX) * PixelSize; + mem_copy(&m_pData[DestOffset], &SrcImage.m_pData[SrcOffset], CopySize); + } +} diff --git a/src/engine/gfx/image_loader.cpp b/src/engine/gfx/image_loader.cpp index e65b6a3ebf3..be80f82df87 100644 --- a/src/engine/gfx/image_loader.cpp +++ b/src/engine/gfx/image_loader.cpp @@ -1,89 +1,94 @@ #include "image_loader.h" + #include #include + #include #include #include -struct SLibPngWarningItem +bool CByteBufferReader::Read(void *pData, size_t Size) { - SImageByteBuffer *m_pByteLoader; - const char *m_pFileName; - std::jmp_buf m_Buf; -}; + if(m_Error) + return false; -[[noreturn]] static void LibPngError(png_structp png_ptr, png_const_charp error_msg) -{ - SLibPngWarningItem *pUserStruct = (SLibPngWarningItem *)png_get_error_ptr(png_ptr); - pUserStruct->m_pByteLoader->m_Err = -1; - dbg_msg("png", "error for file \"%s\": %s", pUserStruct->m_pFileName, error_msg); - std::longjmp(pUserStruct->m_Buf, 1); + if(m_ReadOffset + Size <= m_Size) + { + mem_copy(pData, &m_pData[m_ReadOffset], Size); + m_ReadOffset += Size; + return true; + } + else + { + m_Error = true; + return false; + } } -static void LibPngWarning(png_structp png_ptr, png_const_charp warning_msg) +void CByteBufferWriter::Write(const void *pData, size_t Size) { - SLibPngWarningItem *pUserStruct = (SLibPngWarningItem *)png_get_error_ptr(png_ptr); - dbg_msg("png", "warning for file \"%s\": %s", pUserStruct->m_pFileName, warning_msg); -} + if(!Size) + return; -static bool FileMatchesImageType(SImageByteBuffer &ByteLoader) -{ - if(ByteLoader.m_pvLoadedImageBytes->size() >= 8) - return png_sig_cmp((png_bytep)ByteLoader.m_pvLoadedImageBytes->data(), 0, 8) == 0; - return false; + const size_t WriteOffset = m_vBuffer.size(); + m_vBuffer.resize(WriteOffset + Size); + mem_copy(&m_vBuffer[WriteOffset], pData, Size); } -static void ReadDataFromLoadedBytes(png_structp pPngStruct, png_bytep pOutBytes, png_size_t ByteCountToRead) +class CUserErrorStruct { - png_voidp pIO_Ptr = png_get_io_ptr(pPngStruct); +public: + CByteBufferReader *m_pReader; + const char *m_pContextName; + std::jmp_buf m_JmpBuf; +}; - SImageByteBuffer *pByteLoader = (SImageByteBuffer *)pIO_Ptr; +[[noreturn]] static void PngErrorCallback(png_structp png_ptr, png_const_charp error_msg) +{ + CUserErrorStruct *pUserStruct = static_cast(png_get_error_ptr(png_ptr)); + log_error("png", "error for file \"%s\": %s", pUserStruct->m_pContextName, error_msg); + std::longjmp(pUserStruct->m_JmpBuf, 1); +} - if(pByteLoader->m_pvLoadedImageBytes->size() >= pByteLoader->m_LoadOffset + (size_t)ByteCountToRead) - { - mem_copy(pOutBytes, &(*pByteLoader->m_pvLoadedImageBytes)[pByteLoader->m_LoadOffset], (size_t)ByteCountToRead); +static void PngWarningCallback(png_structp png_ptr, png_const_charp warning_msg) +{ + CUserErrorStruct *pUserStruct = static_cast(png_get_error_ptr(png_ptr)); + log_warn("png", "warning for file \"%s\": %s", pUserStruct->m_pContextName, warning_msg); +} - pByteLoader->m_LoadOffset += (size_t)ByteCountToRead; - } - else +static void PngReadDataCallback(png_structp pPngStruct, png_bytep pOutBytes, png_size_t ByteCountToRead) +{ + CByteBufferReader *pReader = static_cast(png_get_io_ptr(pPngStruct)); + if(!pReader->Read(pOutBytes, ByteCountToRead)) { - pByteLoader->m_Err = -1; - dbg_msg("png", "could not read bytes, file was too small."); + png_error(pPngStruct, "Could not read all bytes, file was too small"); } } -static EImageFormat LibPngGetImageFormat(int ColorChannelCount) +static CImageInfo::EImageFormat ImageFormatFromChannelCount(int ColorChannelCount) { switch(ColorChannelCount) { case 1: - return IMAGE_FORMAT_R; + return CImageInfo::FORMAT_R; case 2: - return IMAGE_FORMAT_RA; + return CImageInfo::FORMAT_RA; case 3: - return IMAGE_FORMAT_RGB; + return CImageInfo::FORMAT_RGB; case 4: - return IMAGE_FORMAT_RGBA; + return CImageInfo::FORMAT_RGBA; default: dbg_assert(false, "ColorChannelCount invalid"); dbg_break(); } } -static void LibPngDeleteReadStruct(png_structp pPngStruct, png_infop pPngInfo) -{ - if(pPngInfo != nullptr) - png_destroy_info_struct(pPngStruct, &pPngInfo); - png_destroy_read_struct(&pPngStruct, nullptr, nullptr); -} - static int PngliteIncompatibility(png_structp pPngStruct, png_infop pPngInfo) { - int ColorType = png_get_color_type(pPngStruct, pPngInfo); - int BitDepth = png_get_bit_depth(pPngStruct, pPngInfo); - int InterlaceType = png_get_interlace_type(pPngStruct, pPngInfo); int Result = 0; + + const int ColorType = png_get_color_type(pPngStruct, pPngInfo); switch(ColorType) { case PNG_COLOR_TYPE_GRAY: @@ -93,9 +98,10 @@ static int PngliteIncompatibility(png_structp pPngStruct, png_infop pPngInfo) break; default: log_debug("png", "color type %d unsupported by pnglite", ColorType); - Result |= PNGLITE_COLOR_TYPE; + Result |= CImageLoader::PNGLITE_COLOR_TYPE; } + const int BitDepth = png_get_bit_depth(pPngStruct, pPngInfo); switch(BitDepth) { case 8: @@ -103,258 +109,294 @@ static int PngliteIncompatibility(png_structp pPngStruct, png_infop pPngInfo) break; default: log_debug("png", "bit depth %d unsupported by pnglite", BitDepth); - Result |= PNGLITE_BIT_DEPTH; + Result |= CImageLoader::PNGLITE_BIT_DEPTH; } + const int InterlaceType = png_get_interlace_type(pPngStruct, pPngInfo); if(InterlaceType != PNG_INTERLACE_NONE) { log_debug("png", "interlace type %d unsupported by pnglite", InterlaceType); - Result |= PNGLITE_INTERLACE_TYPE; + Result |= CImageLoader::PNGLITE_INTERLACE_TYPE; } + if(png_get_compression_type(pPngStruct, pPngInfo) != PNG_COMPRESSION_TYPE_BASE) { log_debug("png", "non-default compression type unsupported by pnglite"); - Result |= PNGLITE_COMPRESSION_TYPE; + Result |= CImageLoader::PNGLITE_COMPRESSION_TYPE; } + if(png_get_filter_type(pPngStruct, pPngInfo) != PNG_FILTER_TYPE_BASE) { log_debug("png", "non-default filter type unsupported by pnglite"); - Result |= PNGLITE_FILTER_TYPE; + Result |= CImageLoader::PNGLITE_FILTER_TYPE; } + return Result; } -bool LoadPng(SImageByteBuffer &ByteLoader, const char *pFileName, int &PngliteIncompatible, size_t &Width, size_t &Height, uint8_t *&pImageBuff, EImageFormat &ImageFormat) +bool CImageLoader::LoadPng(CByteBufferReader &Reader, const char *pContextName, CImageInfo &Image, int &PngliteIncompatible) { - SLibPngWarningItem UserErrorStruct = {&ByteLoader, pFileName, {}}; + CUserErrorStruct UserErrorStruct = {&Reader, pContextName, {}}; png_structp pPngStruct = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); - if(pPngStruct == nullptr) { - dbg_msg("png", "libpng internal failure: png_create_read_struct failed."); + log_error("png", "libpng internal failure: png_create_read_struct failed."); return false; } png_infop pPngInfo = nullptr; png_bytepp pRowPointers = nullptr; - Height = 0; // ensure this is not undefined for the error handler - if(setjmp(UserErrorStruct.m_Buf)) - { + int Height = 0; // ensure this is not undefined for the Cleanup function + const auto &&Cleanup = [&]() { if(pRowPointers != nullptr) { - for(size_t i = 0; i < Height; ++i) + for(int y = 0; y < Height; ++y) { - delete[] pRowPointers[i]; + delete[] pRowPointers[y]; } } delete[] pRowPointers; - LibPngDeleteReadStruct(pPngStruct, pPngInfo); + if(pPngInfo != nullptr) + { + png_destroy_info_struct(pPngStruct, &pPngInfo); + } + png_destroy_read_struct(&pPngStruct, nullptr, nullptr); + }; + if(setjmp(UserErrorStruct.m_JmpBuf)) + { + Cleanup(); return false; } - png_set_error_fn(pPngStruct, &UserErrorStruct, LibPngError, LibPngWarning); + png_set_error_fn(pPngStruct, &UserErrorStruct, PngErrorCallback, PngWarningCallback); pPngInfo = png_create_info_struct(pPngStruct); - if(pPngInfo == nullptr) { - png_destroy_read_struct(&pPngStruct, nullptr, nullptr); - dbg_msg("png", "libpng internal failure: png_create_info_struct failed."); + Cleanup(); + log_error("png", "libpng internal failure: png_create_info_struct failed."); return false; } - if(!FileMatchesImageType(ByteLoader)) + png_byte aSignature[8]; + if(!Reader.Read(aSignature, sizeof(aSignature)) || png_sig_cmp(aSignature, 0, sizeof(aSignature)) != 0) { - LibPngDeleteReadStruct(pPngStruct, pPngInfo); - dbg_msg("png", "file does not match image type."); + Cleanup(); + log_error("png", "file is not a valid PNG file (signature mismatch)."); return false; } - ByteLoader.m_LoadOffset = 8; - - png_set_read_fn(pPngStruct, (png_bytep)&ByteLoader, ReadDataFromLoadedBytes); - - png_set_sig_bytes(pPngStruct, 8); + png_set_read_fn(pPngStruct, (png_bytep)&Reader, PngReadDataCallback); + png_set_sig_bytes(pPngStruct, sizeof(aSignature)); png_read_info(pPngStruct, pPngInfo); - if(ByteLoader.m_Err != 0) + if(Reader.Error()) { - LibPngDeleteReadStruct(pPngStruct, pPngInfo); - dbg_msg("png", "byte loader error."); + // error already logged + Cleanup(); return false; } - Width = png_get_image_width(pPngStruct, pPngInfo); + const int Width = png_get_image_width(pPngStruct, pPngInfo); Height = png_get_image_height(pPngStruct, pPngInfo); - const int ColorType = png_get_color_type(pPngStruct, pPngInfo); const png_byte BitDepth = png_get_bit_depth(pPngStruct, pPngInfo); - PngliteIncompatible = PngliteIncompatibility(pPngStruct, pPngInfo); + const int ColorType = png_get_color_type(pPngStruct, pPngInfo); - if(BitDepth == 16) - { - png_set_strip_16(pPngStruct); - } - else if(BitDepth > 8) + if(Width == 0 || Height == 0) { - dbg_msg("png", "non supported bit depth."); - LibPngDeleteReadStruct(pPngStruct, pPngInfo); + log_error("png", "image has width (%d) or height (%d) of 0.", Width, Height); + Cleanup(); return false; } - if(Width == 0 || Height == 0 || BitDepth == 0) + if(BitDepth == 16) + { + png_set_strip_16(pPngStruct); + } + else if(BitDepth > 8 || BitDepth == 0) { - dbg_msg("png", "image had width, height or bit depth of 0."); - LibPngDeleteReadStruct(pPngStruct, pPngInfo); + log_error("png", "bit depth %d not supported.", BitDepth); + Cleanup(); return false; } if(ColorType == PNG_COLOR_TYPE_PALETTE) + { png_set_palette_to_rgb(pPngStruct); + } if(ColorType == PNG_COLOR_TYPE_GRAY && BitDepth < 8) + { png_set_expand_gray_1_2_4_to_8(pPngStruct); + } if(png_get_valid(pPngStruct, pPngInfo, PNG_INFO_tRNS)) + { png_set_tRNS_to_alpha(pPngStruct); + } png_read_update_info(pPngStruct, pPngInfo); - const size_t ColorChannelCount = png_get_channels(pPngStruct, pPngInfo); - const size_t BytesInRow = png_get_rowbytes(pPngStruct, pPngInfo); + const int ColorChannelCount = png_get_channels(pPngStruct, pPngInfo); + const int BytesInRow = png_get_rowbytes(pPngStruct, pPngInfo); dbg_assert(BytesInRow == Width * ColorChannelCount, "bytes in row incorrect."); pRowPointers = new png_bytep[Height]; - for(size_t y = 0; y < Height; ++y) + for(int y = 0; y < Height; ++y) { pRowPointers[y] = new png_byte[BytesInRow]; } png_read_image(pPngStruct, pRowPointers); - if(ByteLoader.m_Err == 0) - pImageBuff = (uint8_t *)malloc(Height * Width * ColorChannelCount * sizeof(uint8_t)); - - for(size_t i = 0; i < Height; ++i) + if(!Reader.Error()) { - if(ByteLoader.m_Err == 0) - mem_copy(&pImageBuff[i * BytesInRow], pRowPointers[i], BytesInRow); - delete[] pRowPointers[i]; + Image.m_Width = Width; + Image.m_Height = Height; + Image.m_Format = ImageFormatFromChannelCount(ColorChannelCount); + Image.m_pData = static_cast(malloc(Image.DataSize())); + for(int y = 0; y < Height; ++y) + { + mem_copy(&Image.m_pData[y * BytesInRow], pRowPointers[y], BytesInRow); + } + PngliteIncompatible = PngliteIncompatibility(pPngStruct, pPngInfo); } - delete[] pRowPointers; - pRowPointers = nullptr; - if(ByteLoader.m_Err != 0) + Cleanup(); + + return !Reader.Error(); +} + +bool CImageLoader::LoadPng(IOHANDLE File, const char *pFilename, CImageInfo &Image, int &PngliteIncompatible) +{ + if(!File) { - LibPngDeleteReadStruct(pPngStruct, pPngInfo); - dbg_msg("png", "byte loader error."); + log_error("png", "failed to open file for reading. filename='%s'", pFilename); return false; } - ImageFormat = LibPngGetImageFormat(ColorChannelCount); - - png_destroy_info_struct(pPngStruct, &pPngInfo); - png_destroy_read_struct(&pPngStruct, nullptr, nullptr); + void *pFileData; + unsigned FileDataSize; + io_read_all(File, &pFileData, &FileDataSize); + io_close(File); - return true; -} + CByteBufferReader ImageReader(static_cast(pFileData), FileDataSize); -static void WriteDataFromLoadedBytes(png_structp pPngStruct, png_bytep pOutBytes, png_size_t ByteCountToWrite) -{ - if(ByteCountToWrite > 0) + const bool LoadResult = CImageLoader::LoadPng(ImageReader, pFilename, Image, PngliteIncompatible); + free(pFileData); + if(!LoadResult) { - png_voidp pIO_Ptr = png_get_io_ptr(pPngStruct); + log_error("png", "failed to load image from file. filename='%s'", pFilename); + return false; + } - SImageByteBuffer *pByteLoader = (SImageByteBuffer *)pIO_Ptr; + if(Image.m_Format != CImageInfo::FORMAT_RGB && Image.m_Format != CImageInfo::FORMAT_RGBA) + { + log_error("png", "image has unsupported format. filename='%s' format='%s'", pFilename, Image.FormatName()); + Image.Free(); + return false; + } - size_t NewSize = pByteLoader->m_LoadOffset + (size_t)ByteCountToWrite; - pByteLoader->m_pvLoadedImageBytes->resize(NewSize); + return true; +} - mem_copy(&(*pByteLoader->m_pvLoadedImageBytes)[pByteLoader->m_LoadOffset], pOutBytes, (size_t)ByteCountToWrite); - pByteLoader->m_LoadOffset = NewSize; - } +static void PngWriteDataCallback(png_structp pPngStruct, png_bytep pOutBytes, png_size_t ByteCountToWrite) +{ + CByteBufferWriter *pWriter = static_cast(png_get_io_ptr(pPngStruct)); + pWriter->Write(pOutBytes, ByteCountToWrite); } -static void FlushPngWrite(png_structp png_ptr) {} +static void PngOutputFlushCallback(png_structp png_ptr) +{ + // no need to flush memory buffer +} -static size_t ImageLoaderHelperFormatToColorChannel(EImageFormat Format) +static int PngColorTypeFromFormat(CImageInfo::EImageFormat Format) { switch(Format) { - case IMAGE_FORMAT_R: - return 1; - case IMAGE_FORMAT_RA: - return 2; - case IMAGE_FORMAT_RGB: - return 3; - case IMAGE_FORMAT_RGBA: - return 4; + case CImageInfo::FORMAT_R: + return PNG_COLOR_TYPE_GRAY; + case CImageInfo::FORMAT_RA: + return PNG_COLOR_TYPE_GRAY_ALPHA; + case CImageInfo::FORMAT_RGB: + return PNG_COLOR_TYPE_RGB; + case CImageInfo::FORMAT_RGBA: + return PNG_COLOR_TYPE_RGBA; default: dbg_assert(false, "Format invalid"); dbg_break(); } } -bool SavePng(EImageFormat ImageFormat, const uint8_t *pRawBuffer, SImageByteBuffer &WrittenBytes, size_t Width, size_t Height) +bool CImageLoader::SavePng(CByteBufferWriter &Writer, const CImageInfo &Image) { png_structp pPngStruct = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); - if(pPngStruct == nullptr) { - dbg_msg("png", "libpng internal failure: png_create_write_struct failed."); + log_error("png", "libpng internal failure: png_create_write_struct failed."); return false; } png_infop pPngInfo = png_create_info_struct(pPngStruct); - if(pPngInfo == nullptr) { png_destroy_read_struct(&pPngStruct, nullptr, nullptr); - dbg_msg("png", "libpng internal failure: png_create_info_struct failed."); + log_error("png", "libpng internal failure: png_create_info_struct failed."); return false; } - WrittenBytes.m_LoadOffset = 0; - WrittenBytes.m_pvLoadedImageBytes->clear(); - - png_set_write_fn(pPngStruct, (png_bytep)&WrittenBytes, WriteDataFromLoadedBytes, FlushPngWrite); - - int ColorType = PNG_COLOR_TYPE_RGB; - size_t WriteBytesPerPixel = ImageLoaderHelperFormatToColorChannel(ImageFormat); - if(ImageFormat == IMAGE_FORMAT_R) - { - ColorType = PNG_COLOR_TYPE_GRAY; - } - else if(ImageFormat == IMAGE_FORMAT_RGBA) - { - ColorType = PNG_COLOR_TYPE_RGBA; - } - - png_set_IHDR(pPngStruct, pPngInfo, Width, Height, 8, ColorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); + png_set_write_fn(pPngStruct, (png_bytep)&Writer, PngWriteDataCallback, PngOutputFlushCallback); + png_set_IHDR(pPngStruct, pPngInfo, Image.m_Width, Image.m_Height, 8, PngColorTypeFromFormat(Image.m_Format), PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); png_write_info(pPngStruct, pPngInfo); - png_bytepp pRowPointers = new png_bytep[Height]; - size_t WidthBytes = Width * WriteBytesPerPixel; + png_bytepp pRowPointers = new png_bytep[Image.m_Height]; + const int WidthBytes = Image.m_Width * Image.PixelSize(); ptrdiff_t BufferOffset = 0; - for(size_t y = 0; y < Height; ++y) + for(size_t y = 0; y < Image.m_Height; ++y) { pRowPointers[y] = new png_byte[WidthBytes]; - mem_copy(pRowPointers[y], pRawBuffer + BufferOffset, WidthBytes); + mem_copy(pRowPointers[y], Image.m_pData + BufferOffset, WidthBytes); BufferOffset += (ptrdiff_t)WidthBytes; } png_write_image(pPngStruct, pRowPointers); - png_write_end(pPngStruct, pPngInfo); - for(size_t y = 0; y < Height; ++y) + for(size_t y = 0; y < Image.m_Height; ++y) { - delete[](pRowPointers[y]); + delete[] pRowPointers[y]; } - delete[](pRowPointers); + delete[] pRowPointers; png_destroy_info_struct(pPngStruct, &pPngInfo); png_destroy_write_struct(&pPngStruct, nullptr); return true; } + +bool CImageLoader::SavePng(IOHANDLE File, const char *pFilename, const CImageInfo &Image) +{ + if(!File) + { + log_error("png", "failed to open file for writing. filename='%s'", pFilename); + return false; + } + + CByteBufferWriter Writer; + if(!CImageLoader::SavePng(Writer, Image)) + { + // error already logged + io_close(File); + return false; + } + + const bool WriteSuccess = io_write(File, Writer.Data(), Writer.Size()) == Writer.Size(); + if(!WriteSuccess) + { + log_error("png", "failed to write PNG data to file. filename='%s'", pFilename); + } + io_close(File); + return WriteSuccess; +} diff --git a/src/engine/gfx/image_loader.h b/src/engine/gfx/image_loader.h index 9c54d94cf88..c2658a6e3c9 100644 --- a/src/engine/gfx/image_loader.h +++ b/src/engine/gfx/image_loader.h @@ -1,38 +1,57 @@ #ifndef ENGINE_GFX_IMAGE_LOADER_H #define ENGINE_GFX_IMAGE_LOADER_H -#include -#include +#include + +#include + #include -enum EImageFormat +class CByteBufferReader { - IMAGE_FORMAT_R = 0, - IMAGE_FORMAT_RA, - IMAGE_FORMAT_RGB, - IMAGE_FORMAT_RGBA, + const uint8_t *m_pData; + size_t m_Size; + size_t m_ReadOffset = 0; + bool m_Error = false; + +public: + CByteBufferReader(const uint8_t *pData, size_t Size) : + m_pData(pData), + m_Size(Size) {} + + bool Read(void *pData, size_t Size); + bool Error() const { return m_Error; } }; -typedef std::vector TImageByteBuffer; -struct SImageByteBuffer +class CByteBufferWriter { - SImageByteBuffer(std::vector *pvBuff) : - m_LoadOffset(0), m_pvLoadedImageBytes(pvBuff), m_Err(0) {} - size_t m_LoadOffset; - std::vector *m_pvLoadedImageBytes; - int m_Err; + std::vector m_vBuffer; + +public: + void Write(const void *pData, size_t Size); + const uint8_t *Data() const { return m_vBuffer.data(); } + size_t Size() const { return m_vBuffer.size(); } }; -enum +class CImageLoader { - PNGLITE_COLOR_TYPE = 1 << 0, - PNGLITE_BIT_DEPTH = 1 << 1, - PNGLITE_INTERLACE_TYPE = 1 << 2, - PNGLITE_COMPRESSION_TYPE = 1 << 3, - PNGLITE_FILTER_TYPE = 1 << 4, -}; + CImageLoader() = delete; + +public: + enum + { + PNGLITE_COLOR_TYPE = 1 << 0, + PNGLITE_BIT_DEPTH = 1 << 1, + PNGLITE_INTERLACE_TYPE = 1 << 2, + PNGLITE_COMPRESSION_TYPE = 1 << 3, + PNGLITE_FILTER_TYPE = 1 << 4, + }; -bool LoadPng(SImageByteBuffer &ByteLoader, const char *pFileName, int &PngliteIncompatible, size_t &Width, size_t &Height, uint8_t *&pImageBuff, EImageFormat &ImageFormat); -bool SavePng(EImageFormat ImageFormat, const uint8_t *pRawBuffer, SImageByteBuffer &WrittenBytes, size_t Width, size_t Height); + static bool LoadPng(CByteBufferReader &Reader, const char *pContextName, CImageInfo &Image, int &PngliteIncompatible); + static bool LoadPng(IOHANDLE File, const char *pFilename, CImageInfo &Image, int &PngliteIncompatible); + + static bool SavePng(CByteBufferWriter &Writer, const CImageInfo &Image); + static bool SavePng(IOHANDLE File, const char *pFilename, const CImageInfo &Image); +}; #endif // ENGINE_GFX_IMAGE_LOADER_H diff --git a/src/engine/gfx/image_manipulation.cpp b/src/engine/gfx/image_manipulation.cpp index 02c78149dd9..115a319adab 100644 --- a/src/engine/gfx/image_manipulation.cpp +++ b/src/engine/gfx/image_manipulation.cpp @@ -1,7 +1,88 @@ #include "image_manipulation.h" + #include #include +bool ConvertToRgba(uint8_t *pDest, const CImageInfo &SourceImage) +{ + if(SourceImage.m_Format == CImageInfo::FORMAT_RGBA) + { + mem_copy(pDest, SourceImage.m_pData, SourceImage.DataSize()); + return true; + } + else + { + const size_t SrcChannelCount = CImageInfo::PixelSize(SourceImage.m_Format); + const size_t DstChannelCount = CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA); + for(size_t Y = 0; Y < SourceImage.m_Height; ++Y) + { + for(size_t X = 0; X < SourceImage.m_Width; ++X) + { + size_t ImgOffsetSrc = (Y * SourceImage.m_Width * SrcChannelCount) + (X * SrcChannelCount); + size_t ImgOffsetDest = (Y * SourceImage.m_Width * DstChannelCount) + (X * DstChannelCount); + if(SourceImage.m_Format == CImageInfo::FORMAT_RGB) + { + mem_copy(&pDest[ImgOffsetDest], &SourceImage.m_pData[ImgOffsetSrc], SrcChannelCount); + pDest[ImgOffsetDest + 3] = 255; + } + else if(SourceImage.m_Format == CImageInfo::FORMAT_RA) + { + pDest[ImgOffsetDest + 0] = SourceImage.m_pData[ImgOffsetSrc]; + pDest[ImgOffsetDest + 1] = SourceImage.m_pData[ImgOffsetSrc]; + pDest[ImgOffsetDest + 2] = SourceImage.m_pData[ImgOffsetSrc]; + pDest[ImgOffsetDest + 3] = SourceImage.m_pData[ImgOffsetSrc + 1]; + } + else if(SourceImage.m_Format == CImageInfo::FORMAT_R) + { + pDest[ImgOffsetDest + 0] = 255; + pDest[ImgOffsetDest + 1] = 255; + pDest[ImgOffsetDest + 2] = 255; + pDest[ImgOffsetDest + 3] = SourceImage.m_pData[ImgOffsetSrc]; + } + else + { + dbg_assert(false, "SourceImage.m_Format invalid"); + } + } + } + return false; + } +} + +bool ConvertToRgbaAlloc(uint8_t *&pDest, const CImageInfo &SourceImage) +{ + pDest = static_cast(malloc(SourceImage.m_Width * SourceImage.m_Height * CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA))); + return ConvertToRgba(pDest, SourceImage); +} + +bool ConvertToRgba(CImageInfo &Image) +{ + if(Image.m_Format == CImageInfo::FORMAT_RGBA) + return true; + + uint8_t *pRgbaData; + ConvertToRgbaAlloc(pRgbaData, Image); + free(Image.m_pData); + Image.m_pData = pRgbaData; + Image.m_Format = CImageInfo::FORMAT_RGBA; + return false; +} + +void ConvertToGrayscale(const CImageInfo &Image) +{ + if(Image.m_Format == CImageInfo::FORMAT_R || Image.m_Format == CImageInfo::FORMAT_RA) + return; + + const size_t Step = Image.PixelSize(); + for(size_t i = 0; i < Image.m_Width * Image.m_Height; ++i) + { + const int Average = (Image.m_pData[i * Step] + Image.m_pData[i * Step + 1] + Image.m_pData[i * Step + 2]) / 3; + Image.m_pData[i * Step] = Average; + Image.m_pData[i * Step + 1] = Average; + Image.m_pData[i * Step + 2] = Average; + } +} + static constexpr int DILATE_BPP = 4; // RGBA assumed static constexpr uint8_t DILATE_ALPHA_THRESHOLD = 10; @@ -70,6 +151,12 @@ void DilateImage(uint8_t *pImageBuff, int w, int h) DilateImageSub(pImageBuff, w, h, 0, 0, w, h); } +void DilateImage(const CImageInfo &Image) +{ + dbg_assert(Image.m_Format == CImageInfo::FORMAT_RGBA, "Dilate requires RGBA format"); + DilateImage(Image.m_pData, Image.m_Width, Image.m_Height); +} + void DilateImageSub(uint8_t *pImageBuff, int w, int h, int x, int y, int sw, int sh) { uint8_t *apBuffer[2] = {nullptr, nullptr}; @@ -181,6 +268,15 @@ uint8_t *ResizeImage(const uint8_t *pImageData, int Width, int Height, int NewWi return pTmpData; } +void ResizeImage(CImageInfo &Image, int NewWidth, int NewHeight) +{ + uint8_t *pNewData = ResizeImage(Image.m_pData, Image.m_Width, Image.m_Height, NewWidth, NewHeight, Image.PixelSize()); + free(Image.m_pData); + Image.m_pData = pNewData; + Image.m_Width = NewWidth; + Image.m_Height = NewHeight; +} + int HighestBit(int OfVar) { if(!OfVar) diff --git a/src/engine/gfx/image_manipulation.h b/src/engine/gfx/image_manipulation.h index c427f2ec381..c005af7c167 100644 --- a/src/engine/gfx/image_manipulation.h +++ b/src/engine/gfx/image_manipulation.h @@ -1,14 +1,29 @@ #ifndef ENGINE_GFX_IMAGE_MANIPULATION_H #define ENGINE_GFX_IMAGE_MANIPULATION_H +#include + #include +// Destination must have appropriate size for RGBA data +bool ConvertToRgba(uint8_t *pDest, const CImageInfo &SourceImage); +// Allocates appropriate buffer with malloc, must be freed by caller +bool ConvertToRgbaAlloc(uint8_t *&pDest, const CImageInfo &SourceImage); +// Replaces existing image data with RGBA data (unless already RGBA) +bool ConvertToRgba(CImageInfo &Image); + +// Changes the image data (not the format) +void ConvertToGrayscale(const CImageInfo &Image); + // These functions assume that the image data is 4 bytes per pixel RGBA void DilateImage(uint8_t *pImageBuff, int w, int h); +void DilateImage(const CImageInfo &Image); void DilateImageSub(uint8_t *pImageBuff, int w, int h, int x, int y, int sw, int sh); -// returned pointer is allocated with malloc +// Returned buffer is allocated with malloc, must be freed by caller uint8_t *ResizeImage(const uint8_t *pImageData, int Width, int Height, int NewWidth, int NewHeight, int BPP); +// Replaces existing image data with resized buffer +void ResizeImage(CImageInfo &Image, int NewWidth, int NewHeight); int HighestBit(int OfVar); diff --git a/src/engine/graphics.h b/src/engine/graphics.h index 2528c0d792a..3699c3ea73f 100644 --- a/src/engine/graphics.h +++ b/src/engine/graphics.h @@ -3,6 +3,7 @@ #ifndef ENGINE_GRAPHICS_H #define ENGINE_GRAPHICS_H +#include "image.h" #include "kernel.h" #include "warning.h" @@ -64,68 +65,6 @@ struct SGraphicTileTexureCoords ubvec4 m_TexCoordBottomLeft; }; -class CImageInfo -{ -public: - enum EImageFormat - { - FORMAT_ERROR = -1, - FORMAT_RGB = 0, - FORMAT_RGBA = 1, - FORMAT_SINGLE_COMPONENT = 2, - }; - - /** - * Contains the width of the image - */ - size_t m_Width = 0; - - /** - * Contains the height of the image - */ - size_t m_Height = 0; - - /** - * Contains the format of the image. - * - * @see EImageFormat - */ - EImageFormat m_Format = FORMAT_ERROR; - - /** - * Pointer to the image data. - */ - uint8_t *m_pData = nullptr; - - void Free() - { - m_Width = 0; - m_Height = 0; - m_Format = FORMAT_ERROR; - free(m_pData); - m_pData = nullptr; - } - - static size_t PixelSize(EImageFormat Format) - { - dbg_assert(Format != FORMAT_ERROR, "Format invalid"); - static const size_t s_aSizes[] = {3, 4, 1}; - return s_aSizes[(int)Format]; - } - - size_t PixelSize() const - { - return PixelSize(m_Format); - } - - size_t DataSize() const - { - return m_Width * m_Height * PixelSize(m_Format); - } -}; - -bool ConvertToRGBA(uint8_t *pDest, const CImageInfo &SrcImage); - /* Structure: CVideoMode */ @@ -330,16 +269,11 @@ class IGraphics : public IInterface virtual const TTwGraphicsGpuList &GetGpus() const = 0; virtual bool LoadPng(CImageInfo &Image, const char *pFilename, int StorageType) = 0; + virtual bool LoadPng(CImageInfo &Image, const uint8_t *pData, size_t DataSize, const char *pContextName) = 0; virtual bool CheckImageDivisibility(const char *pContextName, CImageInfo &Image, int DivX, int DivY, bool AllowResize) = 0; virtual bool IsImageFormatRgba(const char *pContextName, const CImageInfo &Image) = 0; - // destination and source buffer require to have the same width and height - virtual void CopyTextureBufferSub(uint8_t *pDestBuffer, const CImageInfo &SourceImage, size_t SubOffsetX, size_t SubOffsetY, size_t SubCopyWidth, size_t SubCopyHeight) = 0; - - // destination width must be equal to the subwidth of the source - virtual void CopyTextureFromTextureBufferSub(uint8_t *pDestBuffer, size_t DestWidth, size_t DestHeight, const CImageInfo &SourceImage, size_t SrcSubOffsetX, size_t SrcSubOffsetY, size_t SrcSubCopyWidth, size_t SrcSubCopyHeight) = 0; - virtual void UnloadTexture(CTextureHandle *pIndex) = 0; virtual CTextureHandle LoadTextureRaw(const CImageInfo &Image, int Flags, const char *pTexName = nullptr) = 0; virtual CTextureHandle LoadTextureRawMove(CImageInfo &Image, int Flags, const char *pTexName = nullptr) = 0; diff --git a/src/engine/image.h b/src/engine/image.h new file mode 100644 index 00000000000..6b794952aad --- /dev/null +++ b/src/engine/image.h @@ -0,0 +1,137 @@ +#ifndef ENGINE_IMAGE_H +#define ENGINE_IMAGE_H + +#include + +#include + +/** + * Represents an image that has been loaded into main memory. + */ +class CImageInfo +{ +public: + /** + * Defines the format of image data. + */ + enum EImageFormat + { + FORMAT_UNDEFINED = -1, + FORMAT_RGB = 0, + FORMAT_RGBA = 1, + FORMAT_R = 2, + FORMAT_RA = 3, + }; + + /** + * Width of the image. + */ + size_t m_Width = 0; + + /** + * Height of the image. + */ + size_t m_Height = 0; + + /** + * Format of the image. + * + * @see EImageFormat + */ + EImageFormat m_Format = FORMAT_UNDEFINED; + + /** + * Pointer to the image data. + */ + uint8_t *m_pData = nullptr; + + /** + * Frees the image data and clears all info. + */ + void Free(); + + /** + * Returns the pixel size in bytes for the given image format. + * + * @param Format Image format, must not be `FORMAT_UNDEFINED`. + * + * @return Size of one pixel with the given image format. + */ + static size_t PixelSize(EImageFormat Format); + + /** + * Returns a readable name for the given image format. + * + * @param Format Image format. + * + * @return Readable name for the given image format. + */ + static const char *FormatName(EImageFormat Format); + + /** + * Returns the pixel size in bytes for the format of this image. + * + * @return Size of one pixel with the format of this image. + * + * @remark The format must not be `FORMAT_UNDEFINED`. + */ + size_t PixelSize() const; + + /** + * Returns a readable name for the format of this image. + * + * @return Readable name for the format of this image. + */ + const char *FormatName() const; + + /** + * Returns the size of the data, as derived from the width, height and pixel size. + * + * @return Expected size of the image data. + */ + size_t DataSize() const; + + /** + * Returns whether this image is equal to the given image + * in width, height, format and data. + * + * @param Other The image to compare with. + * + * @return `true` if the images are identical, `false` otherwise. + */ + bool DataEquals(const CImageInfo &Other) const; + + /** + * Returns the color of the pixel at the specified position. + * + * @param x The x-coordinate to read from. + * @param y The y-coordinate to read from. + * + * @return Pixel color converted to normalized RGBA. + */ + ColorRGBA PixelColor(size_t x, size_t y) const; + + /** + * Sets the color of the pixel at the specified position. + * + * @param x The x-coordinate to write to. + * @param y The y-coordinate to write to. + * @param Color The normalized RGBA color to write. + */ + void SetPixelColor(size_t x, size_t y, ColorRGBA Color) const; + + /** + * Copies a rectangle of image data from the given image to this image. + * + * @param SrcImage The image to copy data from. + * @param SrcX The x-offset in the source image. + * @param SrcY The y-offset in the source image. + * @param Width The width of the rectangle to copy. + * @param Height The height of the rectangle to copy. + * @param DestX The x-offset in the destination image (this). + * @param DestY The y-offset in the destination image (this). + */ + void CopyRectFrom(const CImageInfo &SrcImage, size_t SrcX, size_t SrcY, size_t Width, size_t Height, size_t DestX, size_t DestY) const; +}; + +#endif diff --git a/src/game/client/components/mapimages.cpp b/src/game/client/components/mapimages.cpp index 10e703f3d8e..4a34c5f25ce 100644 --- a/src/game/client/components/mapimages.cpp +++ b/src/game/client/components/mapimages.cpp @@ -301,7 +301,7 @@ IGraphics::CTextureHandle CMapImages::GetEntities(EMapImageEntityLayerType Entit const size_t CopyHeight = ImgInfo.m_Height / 16; const size_t OffsetX = (size_t)(TileIndex % 16) * CopyWidth; const size_t OffsetY = (size_t)(TileIndex / 16) * CopyHeight; - Graphics()->CopyTextureBufferSub(BuildImageInfo.m_pData, ImgInfo, OffsetX, OffsetY, CopyWidth, CopyHeight); + BuildImageInfo.CopyRectFrom(ImgInfo, OffsetX, OffsetY, CopyWidth, CopyHeight, OffsetX, OffsetY); } } diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp index 483db0251b8..e9be47778af 100644 --- a/src/game/client/components/menus.cpp +++ b/src/game/client/components/menus.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -2396,16 +2397,7 @@ int CMenus::MenuImageScan(const char *pName, int IsDir, int DirType, void *pUser MenuImage.m_OrgTexture = pSelf->Graphics()->LoadTextureRaw(Info, 0, aPath); - // create gray scale version - unsigned char *pData = static_cast(Info.m_pData); - const size_t Step = Info.PixelSize(); - for(size_t i = 0; i < Info.m_Width * Info.m_Height; i++) - { - int v = (pData[i * Step] + pData[i * Step + 1] + pData[i * Step + 2]) / 3; - pData[i * Step] = v; - pData[i * Step + 1] = v; - pData[i * Step + 2] = v; - } + ConvertToGrayscale(Info); MenuImage.m_GreyTexture = pSelf->Graphics()->LoadTextureRawMove(Info, 0, aPath); str_truncate(MenuImage.m_aName, sizeof(MenuImage.m_aName), pName, str_length(pName) - str_length(pExtension)); diff --git a/src/game/client/components/menus_browser.cpp b/src/game/client/components/menus_browser.cpp index 2e874870695..993cc038cc0 100644 --- a/src/game/client/components/menus_browser.cpp +++ b/src/game/client/components/menus_browser.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -1992,16 +1993,7 @@ void CMenus::LoadCommunityIconFinish(const char *pCommunityId, CImageInfo &Info, CommunityIcon.m_Sha256 = Sha256; CommunityIcon.m_OrgTexture = Graphics()->LoadTextureRaw(Info, 0, pCommunityId); - // create gray scale version - unsigned char *pData = static_cast(Info.m_pData); - const size_t Step = Info.PixelSize(); - for(size_t i = 0; i < Info.m_Width * Info.m_Height; i++) - { - int v = (pData[i * Step] + pData[i * Step + 1] + pData[i * Step + 2]) / 3; - pData[i * Step] = v; - pData[i * Step + 1] = v; - pData[i * Step + 2] = v; - } + ConvertToGrayscale(Info); CommunityIcon.m_GreyTexture = Graphics()->LoadTextureRawMove(Info, 0, pCommunityId); auto ExistingIcon = std::find_if(m_vCommunityIcons.begin(), m_vCommunityIcons.end(), [pCommunityId](const SCommunityIcon &Element) { diff --git a/src/game/client/components/skins.cpp b/src/game/client/components/skins.cpp index e774ad94d5d..221a8805942 100644 --- a/src/game/client/components/skins.cpp +++ b/src/game/client/components/skins.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -237,14 +238,7 @@ const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info) // get feet outline size CheckMetrics(Skin.m_Metrics.m_Feet, pData, Pitch, FeetOutlineOffsetX, FeetOutlineOffsetY, FeetOutlineWidth, FeetOutlineHeight); - // make the texture gray scale - for(size_t i = 0; i < Info.m_Width * Info.m_Height; i++) - { - int v = (pData[i * PixelStep] + pData[i * PixelStep + 1] + pData[i * PixelStep + 2]) / 3; - pData[i * PixelStep] = v; - pData[i * PixelStep + 1] = v; - pData[i * PixelStep + 2] = v; - } + ConvertToGrayscale(Info); int aFreq[256] = {0}; int OrgWeight = 0; diff --git a/src/game/client/components/skins7.cpp b/src/game/client/components/skins7.cpp index e4179168e99..432a6e64df8 100644 --- a/src/game/client/components/skins7.cpp +++ b/src/game/client/components/skins7.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -92,14 +93,7 @@ int CSkins7::SkinPartScan(const char *pName, int IsDir, int DirType, void *pUser Part.m_BloodColor = ColorRGBA(normalize(vec3(aColors[0], aColors[1], aColors[2]))); } - // create colorless version - for(size_t i = 0; i < Info.m_Width * Info.m_Height; i++) - { - const int Average = (pData[i * Step] + pData[i * Step + 1] + pData[i * Step + 2]) / 3; - pData[i * Step] = Average; - pData[i * Step + 1] = Average; - pData[i * Step + 2] = Average; - } + ConvertToGrayscale(Info); Part.m_ColorTexture = pSelf->Graphics()->LoadTextureRawMove(Info, 0, aFilename); diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 90f36db0b4a..fb0e3b780d4 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -850,41 +850,14 @@ bool CEditor::CallbackSaveImage(const char *pFileName, int StorageType, void *pU std::shared_ptr pImg = pEditor->m_Map.m_vpImages[pEditor->m_SelectedImage]; - EImageFormat OutputFormat; - switch(pImg->m_Format) - { - case CImageInfo::FORMAT_RGB: - OutputFormat = IMAGE_FORMAT_RGB; - break; - case CImageInfo::FORMAT_RGBA: - OutputFormat = IMAGE_FORMAT_RGBA; - break; - case CImageInfo::FORMAT_SINGLE_COMPONENT: - OutputFormat = IMAGE_FORMAT_R; - break; - default: - dbg_assert(false, "Image has invalid format."); - return false; - }; - - TImageByteBuffer ByteBuffer; - SImageByteBuffer ImageByteBuffer(&ByteBuffer); - if(SavePng(OutputFormat, pImg->m_pData, ImageByteBuffer, pImg->m_Width, pImg->m_Height)) + if(CImageLoader::SavePng(pEditor->Storage()->OpenFile(pFileName, IOFLAG_WRITE, StorageType), pFileName, *pImg)) { - IOHANDLE File = pEditor->Storage()->OpenFile(pFileName, IOFLAG_WRITE, StorageType); - if(File) - { - io_write(File, &ByteBuffer.front(), ByteBuffer.size()); - io_close(File); - pEditor->m_Dialog = DIALOG_NONE; - return true; - } - pEditor->ShowFileDialogError("Failed to open file '%s'.", pFileName); - return false; + pEditor->m_Dialog = DIALOG_NONE; + return true; } else { - pEditor->ShowFileDialogError("Failed to write image to file."); + pEditor->ShowFileDialogError("Failed to write image to file '%s'.", pFileName); return false; } } @@ -4401,18 +4374,13 @@ bool CEditor::ReplaceImage(const char *pFileName, int StorageType, bool CheckDup str_copy(pImg->m_aName, aBuf); pImg->m_External = IsVanillaImage(pImg->m_aName); - if(!pImg->m_External && pImg->m_Format != CImageInfo::FORMAT_RGBA) + if(!pImg->m_External) { - uint8_t *pRgbaData = static_cast(malloc((size_t)pImg->m_Width * pImg->m_Height * CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA))); - ConvertToRGBA(pRgbaData, *pImg); - free(pImg->m_pData); - pImg->m_pData = pRgbaData; - pImg->m_Format = CImageInfo::FORMAT_RGBA; - } - - if(!pImg->m_External && g_Config.m_ClEditorDilate == 1) - { - DilateImage(pImg->m_pData, pImg->m_Width, pImg->m_Height); + ConvertToRgba(*pImg); + if(g_Config.m_ClEditorDilate == 1) + { + DilateImage(*pImg); + } } pImg->m_AutoMapper.Load(pImg->m_aName); @@ -4473,18 +4441,13 @@ bool CEditor::AddImage(const char *pFileName, int StorageType, void *pUser) pImg->m_pData = ImgInfo.m_pData; pImg->m_External = IsVanillaImage(aBuf); - if(pImg->m_Format != CImageInfo::FORMAT_RGBA) + if(!pImg->m_External) { - uint8_t *pRgbaData = static_cast(malloc((size_t)pImg->m_Width * pImg->m_Height * CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA))); - ConvertToRGBA(pRgbaData, *pImg); - free(pImg->m_pData); - pImg->m_pData = pRgbaData; - pImg->m_Format = CImageInfo::FORMAT_RGBA; - } - - if(!pImg->m_External && g_Config.m_ClEditorDilate == 1) - { - DilateImage(pImg->m_pData, pImg->m_Width, pImg->m_Height); + ConvertToRgba(*pImg); + if(g_Config.m_ClEditorDilate == 1) + { + DilateImage(*pImg); + } } int TextureLoadFlag = pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE; diff --git a/src/game/editor/mapitems/image.cpp b/src/game/editor/mapitems/image.cpp index 1c55ae7e56d..0266ccb99d7 100644 --- a/src/game/editor/mapitems/image.cpp +++ b/src/game/editor/mapitems/image.cpp @@ -52,29 +52,3 @@ void CEditorImage::AnalyseTileFlags() } } } - -bool CEditorImage::DataEquals(const CEditorImage &Other) const -{ - // If height, width or pixel size don't match, then data cannot be equal - const size_t ImgPixelSize = PixelSize(); - - if(Other.m_Height != m_Height || Other.m_Width != m_Width || Other.PixelSize() != ImgPixelSize) - return false; - - const auto &&GetPixel = [&](uint8_t *pData, int x, int y, size_t p) -> uint8_t { - return pData[x * ImgPixelSize + (m_Width * ImgPixelSize * y) + p]; - }; - - // Look through every pixel and check if there are any difference - for(size_t y = 0; y < m_Height; y += ImgPixelSize) - { - for(size_t x = 0; x < m_Width; x += ImgPixelSize) - { - for(size_t p = 0; p < ImgPixelSize; p++) - if(GetPixel(m_pData, x, y, p) != GetPixel(Other.m_pData, x, y, p)) - return false; - } - } - - return true; -} diff --git a/src/game/editor/mapitems/image.h b/src/game/editor/mapitems/image.h index 52731f7ff0d..7e2142f6303 100644 --- a/src/game/editor/mapitems/image.h +++ b/src/game/editor/mapitems/image.h @@ -14,7 +14,6 @@ class CEditorImage : public CImageInfo, public CEditorComponent void OnInit(CEditor *pEditor) override; void AnalyseTileFlags(); - bool DataEquals(const CEditorImage &Other) const; IGraphics::CTextureHandle m_Texture; int m_External = 0; diff --git a/src/game/editor/mapitems/map_io.cpp b/src/game/editor/mapitems/map_io.cpp index a834a3d990d..8322289fd1f 100644 --- a/src/game/editor/mapitems/map_io.cpp +++ b/src/game/editor/mapitems/map_io.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -509,14 +510,7 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio pImg->m_Height = ImgInfo.m_Height; pImg->m_Format = ImgInfo.m_Format; pImg->m_pData = ImgInfo.m_pData; - if(pImg->m_Format != CImageInfo::FORMAT_RGBA) - { - uint8_t *pRgbaData = static_cast(malloc((size_t)pImg->m_Width * pImg->m_Height * CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA))); - ConvertToRGBA(pRgbaData, *pImg); - free(pImg->m_pData); - pImg->m_pData = pRgbaData; - pImg->m_Format = CImageInfo::FORMAT_RGBA; - } + ConvertToRgba(*pImg); int TextureLoadFlag = m_pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE; if(ImgInfo.m_Width % 16 != 0 || ImgInfo.m_Height % 16 != 0) diff --git a/src/game/editor/tileart.cpp b/src/game/editor/tileart.cpp index ec79d71bd85..102a75cc2c0 100644 --- a/src/game/editor/tileart.cpp +++ b/src/game/editor/tileart.cpp @@ -17,51 +17,6 @@ bool operator<(const ColorRGBA &Left, const ColorRGBA &Right) return Left.a < Right.a; } -static ColorRGBA GetPixelColor(const CImageInfo &Image, size_t x, size_t y) -{ - uint8_t *pData = Image.m_pData; - const size_t PixelSize = Image.PixelSize(); - const size_t PixelStartIndex = x * PixelSize + (Image.m_Width * PixelSize * y); - - ColorRGBA Color = {255, 255, 255, 255}; - if(PixelSize == 1) - { - Color.a = pData[PixelStartIndex]; - } - else - { - Color.r = pData[PixelStartIndex + 0]; - Color.g = pData[PixelStartIndex + 1]; - Color.b = pData[PixelStartIndex + 2]; - - if(PixelSize == 4) - Color.a = pData[PixelStartIndex + 3]; - } - - return Color; -} - -static void SetPixelColor(CImageInfo *pImage, size_t x, size_t y, ColorRGBA Color) -{ - uint8_t *pData = pImage->m_pData; - const size_t PixelSize = pImage->PixelSize(); - const size_t PixelStartIndex = x * PixelSize + (pImage->m_Width * PixelSize * y); - - if(PixelSize == 1) - { - pData[PixelStartIndex] = Color.a; - } - else - { - pData[PixelStartIndex + 0] = Color.r; - pData[PixelStartIndex + 1] = Color.g; - pData[PixelStartIndex + 2] = Color.b; - - if(PixelSize == 4) - pData[PixelStartIndex + 3] = Color.a; - } -} - static std::vector GetUniqueColors(const CImageInfo &Image) { std::set ColorSet; @@ -70,7 +25,7 @@ static std::vector GetUniqueColors(const CImageInfo &Image) { for(size_t y = 0; y < Image.m_Height; y++) { - ColorRGBA Color = GetPixelColor(Image, x, y); + ColorRGBA Color = Image.PixelColor(x, y); if(Color.a > 0 && ColorSet.insert(Color).second) vUniqueColors.push_back(Color); } @@ -106,12 +61,12 @@ static std::vector> GroupColors(const std::vecto return vaColorGroups; } -static void SetColorTile(CImageInfo *pImage, int x, int y, ColorRGBA Color) +static void SetColorTile(CImageInfo &Image, int x, int y, ColorRGBA Color) { for(int i = 0; i < TileSize; i++) { for(int j = 0; j < TileSize; j++) - SetPixelColor(pImage, x * TileSize + i, y * TileSize + j, Color); + Image.SetPixelColor(x * TileSize + i, y * TileSize + j, Color); } } @@ -128,7 +83,7 @@ static CImageInfo ColorGroupToImage(const std::array &aColo for(int x = 0; x < NumTilesRow; x++) { int ColorIndex = x + NumTilesRow * y; - SetColorTile(&Image, x, y, aColorGroup[ColorIndex]); + SetColorTile(Image, x, y, aColorGroup[ColorIndex]); } } @@ -179,7 +134,7 @@ static void SetTilelayerIndices(const std::shared_ptr &pLayer, cons for(int x = 0; x < pLayer->m_Width; x++) { for(int y = 0; y < pLayer->m_Height; y++) - pLayer->m_pTiles[x + y * pLayer->m_Width].m_Index = GetColorIndex(aColorGroup, GetPixelColor(Image, x, y)); + pLayer->m_pTiles[x + y * pLayer->m_Width].m_Index = GetColorIndex(aColorGroup, Image.PixelColor(x, y)); } } diff --git a/src/tools/dilate.cpp b/src/tools/dilate.cpp index 3ee2ff6c0ee..d4246a26b9e 100644 --- a/src/tools/dilate.cpp +++ b/src/tools/dilate.cpp @@ -1,88 +1,49 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ + #include #include + #include #include -#include -int DilateFile(const char *pFilename) +static bool DilateFile(const char *pFilename) { - IOHANDLE File = io_open(pFilename, IOFLAG_READ); - if(File) - { - io_seek(File, 0, IOSEEK_END); - long int FileSize = io_tell(File); - if(FileSize <= 0) - { - io_close(File); - dbg_msg("dilate", "failed to get file size (%ld). filename='%s'", FileSize, pFilename); - return false; - } - io_seek(File, 0, IOSEEK_START); - TImageByteBuffer ByteBuffer; - SImageByteBuffer ImageByteBuffer(&ByteBuffer); - - ByteBuffer.resize(FileSize); - io_read(File, &ByteBuffer.front(), FileSize); - - io_close(File); - - CImageInfo Img; - EImageFormat ImageFormat; - int PngliteIncompatible; - if(LoadPng(ImageByteBuffer, pFilename, PngliteIncompatible, Img.m_Width, Img.m_Height, Img.m_pData, ImageFormat)) - { - if(ImageFormat != IMAGE_FORMAT_RGBA) - { - free(Img.m_pData); - dbg_msg("dilate", "%s: not an RGBA image", pFilename); - return -1; - } - - DilateImage(Img.m_pData, Img.m_Width, Img.m_Height); - - // save here - IOHANDLE SaveFile = io_open(pFilename, IOFLAG_WRITE); - if(SaveFile) - { - TImageByteBuffer ByteBuffer2; - SImageByteBuffer ImageByteBuffer2(&ByteBuffer2); - - if(SavePng(IMAGE_FORMAT_RGBA, Img.m_pData, ImageByteBuffer2, Img.m_Width, Img.m_Height)) - io_write(SaveFile, &ByteBuffer2.front(), ByteBuffer2.size()); - io_close(SaveFile); + CImageInfo Image; + int PngliteIncompatible; + if(!CImageLoader::LoadPng(io_open(pFilename, IOFLAG_READ), pFilename, Image, PngliteIncompatible)) + return false; - free(Img.m_pData); - } - } - else - { - dbg_msg("dilate", "failed unknown image format: %s", pFilename); - return -1; - } - } - else + if(Image.m_Format != CImageInfo::FORMAT_RGBA) { - dbg_msg("dilate", "failed to open image file. filename='%s'", pFilename); - return -1; + log_error("dilate", "ERROR: only RGBA PNG images are supported"); + Image.Free(); + return false; } - return 0; + DilateImage(Image); + + const bool SaveResult = CImageLoader::SavePng(io_open(pFilename, IOFLAG_WRITE), pFilename, Image); + Image.Free(); + + return SaveResult; } int main(int argc, const char **argv) { CCmdlineFix CmdlineFix(&argc, &argv); log_set_global_logger_default(); + if(argc == 1) { - dbg_msg("usage", "%s FILE1 [ FILE2... ]", argv[0]); + log_error("dilate", "Usage: %s [ ...]", argv[0]); return -1; } + bool Success = true; for(int i = 1; i < argc; i++) - DilateFile(argv[i]); - - return 0; + { + Success &= DilateFile(argv[i]); + } + return Success ? 0 : -1; } diff --git a/src/tools/map_convert_07.cpp b/src/tools/map_convert_07.cpp index 135bd7f0b24..09f801b8eb3 100644 --- a/src/tools/map_convert_07.cpp +++ b/src/tools/map_convert_07.cpp @@ -3,12 +3,14 @@ #include #include + #include -#include #include #include + #include #include + /* Usage: map_convert_07 */ @@ -25,53 +27,6 @@ int g_NextDataItemId = -1; int g_aImageIds[MAX_MAPIMAGES]; -int LoadPng(CImageInfo *pImg, const char *pFilename) -{ - IOHANDLE File = io_open(pFilename, IOFLAG_READ); - if(File) - { - io_seek(File, 0, IOSEEK_END); - long int FileSize = io_tell(File); - if(FileSize <= 0) - { - io_close(File); - dbg_msg("map_convert_07", "failed to get file size (%ld). filename='%s'", FileSize, pFilename); - return false; - } - io_seek(File, 0, IOSEEK_START); - TImageByteBuffer ByteBuffer; - SImageByteBuffer ImageByteBuffer(&ByteBuffer); - - ByteBuffer.resize(FileSize); - io_read(File, &ByteBuffer.front(), FileSize); - - io_close(File); - - uint8_t *pImgBuffer = NULL; - EImageFormat ImageFormat; - int PngliteIncompatible; - if(LoadPng(ImageByteBuffer, pFilename, PngliteIncompatible, pImg->m_Width, pImg->m_Height, pImgBuffer, ImageFormat)) - { - pImg->m_pData = pImgBuffer; - - if(ImageFormat == IMAGE_FORMAT_RGBA && pImg->m_Width <= (2 << 13) && pImg->m_Height <= (2 << 13)) - { - pImg->m_Format = CImageInfo::FORMAT_RGBA; - } - else - { - dbg_msg("map_convert_07", "invalid image format. filename='%s'", pFilename); - return 0; - } - } - else - return 0; - } - else - return 0; - return 1; -} - bool CheckImageDimensions(void *pLayerItem, int LayerType, const char *pFilename) { if(LayerType != MAPITEMTYPE_LAYER) @@ -117,15 +72,19 @@ void *ReplaceImageItem(int Index, CMapItemImage *pImgItem, CMapItemImage *pNewIm dbg_msg("map_convert_07", "embedding image '%s'", pName); - CImageInfo ImgInfo; char aStr[IO_MAX_PATH_LENGTH]; str_format(aStr, sizeof(aStr), "data/mapres/%s.png", pName); - if(!LoadPng(&ImgInfo, aStr)) + + CImageInfo ImgInfo; + int PngliteIncompatible; + if(!CImageLoader::LoadPng(io_open(aStr, IOFLAG_READ), aStr, ImgInfo, PngliteIncompatible)) return pImgItem; // keep as external if we don't have a mapres to replace - if(ImgInfo.m_Format != CImageInfo::FORMAT_RGBA) + const size_t MaxImageDimension = 1 << 13; + if(ImgInfo.m_Format != CImageInfo::FORMAT_RGBA || ImgInfo.m_Width > MaxImageDimension || ImgInfo.m_Height > MaxImageDimension) { - dbg_msg("map_convert_07", "image '%s' is not in RGBA format", aStr); + dbg_msg("map_convert_07", "ERROR: only RGBA PNG images with maximum width/height %d are supported", MaxImageDimension); + ImgInfo.Free(); return pImgItem; } diff --git a/src/tools/map_create_pixelart.cpp b/src/tools/map_create_pixelart.cpp index b2486b17651..98d68d5ea2e 100644 --- a/src/tools/map_create_pixelart.cpp +++ b/src/tools/map_create_pixelart.cpp @@ -1,16 +1,16 @@ #include #include + #include -#include #include #include + #include bool CreatePixelArt(const char[3][IO_MAX_PATH_LENGTH], const int[2], const int[2], int[2], const bool[2]); void InsertCurrentQuads(CDataFileReader &, CMapItemLayerQuads *, CQuad *); int InsertPixelArtQuads(CQuad *, int &, const CImageInfo &, const int[2], const int[2], const bool[2]); -bool LoadPng(CImageInfo *, const char *); bool OpenMaps(const char[2][IO_MAX_PATH_LENGTH], CDataFileReader &, CDataFileWriter &); void SaveOutputMap(CDataFileReader &, CDataFileWriter &, CMapItemLayerQuads *, int, CQuad *, int); @@ -73,7 +73,8 @@ int main(int argc, const char **argv) bool CreatePixelArt(const char aFilenames[3][IO_MAX_PATH_LENGTH], const int aLayerId[2], const int aStartingPos[2], int aPixelSizes[2], const bool aArtOptions[2]) { CImageInfo Img; - if(!LoadPng(&Img, aFilenames[2])) + int PngliteIncompatible; + if(!CImageLoader::LoadPng(io_open(aFilenames[2], IOFLAG_READ), aFilenames[2], Img, PngliteIncompatible)) return false; aPixelSizes[0] = aPixelSizes[0] ? aPixelSizes[0] : GetImagePixelSize(Img); @@ -227,7 +228,7 @@ bool GetPixelClamped(const CImageInfo &Img, size_t x, size_t y, uint8_t aPixel[4 aPixel[3] = 255; const size_t PixelSize = Img.PixelSize(); - for(size_t i = 0; i < PixelSize; i++) + for(size_t i = 0; i < minimum(std::size(aPixel), PixelSize); i++) // minimum is used here to avoid false positive stringop-overflow warning aPixel[i] = Img.m_pData[x * PixelSize + (Img.m_Width * PixelSize * y) + i]; return aPixel[3] > 0; @@ -315,54 +316,6 @@ CQuad CreateNewQuad(const float PosX, const float PosY, const int Width, const i return Quad; } -bool LoadPng(CImageInfo *pImg, const char *pFilename) -{ - IOHANDLE File = io_open(pFilename, IOFLAG_READ); - if(!File) - { - dbg_msg("map_create_pixelart", "ERROR: Unable to open file %s", pFilename); - return false; - } - - io_seek(File, 0, IOSEEK_END); - long int FileSize = io_tell(File); - if(FileSize <= 0) - { - io_close(File); - dbg_msg("map_create_pixelart", "ERROR: Failed to get file size (%ld). filename='%s'", FileSize, pFilename); - return false; - } - io_seek(File, 0, IOSEEK_START); - TImageByteBuffer ByteBuffer; - SImageByteBuffer ImageByteBuffer(&ByteBuffer); - - ByteBuffer.resize(FileSize); - io_read(File, &ByteBuffer.front(), FileSize); - io_close(File); - - uint8_t *pImgBuffer = NULL; - EImageFormat ImageFormat; - int PngliteIncompatible; - - if(!LoadPng(ImageByteBuffer, pFilename, PngliteIncompatible, pImg->m_Width, pImg->m_Height, pImgBuffer, ImageFormat)) - { - dbg_msg("map_create_pixelart", "ERROR: Unable to load a valid PNG from file %s", pFilename); - return false; - } - - if(ImageFormat != IMAGE_FORMAT_RGBA && ImageFormat != IMAGE_FORMAT_RGB) - { - dbg_msg("map_create_pixelart", "ERROR: only RGB and RGBA PNG images are supported"); - free(pImgBuffer); - return false; - } - - pImg->m_pData = pImgBuffer; - pImg->m_Format = ImageFormat == IMAGE_FORMAT_RGB ? CImageInfo::FORMAT_RGB : CImageInfo::FORMAT_RGBA; - - return true; -} - bool OpenMaps(const char pMapNames[2][IO_MAX_PATH_LENGTH], CDataFileReader &InputMap, CDataFileWriter &OutputMap) { IStorage *pStorage = CreateLocalStorage(); diff --git a/src/tools/map_extract.cpp b/src/tools/map_extract.cpp index 17b68792149..d38cc6b3aa5 100644 --- a/src/tools/map_extract.cpp +++ b/src/tools/map_extract.cpp @@ -1,10 +1,12 @@ // Adapted from TWMapImagesRecovery by Tardo: https://github.com/Tardo/TWMapImagesRecovery + #include #include + #include -#include #include #include + #include static void PrintMapInfo(CDataFileReader &Reader) @@ -50,22 +52,18 @@ static void ExtractMapImages(CDataFileReader &Reader, const char *pPathSave) continue; } - IOHANDLE File = io_open(aBuf, IOFLAG_WRITE); - if(File) - { - log_info("map_extract", "writing image: %s (%dx%d)", aBuf, pItem->m_Width, pItem->m_Height); - TImageByteBuffer ByteBuffer; - SImageByteBuffer ImageByteBuffer(&ByteBuffer); - - if(SavePng(IMAGE_FORMAT_RGBA, (const uint8_t *)Reader.GetData(pItem->m_ImageData), ImageByteBuffer, pItem->m_Width, pItem->m_Height)) - io_write(File, &ByteBuffer.front(), ByteBuffer.size()); - io_close(File); - Reader.UnloadData(pItem->m_ImageData); - } - else + CImageInfo Image; + Image.m_Width = pItem->m_Width; + Image.m_Height = pItem->m_Height; + Image.m_Format = CImageInfo::FORMAT_RGBA; + Image.m_pData = static_cast(Reader.GetData(pItem->m_ImageData)); + + log_info("map_extract", "writing image: %s (%dx%d)", aBuf, pItem->m_Width, pItem->m_Height); + if(!CImageLoader::SavePng(io_open(aBuf, IOFLAG_WRITE), aBuf, Image)) { - log_error("map_extract", "failed to open image file for writing. filename='%s'", aBuf); + log_error("map_extract", "failed to write image file. filename='%s'", aBuf); } + Reader.UnloadData(pItem->m_ImageData); } } diff --git a/src/tools/map_replace_image.cpp b/src/tools/map_replace_image.cpp index 07666020f01..2c2ba68a89d 100644 --- a/src/tools/map_replace_image.cpp +++ b/src/tools/map_replace_image.cpp @@ -3,11 +3,13 @@ #include #include + #include -#include #include #include + #include + /* Usage: map_replace_image Notes: map filepath must be relative to user default teeworlds folder @@ -23,55 +25,6 @@ int g_NewDataId = -1; int g_NewDataSize = 0; void *g_pNewData = nullptr; -bool LoadPng(CImageInfo *pImg, const char *pFilename) -{ - IOHANDLE File = io_open(pFilename, IOFLAG_READ); - if(File) - { - io_seek(File, 0, IOSEEK_END); - long int FileSize = io_tell(File); - if(FileSize <= 0) - { - io_close(File); - return false; - } - io_seek(File, 0, IOSEEK_START); - TImageByteBuffer ByteBuffer; - SImageByteBuffer ImageByteBuffer(&ByteBuffer); - - ByteBuffer.resize(FileSize); - io_read(File, &ByteBuffer.front(), FileSize); - - io_close(File); - - uint8_t *pImgBuffer = NULL; - EImageFormat ImageFormat; - int PngliteIncompatible; - if(LoadPng(ImageByteBuffer, pFilename, PngliteIncompatible, pImg->m_Width, pImg->m_Height, pImgBuffer, ImageFormat)) - { - if((ImageFormat == IMAGE_FORMAT_RGBA || ImageFormat == IMAGE_FORMAT_RGB) && pImg->m_Width <= (2 << 13) && pImg->m_Height <= (2 << 13)) - { - pImg->m_pData = pImgBuffer; - - if(ImageFormat == IMAGE_FORMAT_RGB) - pImg->m_Format = CImageInfo::FORMAT_RGB; - else if(ImageFormat == IMAGE_FORMAT_RGBA) - pImg->m_Format = CImageInfo::FORMAT_RGBA; - else - { - free(pImgBuffer); - return false; - } - } - } - else - return false; - } - else - return false; - return true; -} - void *ReplaceImageItem(int Index, CMapItemImage *pImgItem, const char *pImgName, const char *pImgFile, CMapItemImage *pNewImgItem) { const char *pName = g_DataReader.GetDataString(pImgItem->m_ImageName); @@ -87,13 +40,15 @@ void *ReplaceImageItem(int Index, CMapItemImage *pImgItem, const char *pImgName, dbg_msg("map_replace_image", "found image '%s'", pImgName); CImageInfo ImgInfo; - if(!LoadPng(&ImgInfo, pImgFile)) - return 0; + int PngliteIncompatible; + if(!CImageLoader::LoadPng(io_open(pImgName, IOFLAG_READ), pImgName, ImgInfo, PngliteIncompatible)) + return nullptr; if(ImgInfo.m_Format != CImageInfo::FORMAT_RGBA) { - dbg_msg("map_replace_image", "image '%s' is not in RGBA format", pImgName); - return 0; + dbg_msg("map_replace_image", "ERROR: only RGBA PNG images are supported"); + ImgInfo.Free(); + return nullptr; } *pNewImgItem = *pImgItem;