From 3d746099fa05ae5744ce7b0a2a5269a10eaf3371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Wed, 3 Jul 2024 23:40:50 +0200 Subject: [PATCH] Refactor image loading, saving and manipulation Move `CImageInfo` from `engine/graphics.h` to own file `engine/image.h`. Also add 2-component image format to `CImageInfo::EImageFormat` for completeness, to replace the separate `EImageFormat` in `image_loader.h` with `CImageInfo::EImageFormat`. Move `SetPixelColor`/`GetPixelColor` functions from editor to `CImageInfo` as member functions. Replace `IGraphics::CopyTextureBufferSub` and `IGraphics::CopyTextureFromTextureBufferSub` functions with more versatile `CImageInfo::CopyRectFrom` function. Make `IGraphics::LoadSpriteTexture` function more efficient by avoiding a copy of the image data by using the `LoadTextureRawMove` function. Remove unnecessary delegate function `CGraphics_Threaded::LoadSpriteTextureImpl` and temporary buffer `m_vSpriteHelper`. Move `CEditorImage::DataEquals` function to `CImageInfo::DataEquals`. Use `mem_comp` to compare image data for more efficiency, instead of comparing each pixel individually. Add another `IGraphics::LoadPng` function that loads image directly from memory and also handles the pnglite incompatibility warnings. This function will be used for more efficient loading of downloaded skin in the future. Add convenience functions to load/save PNGs from/to `IOHANDLE` to reduce duplicate code when loading and saving images especially in the tools. These functions explicitly only allow loading images in RGBA and RGB format. Move general purpose image loading and saving functions to class `CImageLoader`. Add more convenient `CByteBufferReader` and `CByteBufferWriter` classes for reading from and writing to a byte buffer while keeping track of the read/write position to replace existing `SImageByteBuffer`. Extract `ConvertToGrayscale` utility function to reduce duplicate code when creating grayscale versions of skins, start menu images and community icons. Move and rename `ConvertToRGBA` static function from graphics to `ConvertToRgba` in `image_manipulation.h/cpp`. Add `ConvertToRgbaAlloc` convenience function which allocates the target buffer. Add `` Add `DilateImage`, `ResizeImage` and `ConvertToRgba` convenience functions that directly accept a `CImageInfo` argument that will be modified. Remove unnecessary image size limitation in `map_replace_image` tool, which would only be relevant for 0.7 compatible maps. Adjust the maximum allowed image width/height in `map_convert_07` tool to be consistent with the actual limit that the 0.7 client has when loading images (`1 << 13 == 8192`). Add doxygen comments for `CImageInfo`. Pass `CImageInfo` by reference consistently, instead of sometimes passing a pointer. Cleanup image loading and saving code. Improve error handling. --- CMakeLists.txt | 2 + src/engine/client/graphics_threaded.cpp | 227 +++-------- src/engine/client/graphics_threaded.h | 7 +- src/engine/gfx/image.cpp | 120 ++++++ src/engine/gfx/image_loader.cpp | 376 +++++++++++-------- src/engine/gfx/image_loader.h | 65 ++-- src/engine/gfx/image_manipulation.cpp | 96 +++++ src/engine/gfx/image_manipulation.h | 17 +- src/engine/graphics.h | 70 +--- src/engine/image.h | 137 +++++++ src/game/client/components/mapimages.cpp | 2 +- src/game/client/components/menus.cpp | 12 +- src/game/client/components/menus_browser.cpp | 12 +- src/game/client/components/skins.cpp | 10 +- src/game/client/components/skins7.cpp | 10 +- src/game/editor/editor.cpp | 69 +--- src/game/editor/mapitems/image.cpp | 26 -- src/game/editor/mapitems/image.h | 1 - src/game/editor/mapitems/map_io.cpp | 10 +- src/game/editor/tileart.cpp | 55 +-- src/tools/dilate.cpp | 89 ++--- src/tools/map_convert_07.cpp | 63 +--- src/tools/map_create_pixelart.cpp | 57 +-- src/tools/map_extract.cpp | 26 +- src/tools/map_replace_image.cpp | 63 +--- 25 files changed, 779 insertions(+), 843 deletions(-) create mode 100644 src/engine/gfx/image.cpp create mode 100644 src/engine/image.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d1ebde9c..899ad43c6 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 21e5687fe..8dc82c34a 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; - } - 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, 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 + if(!First) { - free(pImgBuffer); - log_error("game/png", "image had unsupported image format. filename='%s' format='%d'", pFilename, (int)ImageFormat); - return false; + str_append(Warning.m_aWarningMsg, ", "); } - Image.m_pData = pImgBuffer; - - 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 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; + str_append(Warning.m_aWarningMsg, EXPLANATION[i]); + First = false; } } - else - { - log_error("game/png", "failed to open file. filename='%s'", pFilename); + str_append(Warning.m_aWarningMsg, " unsupported"); + return Warning; +} + +bool CGraphics_Threaded::LoadPng(CImageInfo &Image, const char *pFilename, int StorageType) +{ + IOHANDLE File = m_pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType); + + int PngliteIncompatible; + if(!CImageLoader::LoadPng(File, pFilename, Image, PngliteIncompatible)) return false; + + if(m_WarnPngliteIncompatibleImages && PngliteIncompatible != 0) + { + m_vWarnings.emplace_back(FormatPngliteIncompatibilityWarning(PngliteIncompatible, pFilename)); + } + + return true; +} + +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; + + if(m_WarnPngliteIncompatibleImages && PngliteIncompatible != 0) + { + 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 dd1d4ce65..01774d58f 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 @@ public: 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 @@ public: // 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 000000000..78475aae0 --- /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 e65b6a3eb..be80f82df 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); -} - -static void LibPngWarning(png_structp png_ptr, png_const_charp warning_msg) -{ - SLibPngWarningItem *pUserStruct = (SLibPngWarningItem *)png_get_error_ptr(png_ptr); - dbg_msg("png", "warning for file \"%s\": %s", pUserStruct->m_pFileName, warning_msg); -} - -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; -} - -static void ReadDataFromLoadedBytes(png_structp pPngStruct, png_bytep pOutBytes, png_size_t ByteCountToRead) -{ - png_voidp pIO_Ptr = png_get_io_ptr(pPngStruct); - - SImageByteBuffer *pByteLoader = (SImageByteBuffer *)pIO_Ptr; - - if(pByteLoader->m_pvLoadedImageBytes->size() >= pByteLoader->m_LoadOffset + (size_t)ByteCountToRead) + if(m_ReadOffset + Size <= m_Size) { - mem_copy(pOutBytes, &(*pByteLoader->m_pvLoadedImageBytes)[pByteLoader->m_LoadOffset], (size_t)ByteCountToRead); - - pByteLoader->m_LoadOffset += (size_t)ByteCountToRead; + mem_copy(pData, &m_pData[m_ReadOffset], Size); + m_ReadOffset += Size; + return true; } else { - pByteLoader->m_Err = -1; - dbg_msg("png", "could not read bytes, file was too small."); + m_Error = true; + return false; } } -static EImageFormat LibPngGetImageFormat(int ColorChannelCount) +void CByteBufferWriter::Write(const void *pData, size_t Size) +{ + if(!Size) + return; + + const size_t WriteOffset = m_vBuffer.size(); + m_vBuffer.resize(WriteOffset + Size); + mem_copy(&m_vBuffer[WriteOffset], pData, Size); +} + +class CUserErrorStruct +{ +public: + CByteBufferReader *m_pReader; + const char *m_pContextName; + std::jmp_buf m_JmpBuf; +}; + +[[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); +} + +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); +} + +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)) + { + png_error(pPngStruct, "Could not read all bytes, file was too small"); + } +} + +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(Width == 0 || Height == 0) + { + log_error("png", "image has width (%d) or height (%d) of 0.", Width, Height); + Cleanup(); + return false; + } if(BitDepth == 16) { png_set_strip_16(pPngStruct); } - else if(BitDepth > 8) + else if(BitDepth > 8 || BitDepth == 0) { - dbg_msg("png", "non supported bit depth."); - LibPngDeleteReadStruct(pPngStruct, pPngInfo); - return false; - } - - if(Width == 0 || Height == 0 || 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); + void *pFileData; + unsigned FileDataSize; + io_read_all(File, &pFileData, &FileDataSize); + io_close(File); - png_destroy_info_struct(pPngStruct, &pPngInfo); - png_destroy_read_struct(&pPngStruct, nullptr, nullptr); + CByteBufferReader ImageReader(static_cast(pFileData), FileDataSize); + + const bool LoadResult = CImageLoader::LoadPng(ImageReader, pFilename, Image, PngliteIncompatible); + free(pFileData); + if(!LoadResult) + { + log_error("png", "failed to load image from file. filename='%s'", pFilename); + return false; + } + + 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; + } return true; } -static void WriteDataFromLoadedBytes(png_structp pPngStruct, png_bytep pOutBytes, png_size_t ByteCountToWrite) +static void PngWriteDataCallback(png_structp pPngStruct, png_bytep pOutBytes, png_size_t ByteCountToWrite) { - if(ByteCountToWrite > 0) - { - png_voidp pIO_Ptr = png_get_io_ptr(pPngStruct); - - SImageByteBuffer *pByteLoader = (SImageByteBuffer *)pIO_Ptr; - - size_t NewSize = pByteLoader->m_LoadOffset + (size_t)ByteCountToWrite; - pByteLoader->m_pvLoadedImageBytes->resize(NewSize); - - mem_copy(&(*pByteLoader->m_pvLoadedImageBytes)[pByteLoader->m_LoadOffset], pOutBytes, (size_t)ByteCountToWrite); - pByteLoader->m_LoadOffset = NewSize; - } + 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 9c54d94cf..c2658a6e3 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; -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); +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, + }; + + 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 02c78149d..115a319ad 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 c427f2ec3..c005af7c1 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 2528c0d79..3699c3ea7 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 @@ public: 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 000000000..6b794952a --- /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 10e703f3d..4a34c5f25 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 483db0251..e9be47778 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 2e8748706..993cc038c 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 e774ad94d..221a88059 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 e4179168e..432a6e64d 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 90f36db0b..fb0e3b780 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) + if(CImageLoader::SavePng(pEditor->Storage()->OpenFile(pFileName, IOFLAG_WRITE, StorageType), pFileName, *pImg)) { - 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)) - { - 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 1c55ae7e5..0266ccb99 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 52731f7ff..7e2142f63 100644 --- a/src/game/editor/mapitems/image.h +++ b/src/game/editor/mapitems/image.h @@ -14,7 +14,6 @@ public: 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 a834a3d99..8322289fd 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 ec79d71bd..102a75cc2 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 3ee2ff6c0..d4246a26b 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) + CImageInfo Image; + int PngliteIncompatible; + if(!CImageLoader::LoadPng(io_open(pFilename, IOFLAG_READ), pFilename, Image, PngliteIncompatible)) + return false; + + if(Image.m_Format != CImageInfo::FORMAT_RGBA) { - 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); - - free(Img.m_pData); - } - } - else - { - dbg_msg("dilate", "failed unknown image format: %s", pFilename); - return -1; - } - } - else - { - 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 135bd7f0b..b64d051dc 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 %" PRIzu " are supported", MaxImageDimension); + ImgInfo.Free(); return pImgItem; } diff --git a/src/tools/map_create_pixelart.cpp b/src/tools/map_create_pixelart.cpp index b2486b176..3d3ae5edc 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(4, 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 17b687921..d38cc6b3a 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); + 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)); - 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 + 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 07666020f..2c2ba68a8 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;