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.
This commit is contained in:
Robert Müller 2024-07-03 23:40:50 +02:00
parent 5212d1d7ce
commit 3d746099fa
25 changed files with 779 additions and 843 deletions

View file

@ -2033,6 +2033,7 @@ set_src(ENGINE_INTERFACE GLOB src/engine
ghost.h ghost.h
graphics.h graphics.h
http.h http.h
image.h
input.h input.h
kernel.h kernel.h
keys.h keys.h
@ -2139,6 +2140,7 @@ set_src(ENGINE_SHARED GLOB_RECURSE src/engine/shared
websockets.h websockets.h
) )
set_src(ENGINE_GFX GLOB src/engine/gfx set_src(ENGINE_GFX GLOB src/engine/gfx
image.cpp
image_loader.cpp image_loader.cpp
image_loader.h image_loader.h
image_manipulation.cpp image_manipulation.cpp

View file

@ -300,54 +300,6 @@ void CGraphics_Threaded::UnloadTexture(CTextureHandle *pIndex)
FreeTextureIndex(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) IGraphics::CTextureHandle CGraphics_Threaded::LoadSpriteTexture(const CImageInfo &FromImageInfo, const CDataSprite *pSprite)
{ {
int ImageGridX = FromImageInfo.m_Width / pSprite->m_pSet->m_Gridx; 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 y = pSprite->m_Y * ImageGridY;
int w = pSprite->m_W * ImageGridX; int w = pSprite->m_W * ImageGridX;
int h = pSprite->m_H * ImageGridY; 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<uint8_t *>(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) 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 uint8_t *pImgData = FromImageInfo.m_pData;
const size_t PixelSize = FromImageInfo.PixelSize(); 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); CCommandBuffer::SCommand_Texture_Create Cmd = LoadTextureCreateCommand(TextureHandle.Id(), Image.m_Width, Image.m_Height, Flags);
// Copy texture data and convert if necessary // Copy texture data and convert if necessary
uint8_t *pTmpData = static_cast<uint8_t *>(malloc(Image.m_Width * Image.m_Height * CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA))); uint8_t *pTmpData;
if(!ConvertToRGBA(pTmpData, Image)) if(!ConvertToRgbaAlloc(pTmpData, Image))
{ {
dbg_msg("graphics", "converted image '%s' to RGBA, consider making its file format RGBA", pTexName ? pTexName : "(no name)"); 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; return TextureHandle;
} }
// simple uncompressed RGBA loaders
IGraphics::CTextureHandle CGraphics_Threaded::LoadTexture(const char *pFilename, int StorageType, int Flags) 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 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; 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]; SWarning Warning;
IOHANDLE File = m_pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType, aCompleteFilename, sizeof(aCompleteFilename)); 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);
if(File) 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); if((PngliteIncompatible & FLAGS[i]) != 0)
long int FileSize = io_tell(File);
if(FileSize <= 0)
{ {
io_close(File); if(!First)
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
{ {
free(pImgBuffer); str_append(Warning.m_aWarningMsg, ", ");
log_error("game/png", "image had unsupported image format. filename='%s' format='%d'", pFilename, (int)ImageFormat);
return false;
} }
Image.m_pData = pImgBuffer; str_append(Warning.m_aWarningMsg, EXPLANATION[i]);
First = false;
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;
} }
} }
else str_append(Warning.m_aWarningMsg, " unsupported");
{ return Warning;
log_error("game/png", "failed to open file. filename='%s'", pFilename); }
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; 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; return true;
@ -655,12 +588,7 @@ bool CGraphics_Threaded::CheckImageDivisibility(const char *pContextName, CImage
NewHeight = maximum<int>(HighestBit(Image.m_Height), DivY); NewHeight = maximum<int>(HighestBit(Image.m_Height), DivY);
NewWidth = (NewHeight / DivY) * DivX; NewWidth = (NewHeight / DivY) * DivX;
} }
ResizeImage(Image, NewWidth, NewHeight);
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;
ImageIsValid = true; ImageIsValid = true;
} }
@ -682,29 +610,6 @@ bool CGraphics_Threaded::IsImageFormatRgba(const char *pContextName, const CImag
return true; 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() void CGraphics_Threaded::KickCommandBuffer()
{ {
m_pBackend->RunBuffer(m_pCommandBuffer); m_pBackend->RunBuffer(m_pCommandBuffer);
@ -731,24 +636,14 @@ class CScreenshotSaveJob : public IJob
IStorage *m_pStorage; IStorage *m_pStorage;
IConsole *m_pConsole; IConsole *m_pConsole;
char m_aName[IO_MAX_PATH_LENGTH]; char m_aName[IO_MAX_PATH_LENGTH];
int m_Width; CImageInfo m_Image;
int m_Height;
uint8_t *m_pData;
void Run() override void Run() override
{ {
char aWholePath[IO_MAX_PATH_LENGTH]; char aWholePath[IO_MAX_PATH_LENGTH];
char aBuf[64 + 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(CImageLoader::SavePng(m_pStorage->OpenFile(m_aName, IOFLAG_WRITE, IStorage::TYPE_SAVE, aWholePath, sizeof(aWholePath)), m_aName, m_Image))
if(File)
{ {
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); str_format(aBuf, sizeof(aBuf), "saved screenshot to '%s'", aWholePath);
} }
else else
@ -759,19 +654,17 @@ class CScreenshotSaveJob : public IJob
} }
public: 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_pStorage(pStorage),
m_pConsole(pConsole), m_pConsole(pConsole),
m_Width(Width), m_Image(Image)
m_Height(Height),
m_pData(pData)
{ {
str_copy(m_aName, pName); str_copy(m_aName, pName);
} }
~CScreenshotSaveJob() override ~CScreenshotSaveJob() override
{ {
free(m_pData); m_Image.Free();
} }
}; };
@ -795,7 +688,7 @@ void CGraphics_Threaded::ScreenshotDirect(bool *pSwapped)
if(Image.m_pData) if(Image.m_pData)
{ {
m_pEngine->AddJob(std::make_shared<CScreenshotSaveJob>(m_pStorage, m_pConsole, m_aScreenshotName, Image.m_Width, Image.m_Height, Image.m_pData)); m_pEngine->AddJob(std::make_shared<CScreenshotSaveJob>(m_pStorage, m_pConsole, m_aScreenshotName, Image));
} }
} }

View file

@ -798,8 +798,6 @@ class CGraphics_Threaded : public IEngineGraphics
size_t m_FirstFreeTexture; size_t m_FirstFreeTexture;
int m_TextureMemoryUsage; int m_TextureMemoryUsage;
std::vector<uint8_t> m_vSpriteHelper;
bool m_WarnPngliteIncompatibleImages = false; bool m_WarnPngliteIncompatibleImages = false;
std::vector<SWarning> m_vWarnings; std::vector<SWarning> m_vWarnings;
@ -954,7 +952,6 @@ public:
bool UnloadTextTextures(CTextureHandle &TextTexture, CTextureHandle &TextOutlineTexture) override; 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; 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; CTextureHandle LoadSpriteTexture(const CImageInfo &FromImageInfo, const struct CDataSprite *pSprite) override;
bool IsImageSubFullyTransparent(const CImageInfo &FromImageInfo, int x, int y, int w, int h) override; bool IsImageSubFullyTransparent(const CImageInfo &FromImageInfo, int x, int y, int w, int h) override;
@ -963,13 +960,11 @@ public:
// simple uncompressed RGBA loaders // simple uncompressed RGBA loaders
IGraphics::CTextureHandle LoadTexture(const char *pFilename, int StorageType, int Flags = 0) override; 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 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 CheckImageDivisibility(const char *pContextName, CImageInfo &Image, int DivX, int DivY, bool AllowResize) override;
bool IsImageFormatRgba(const char *pContextName, const CImageInfo &Image) 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 TextureSet(CTextureHandle TextureId) override;
void Clear(float r, float g, float b, bool ForceClearNow = false) override; void Clear(float r, float g, float b, bool ForceClearNow = false) override;

120
src/engine/gfx/image.cpp Normal file
View file

@ -0,0 +1,120 @@
#include <base/system.h>
#include <engine/image.h>
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);
}
}

View file

@ -1,89 +1,94 @@
#include "image_loader.h" #include "image_loader.h"
#include <base/log.h> #include <base/log.h>
#include <base/system.h> #include <base/system.h>
#include <csetjmp> #include <csetjmp>
#include <cstdlib> #include <cstdlib>
#include <png.h> #include <png.h>
struct SLibPngWarningItem bool CByteBufferReader::Read(void *pData, size_t Size)
{ {
SImageByteBuffer *m_pByteLoader; if(m_Error)
const char *m_pFileName; return false;
std::jmp_buf m_Buf;
};
[[noreturn]] static void LibPngError(png_structp png_ptr, png_const_charp error_msg) if(m_ReadOffset + Size <= m_Size)
{
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)
{ {
mem_copy(pOutBytes, &(*pByteLoader->m_pvLoadedImageBytes)[pByteLoader->m_LoadOffset], (size_t)ByteCountToRead); mem_copy(pData, &m_pData[m_ReadOffset], Size);
m_ReadOffset += Size;
pByteLoader->m_LoadOffset += (size_t)ByteCountToRead; return true;
} }
else else
{ {
pByteLoader->m_Err = -1; m_Error = true;
dbg_msg("png", "could not read bytes, file was too small."); 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<CUserErrorStruct *>(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<CUserErrorStruct *>(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<CByteBufferReader *>(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) switch(ColorChannelCount)
{ {
case 1: case 1:
return IMAGE_FORMAT_R; return CImageInfo::FORMAT_R;
case 2: case 2:
return IMAGE_FORMAT_RA; return CImageInfo::FORMAT_RA;
case 3: case 3:
return IMAGE_FORMAT_RGB; return CImageInfo::FORMAT_RGB;
case 4: case 4:
return IMAGE_FORMAT_RGBA; return CImageInfo::FORMAT_RGBA;
default: default:
dbg_assert(false, "ColorChannelCount invalid"); dbg_assert(false, "ColorChannelCount invalid");
dbg_break(); 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) 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; int Result = 0;
const int ColorType = png_get_color_type(pPngStruct, pPngInfo);
switch(ColorType) switch(ColorType)
{ {
case PNG_COLOR_TYPE_GRAY: case PNG_COLOR_TYPE_GRAY:
@ -93,9 +98,10 @@ static int PngliteIncompatibility(png_structp pPngStruct, png_infop pPngInfo)
break; break;
default: default:
log_debug("png", "color type %d unsupported by pnglite", ColorType); 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) switch(BitDepth)
{ {
case 8: case 8:
@ -103,258 +109,294 @@ static int PngliteIncompatibility(png_structp pPngStruct, png_infop pPngInfo)
break; break;
default: default:
log_debug("png", "bit depth %d unsupported by pnglite", BitDepth); 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) if(InterlaceType != PNG_INTERLACE_NONE)
{ {
log_debug("png", "interlace type %d unsupported by pnglite", InterlaceType); 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) if(png_get_compression_type(pPngStruct, pPngInfo) != PNG_COMPRESSION_TYPE_BASE)
{ {
log_debug("png", "non-default compression type unsupported by pnglite"); 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) if(png_get_filter_type(pPngStruct, pPngInfo) != PNG_FILTER_TYPE_BASE)
{ {
log_debug("png", "non-default filter type unsupported by pnglite"); log_debug("png", "non-default filter type unsupported by pnglite");
Result |= PNGLITE_FILTER_TYPE; Result |= CImageLoader::PNGLITE_FILTER_TYPE;
} }
return Result; 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); png_structp pPngStruct = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if(pPngStruct == 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; return false;
} }
png_infop pPngInfo = nullptr; png_infop pPngInfo = nullptr;
png_bytepp pRowPointers = nullptr; png_bytepp pRowPointers = nullptr;
Height = 0; // ensure this is not undefined for the error handler int Height = 0; // ensure this is not undefined for the Cleanup function
if(setjmp(UserErrorStruct.m_Buf)) const auto &&Cleanup = [&]() {
{
if(pRowPointers != nullptr) 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; 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; return false;
} }
png_set_error_fn(pPngStruct, &UserErrorStruct, LibPngError, LibPngWarning); png_set_error_fn(pPngStruct, &UserErrorStruct, PngErrorCallback, PngWarningCallback);
pPngInfo = png_create_info_struct(pPngStruct); pPngInfo = png_create_info_struct(pPngStruct);
if(pPngInfo == nullptr) if(pPngInfo == nullptr)
{ {
png_destroy_read_struct(&pPngStruct, nullptr, nullptr); Cleanup();
dbg_msg("png", "libpng internal failure: png_create_info_struct failed."); log_error("png", "libpng internal failure: png_create_info_struct failed.");
return false; 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); Cleanup();
dbg_msg("png", "file does not match image type."); log_error("png", "file is not a valid PNG file (signature mismatch).");
return false; return false;
} }
ByteLoader.m_LoadOffset = 8; png_set_read_fn(pPngStruct, (png_bytep)&Reader, PngReadDataCallback);
png_set_sig_bytes(pPngStruct, sizeof(aSignature));
png_set_read_fn(pPngStruct, (png_bytep)&ByteLoader, ReadDataFromLoadedBytes);
png_set_sig_bytes(pPngStruct, 8);
png_read_info(pPngStruct, pPngInfo); png_read_info(pPngStruct, pPngInfo);
if(ByteLoader.m_Err != 0) if(Reader.Error())
{ {
LibPngDeleteReadStruct(pPngStruct, pPngInfo); // error already logged
dbg_msg("png", "byte loader error."); Cleanup();
return false; 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); 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); 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) if(BitDepth == 16)
{ {
png_set_strip_16(pPngStruct); png_set_strip_16(pPngStruct);
} }
else if(BitDepth > 8) else if(BitDepth > 8 || BitDepth == 0)
{ {
dbg_msg("png", "non supported bit depth."); log_error("png", "bit depth %d not supported.", BitDepth);
LibPngDeleteReadStruct(pPngStruct, pPngInfo); Cleanup();
return false;
}
if(Width == 0 || Height == 0 || BitDepth == 0)
{
dbg_msg("png", "image had width, height or bit depth of 0.");
LibPngDeleteReadStruct(pPngStruct, pPngInfo);
return false; return false;
} }
if(ColorType == PNG_COLOR_TYPE_PALETTE) if(ColorType == PNG_COLOR_TYPE_PALETTE)
{
png_set_palette_to_rgb(pPngStruct); png_set_palette_to_rgb(pPngStruct);
}
if(ColorType == PNG_COLOR_TYPE_GRAY && BitDepth < 8) if(ColorType == PNG_COLOR_TYPE_GRAY && BitDepth < 8)
{
png_set_expand_gray_1_2_4_to_8(pPngStruct); png_set_expand_gray_1_2_4_to_8(pPngStruct);
}
if(png_get_valid(pPngStruct, pPngInfo, PNG_INFO_tRNS)) if(png_get_valid(pPngStruct, pPngInfo, PNG_INFO_tRNS))
{
png_set_tRNS_to_alpha(pPngStruct); png_set_tRNS_to_alpha(pPngStruct);
}
png_read_update_info(pPngStruct, pPngInfo); png_read_update_info(pPngStruct, pPngInfo);
const size_t ColorChannelCount = png_get_channels(pPngStruct, pPngInfo); const int ColorChannelCount = png_get_channels(pPngStruct, pPngInfo);
const size_t BytesInRow = png_get_rowbytes(pPngStruct, pPngInfo); const int BytesInRow = png_get_rowbytes(pPngStruct, pPngInfo);
dbg_assert(BytesInRow == Width * ColorChannelCount, "bytes in row incorrect."); dbg_assert(BytesInRow == Width * ColorChannelCount, "bytes in row incorrect.");
pRowPointers = new png_bytep[Height]; 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]; pRowPointers[y] = new png_byte[BytesInRow];
} }
png_read_image(pPngStruct, pRowPointers); png_read_image(pPngStruct, pRowPointers);
if(ByteLoader.m_Err == 0) if(!Reader.Error())
pImageBuff = (uint8_t *)malloc(Height * Width * ColorChannelCount * sizeof(uint8_t));
for(size_t i = 0; i < Height; ++i)
{ {
if(ByteLoader.m_Err == 0) Image.m_Width = Width;
mem_copy(&pImageBuff[i * BytesInRow], pRowPointers[i], BytesInRow); Image.m_Height = Height;
delete[] pRowPointers[i]; Image.m_Format = ImageFormatFromChannelCount(ColorChannelCount);
Image.m_pData = static_cast<uint8_t *>(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); log_error("png", "failed to open file for reading. filename='%s'", pFilename);
dbg_msg("png", "byte loader error.");
return false; return false;
} }
ImageFormat = LibPngGetImageFormat(ColorChannelCount); void *pFileData;
unsigned FileDataSize;
io_read_all(File, &pFileData, &FileDataSize);
io_close(File);
png_destroy_info_struct(pPngStruct, &pPngInfo); CByteBufferReader ImageReader(static_cast<const uint8_t *>(pFileData), FileDataSize);
png_destroy_read_struct(&pPngStruct, nullptr, nullptr);
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; 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) CByteBufferWriter *pWriter = static_cast<CByteBufferWriter *>(png_get_io_ptr(pPngStruct));
{ pWriter->Write(pOutBytes, ByteCountToWrite);
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;
}
} }
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) switch(Format)
{ {
case IMAGE_FORMAT_R: case CImageInfo::FORMAT_R:
return 1; return PNG_COLOR_TYPE_GRAY;
case IMAGE_FORMAT_RA: case CImageInfo::FORMAT_RA:
return 2; return PNG_COLOR_TYPE_GRAY_ALPHA;
case IMAGE_FORMAT_RGB: case CImageInfo::FORMAT_RGB:
return 3; return PNG_COLOR_TYPE_RGB;
case IMAGE_FORMAT_RGBA: case CImageInfo::FORMAT_RGBA:
return 4; return PNG_COLOR_TYPE_RGBA;
default: default:
dbg_assert(false, "Format invalid"); dbg_assert(false, "Format invalid");
dbg_break(); 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); png_structp pPngStruct = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if(pPngStruct == 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; return false;
} }
png_infop pPngInfo = png_create_info_struct(pPngStruct); png_infop pPngInfo = png_create_info_struct(pPngStruct);
if(pPngInfo == nullptr) if(pPngInfo == nullptr)
{ {
png_destroy_read_struct(&pPngStruct, nullptr, 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; return false;
} }
WrittenBytes.m_LoadOffset = 0; png_set_write_fn(pPngStruct, (png_bytep)&Writer, PngWriteDataCallback, PngOutputFlushCallback);
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_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_write_info(pPngStruct, pPngInfo);
png_bytepp pRowPointers = new png_bytep[Height]; png_bytepp pRowPointers = new png_bytep[Image.m_Height];
size_t WidthBytes = Width * WriteBytesPerPixel; const int WidthBytes = Image.m_Width * Image.PixelSize();
ptrdiff_t BufferOffset = 0; 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]; 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; BufferOffset += (ptrdiff_t)WidthBytes;
} }
png_write_image(pPngStruct, pRowPointers); png_write_image(pPngStruct, pRowPointers);
png_write_end(pPngStruct, pPngInfo); 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_info_struct(pPngStruct, &pPngInfo);
png_destroy_write_struct(&pPngStruct, nullptr); png_destroy_write_struct(&pPngStruct, nullptr);
return true; 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;
}

View file

@ -1,38 +1,57 @@
#ifndef ENGINE_GFX_IMAGE_LOADER_H #ifndef ENGINE_GFX_IMAGE_LOADER_H
#define ENGINE_GFX_IMAGE_LOADER_H #define ENGINE_GFX_IMAGE_LOADER_H
#include <cstddef> #include <base/types.h>
#include <cstdint>
#include <engine/image.h>
#include <vector> #include <vector>
enum EImageFormat class CByteBufferReader
{ {
IMAGE_FORMAT_R = 0, const uint8_t *m_pData;
IMAGE_FORMAT_RA, size_t m_Size;
IMAGE_FORMAT_RGB, size_t m_ReadOffset = 0;
IMAGE_FORMAT_RGBA, 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<uint8_t> TImageByteBuffer; class CByteBufferWriter
struct SImageByteBuffer
{ {
SImageByteBuffer(std::vector<uint8_t> *pvBuff) : std::vector<uint8_t> m_vBuffer;
m_LoadOffset(0), m_pvLoadedImageBytes(pvBuff), m_Err(0) {}
size_t m_LoadOffset; public:
std::vector<uint8_t> *m_pvLoadedImageBytes; void Write(const void *pData, size_t Size);
int m_Err; 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, CImageLoader() = delete;
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); public:
bool SavePng(EImageFormat ImageFormat, const uint8_t *pRawBuffer, SImageByteBuffer &WrittenBytes, size_t Width, size_t Height); 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 #endif // ENGINE_GFX_IMAGE_LOADER_H

View file

@ -1,7 +1,88 @@
#include "image_manipulation.h" #include "image_manipulation.h"
#include <base/math.h> #include <base/math.h>
#include <base/system.h> #include <base/system.h>
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<uint8_t *>(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 int DILATE_BPP = 4; // RGBA assumed
static constexpr uint8_t DILATE_ALPHA_THRESHOLD = 10; 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); 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) void DilateImageSub(uint8_t *pImageBuff, int w, int h, int x, int y, int sw, int sh)
{ {
uint8_t *apBuffer[2] = {nullptr, nullptr}; 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; 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) int HighestBit(int OfVar)
{ {
if(!OfVar) if(!OfVar)

View file

@ -1,14 +1,29 @@
#ifndef ENGINE_GFX_IMAGE_MANIPULATION_H #ifndef ENGINE_GFX_IMAGE_MANIPULATION_H
#define ENGINE_GFX_IMAGE_MANIPULATION_H #define ENGINE_GFX_IMAGE_MANIPULATION_H
#include <engine/image.h>
#include <cstdint> #include <cstdint>
// 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 // These functions assume that the image data is 4 bytes per pixel RGBA
void DilateImage(uint8_t *pImageBuff, int w, int h); 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); 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); 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); int HighestBit(int OfVar);

View file

@ -3,6 +3,7 @@
#ifndef ENGINE_GRAPHICS_H #ifndef ENGINE_GRAPHICS_H
#define ENGINE_GRAPHICS_H #define ENGINE_GRAPHICS_H
#include "image.h"
#include "kernel.h" #include "kernel.h"
#include "warning.h" #include "warning.h"
@ -64,68 +65,6 @@ struct SGraphicTileTexureCoords
ubvec4 m_TexCoordBottomLeft; 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 Structure: CVideoMode
*/ */
@ -330,16 +269,11 @@ public:
virtual const TTwGraphicsGpuList &GetGpus() const = 0; virtual const TTwGraphicsGpuList &GetGpus() const = 0;
virtual bool LoadPng(CImageInfo &Image, const char *pFilename, int StorageType) = 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 CheckImageDivisibility(const char *pContextName, CImageInfo &Image, int DivX, int DivY, bool AllowResize) = 0;
virtual bool IsImageFormatRgba(const char *pContextName, const CImageInfo &Image) = 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 void UnloadTexture(CTextureHandle *pIndex) = 0;
virtual CTextureHandle LoadTextureRaw(const CImageInfo &Image, int Flags, const char *pTexName = nullptr) = 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; virtual CTextureHandle LoadTextureRawMove(CImageInfo &Image, int Flags, const char *pTexName = nullptr) = 0;

137
src/engine/image.h Normal file
View file

@ -0,0 +1,137 @@
#ifndef ENGINE_IMAGE_H
#define ENGINE_IMAGE_H
#include <cstdint>
#include <base/color.h>
/**
* 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

View file

@ -301,7 +301,7 @@ IGraphics::CTextureHandle CMapImages::GetEntities(EMapImageEntityLayerType Entit
const size_t CopyHeight = ImgInfo.m_Height / 16; const size_t CopyHeight = ImgInfo.m_Height / 16;
const size_t OffsetX = (size_t)(TileIndex % 16) * CopyWidth; const size_t OffsetX = (size_t)(TileIndex % 16) * CopyWidth;
const size_t OffsetY = (size_t)(TileIndex / 16) * CopyHeight; 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);
} }
} }

View file

@ -14,6 +14,7 @@
#include <engine/config.h> #include <engine/config.h>
#include <engine/editor.h> #include <engine/editor.h>
#include <engine/friends.h> #include <engine/friends.h>
#include <engine/gfx/image_manipulation.h>
#include <engine/graphics.h> #include <engine/graphics.h>
#include <engine/keys.h> #include <engine/keys.h>
#include <engine/serverbrowser.h> #include <engine/serverbrowser.h>
@ -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); MenuImage.m_OrgTexture = pSelf->Graphics()->LoadTextureRaw(Info, 0, aPath);
// create gray scale version ConvertToGrayscale(Info);
unsigned char *pData = static_cast<unsigned char *>(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;
}
MenuImage.m_GreyTexture = pSelf->Graphics()->LoadTextureRawMove(Info, 0, aPath); 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)); str_truncate(MenuImage.m_aName, sizeof(MenuImage.m_aName), pName, str_length(pName) - str_length(pExtension));

View file

@ -5,6 +5,7 @@
#include <engine/engine.h> #include <engine/engine.h>
#include <engine/favorites.h> #include <engine/favorites.h>
#include <engine/friends.h> #include <engine/friends.h>
#include <engine/gfx/image_manipulation.h>
#include <engine/keys.h> #include <engine/keys.h>
#include <engine/serverbrowser.h> #include <engine/serverbrowser.h>
#include <engine/shared/config.h> #include <engine/shared/config.h>
@ -1992,16 +1993,7 @@ void CMenus::LoadCommunityIconFinish(const char *pCommunityId, CImageInfo &Info,
CommunityIcon.m_Sha256 = Sha256; CommunityIcon.m_Sha256 = Sha256;
CommunityIcon.m_OrgTexture = Graphics()->LoadTextureRaw(Info, 0, pCommunityId); CommunityIcon.m_OrgTexture = Graphics()->LoadTextureRaw(Info, 0, pCommunityId);
// create gray scale version ConvertToGrayscale(Info);
unsigned char *pData = static_cast<unsigned char *>(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;
}
CommunityIcon.m_GreyTexture = Graphics()->LoadTextureRawMove(Info, 0, pCommunityId); CommunityIcon.m_GreyTexture = Graphics()->LoadTextureRawMove(Info, 0, pCommunityId);
auto ExistingIcon = std::find_if(m_vCommunityIcons.begin(), m_vCommunityIcons.end(), [pCommunityId](const SCommunityIcon &Element) { auto ExistingIcon = std::find_if(m_vCommunityIcons.begin(), m_vCommunityIcons.end(), [pCommunityId](const SCommunityIcon &Element) {

View file

@ -6,6 +6,7 @@
#include <base/system.h> #include <base/system.h>
#include <engine/engine.h> #include <engine/engine.h>
#include <engine/gfx/image_manipulation.h>
#include <engine/graphics.h> #include <engine/graphics.h>
#include <engine/shared/config.h> #include <engine/shared/config.h>
#include <engine/storage.h> #include <engine/storage.h>
@ -237,14 +238,7 @@ const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info)
// get feet outline size // get feet outline size
CheckMetrics(Skin.m_Metrics.m_Feet, pData, Pitch, FeetOutlineOffsetX, FeetOutlineOffsetY, FeetOutlineWidth, FeetOutlineHeight); CheckMetrics(Skin.m_Metrics.m_Feet, pData, Pitch, FeetOutlineOffsetX, FeetOutlineOffsetY, FeetOutlineWidth, FeetOutlineHeight);
// make the texture gray scale ConvertToGrayscale(Info);
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;
}
int aFreq[256] = {0}; int aFreq[256] = {0};
int OrgWeight = 0; int OrgWeight = 0;

View file

@ -6,6 +6,7 @@
#include <base/system.h> #include <base/system.h>
#include <engine/external/json-parser/json.h> #include <engine/external/json-parser/json.h>
#include <engine/gfx/image_manipulation.h>
#include <engine/graphics.h> #include <engine/graphics.h>
#include <engine/shared/config.h> #include <engine/shared/config.h>
#include <engine/shared/jsonwriter.h> #include <engine/shared/jsonwriter.h>
@ -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]))); Part.m_BloodColor = ColorRGBA(normalize(vec3(aColors[0], aColors[1], aColors[2])));
} }
// create colorless version ConvertToGrayscale(Info);
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;
}
Part.m_ColorTexture = pSelf->Graphics()->LoadTextureRawMove(Info, 0, aFilename); Part.m_ColorTexture = pSelf->Graphics()->LoadTextureRawMove(Info, 0, aFilename);

View file

@ -850,41 +850,14 @@ bool CEditor::CallbackSaveImage(const char *pFileName, int StorageType, void *pU
std::shared_ptr<CEditorImage> pImg = pEditor->m_Map.m_vpImages[pEditor->m_SelectedImage]; std::shared_ptr<CEditorImage> pImg = pEditor->m_Map.m_vpImages[pEditor->m_SelectedImage];
EImageFormat OutputFormat; if(CImageLoader::SavePng(pEditor->Storage()->OpenFile(pFileName, IOFLAG_WRITE, StorageType), pFileName, *pImg))
switch(pImg->m_Format)
{ {
case CImageInfo::FORMAT_RGB: pEditor->m_Dialog = DIALOG_NONE;
OutputFormat = IMAGE_FORMAT_RGB; return true;
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;
} }
else else
{ {
pEditor->ShowFileDialogError("Failed to write image to file."); pEditor->ShowFileDialogError("Failed to write image to file '%s'.", pFileName);
return false; return false;
} }
} }
@ -4401,18 +4374,13 @@ bool CEditor::ReplaceImage(const char *pFileName, int StorageType, bool CheckDup
str_copy(pImg->m_aName, aBuf); str_copy(pImg->m_aName, aBuf);
pImg->m_External = IsVanillaImage(pImg->m_aName); 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<uint8_t *>(malloc((size_t)pImg->m_Width * pImg->m_Height * CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA))); ConvertToRgba(*pImg);
ConvertToRGBA(pRgbaData, *pImg); if(g_Config.m_ClEditorDilate == 1)
free(pImg->m_pData); {
pImg->m_pData = pRgbaData; DilateImage(*pImg);
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);
} }
pImg->m_AutoMapper.Load(pImg->m_aName); 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_pData = ImgInfo.m_pData;
pImg->m_External = IsVanillaImage(aBuf); pImg->m_External = IsVanillaImage(aBuf);
if(pImg->m_Format != CImageInfo::FORMAT_RGBA) if(!pImg->m_External)
{ {
uint8_t *pRgbaData = static_cast<uint8_t *>(malloc((size_t)pImg->m_Width * pImg->m_Height * CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA))); ConvertToRgba(*pImg);
ConvertToRGBA(pRgbaData, *pImg); if(g_Config.m_ClEditorDilate == 1)
free(pImg->m_pData); {
pImg->m_pData = pRgbaData; DilateImage(*pImg);
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);
} }
int TextureLoadFlag = pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE; int TextureLoadFlag = pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;

View file

@ -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;
}

View file

@ -14,7 +14,6 @@ public:
void OnInit(CEditor *pEditor) override; void OnInit(CEditor *pEditor) override;
void AnalyseTileFlags(); void AnalyseTileFlags();
bool DataEquals(const CEditorImage &Other) const;
IGraphics::CTextureHandle m_Texture; IGraphics::CTextureHandle m_Texture;
int m_External = 0; int m_External = 0;

View file

@ -2,6 +2,7 @@
#include <engine/client.h> #include <engine/client.h>
#include <engine/console.h> #include <engine/console.h>
#include <engine/gfx/image_manipulation.h>
#include <engine/graphics.h> #include <engine/graphics.h>
#include <engine/serverbrowser.h> #include <engine/serverbrowser.h>
#include <engine/shared/datafile.h> #include <engine/shared/datafile.h>
@ -509,14 +510,7 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio
pImg->m_Height = ImgInfo.m_Height; pImg->m_Height = ImgInfo.m_Height;
pImg->m_Format = ImgInfo.m_Format; pImg->m_Format = ImgInfo.m_Format;
pImg->m_pData = ImgInfo.m_pData; pImg->m_pData = ImgInfo.m_pData;
if(pImg->m_Format != CImageInfo::FORMAT_RGBA) ConvertToRgba(*pImg);
{
uint8_t *pRgbaData = static_cast<uint8_t *>(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;
}
int TextureLoadFlag = m_pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE; 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) if(ImgInfo.m_Width % 16 != 0 || ImgInfo.m_Height % 16 != 0)

View file

@ -17,51 +17,6 @@ bool operator<(const ColorRGBA &Left, const ColorRGBA &Right)
return Left.a < Right.a; 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<ColorRGBA> GetUniqueColors(const CImageInfo &Image) static std::vector<ColorRGBA> GetUniqueColors(const CImageInfo &Image)
{ {
std::set<ColorRGBA> ColorSet; std::set<ColorRGBA> ColorSet;
@ -70,7 +25,7 @@ static std::vector<ColorRGBA> GetUniqueColors(const CImageInfo &Image)
{ {
for(size_t y = 0; y < Image.m_Height; y++) 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) if(Color.a > 0 && ColorSet.insert(Color).second)
vUniqueColors.push_back(Color); vUniqueColors.push_back(Color);
} }
@ -106,12 +61,12 @@ static std::vector<std::array<ColorRGBA, NumTiles>> GroupColors(const std::vecto
return vaColorGroups; 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 i = 0; i < TileSize; i++)
{ {
for(int j = 0; j < TileSize; j++) 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<ColorRGBA, NumTiles> &aColo
for(int x = 0; x < NumTilesRow; x++) for(int x = 0; x < NumTilesRow; x++)
{ {
int ColorIndex = x + NumTilesRow * y; 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<CLayerTiles> &pLayer, cons
for(int x = 0; x < pLayer->m_Width; x++) for(int x = 0; x < pLayer->m_Width; x++)
{ {
for(int y = 0; y < pLayer->m_Height; y++) 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));
} }
} }

View file

@ -1,88 +1,49 @@
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* (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. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */
#include <base/logger.h> #include <base/logger.h>
#include <base/system.h> #include <base/system.h>
#include <engine/gfx/image_loader.h> #include <engine/gfx/image_loader.h>
#include <engine/gfx/image_manipulation.h> #include <engine/gfx/image_manipulation.h>
#include <engine/graphics.h>
int DilateFile(const char *pFilename) static bool DilateFile(const char *pFilename)
{ {
IOHANDLE File = io_open(pFilename, IOFLAG_READ); CImageInfo Image;
if(File) 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); log_error("dilate", "ERROR: only RGBA PNG images are supported");
long int FileSize = io_tell(File); Image.Free();
if(FileSize <= 0) return false;
{
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;
} }
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) int main(int argc, const char **argv)
{ {
CCmdlineFix CmdlineFix(&argc, &argv); CCmdlineFix CmdlineFix(&argc, &argv);
log_set_global_logger_default(); log_set_global_logger_default();
if(argc == 1) if(argc == 1)
{ {
dbg_msg("usage", "%s FILE1 [ FILE2... ]", argv[0]); log_error("dilate", "Usage: %s <image1.png> [<image2.png> ...]", argv[0]);
return -1; return -1;
} }
bool Success = true;
for(int i = 1; i < argc; i++) for(int i = 1; i < argc; i++)
DilateFile(argv[i]); {
Success &= DilateFile(argv[i]);
return 0; }
return Success ? 0 : -1;
} }

View file

@ -3,12 +3,14 @@
#include <base/logger.h> #include <base/logger.h>
#include <base/system.h> #include <base/system.h>
#include <engine/gfx/image_loader.h> #include <engine/gfx/image_loader.h>
#include <engine/graphics.h>
#include <engine/shared/datafile.h> #include <engine/shared/datafile.h>
#include <engine/storage.h> #include <engine/storage.h>
#include <game/gamecore.h> #include <game/gamecore.h>
#include <game/mapitems.h> #include <game/mapitems.h>
/* /*
Usage: map_convert_07 <source map filepath> <dest map filepath> Usage: map_convert_07 <source map filepath> <dest map filepath>
*/ */
@ -25,53 +27,6 @@ int g_NextDataItemId = -1;
int g_aImageIds[MAX_MAPIMAGES]; 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) bool CheckImageDimensions(void *pLayerItem, int LayerType, const char *pFilename)
{ {
if(LayerType != MAPITEMTYPE_LAYER) 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); dbg_msg("map_convert_07", "embedding image '%s'", pName);
CImageInfo ImgInfo;
char aStr[IO_MAX_PATH_LENGTH]; char aStr[IO_MAX_PATH_LENGTH];
str_format(aStr, sizeof(aStr), "data/mapres/%s.png", pName); 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 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; return pImgItem;
} }

View file

@ -1,16 +1,16 @@
#include <base/logger.h> #include <base/logger.h>
#include <base/system.h> #include <base/system.h>
#include <engine/gfx/image_loader.h> #include <engine/gfx/image_loader.h>
#include <engine/graphics.h>
#include <engine/shared/datafile.h> #include <engine/shared/datafile.h>
#include <engine/storage.h> #include <engine/storage.h>
#include <game/mapitems.h> #include <game/mapitems.h>
bool CreatePixelArt(const char[3][IO_MAX_PATH_LENGTH], const int[2], const int[2], int[2], const bool[2]); 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 *); void InsertCurrentQuads(CDataFileReader &, CMapItemLayerQuads *, CQuad *);
int InsertPixelArtQuads(CQuad *, int &, const CImageInfo &, const int[2], const int[2], const bool[2]); 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 &); bool OpenMaps(const char[2][IO_MAX_PATH_LENGTH], CDataFileReader &, CDataFileWriter &);
void SaveOutputMap(CDataFileReader &, CDataFileWriter &, CMapItemLayerQuads *, int, CQuad *, int); 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]) 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; CImageInfo Img;
if(!LoadPng(&Img, aFilenames[2])) int PngliteIncompatible;
if(!CImageLoader::LoadPng(io_open(aFilenames[2], IOFLAG_READ), aFilenames[2], Img, PngliteIncompatible))
return false; return false;
aPixelSizes[0] = aPixelSizes[0] ? aPixelSizes[0] : GetImagePixelSize(Img); 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; aPixel[3] = 255;
const size_t PixelSize = Img.PixelSize(); const size_t PixelSize = Img.PixelSize();
for(size_t i = 0; i < PixelSize; i++) for(size_t i = 0; i < minimum<size_t>(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]; aPixel[i] = Img.m_pData[x * PixelSize + (Img.m_Width * PixelSize * y) + i];
return aPixel[3] > 0; return aPixel[3] > 0;
@ -315,54 +316,6 @@ CQuad CreateNewQuad(const float PosX, const float PosY, const int Width, const i
return Quad; 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) bool OpenMaps(const char pMapNames[2][IO_MAX_PATH_LENGTH], CDataFileReader &InputMap, CDataFileWriter &OutputMap)
{ {
IStorage *pStorage = CreateLocalStorage(); IStorage *pStorage = CreateLocalStorage();

View file

@ -1,10 +1,12 @@
// Adapted from TWMapImagesRecovery by Tardo: https://github.com/Tardo/TWMapImagesRecovery // Adapted from TWMapImagesRecovery by Tardo: https://github.com/Tardo/TWMapImagesRecovery
#include <base/logger.h> #include <base/logger.h>
#include <base/system.h> #include <base/system.h>
#include <engine/gfx/image_loader.h> #include <engine/gfx/image_loader.h>
#include <engine/graphics.h>
#include <engine/shared/datafile.h> #include <engine/shared/datafile.h>
#include <engine/storage.h> #include <engine/storage.h>
#include <game/mapitems.h> #include <game/mapitems.h>
static void PrintMapInfo(CDataFileReader &Reader) static void PrintMapInfo(CDataFileReader &Reader)
@ -50,22 +52,18 @@ static void ExtractMapImages(CDataFileReader &Reader, const char *pPathSave)
continue; continue;
} }
IOHANDLE File = io_open(aBuf, IOFLAG_WRITE); CImageInfo Image;
if(File) Image.m_Width = pItem->m_Width;
{ Image.m_Height = pItem->m_Height;
log_info("map_extract", "writing image: %s (%dx%d)", aBuf, pItem->m_Width, pItem->m_Height); Image.m_Format = CImageInfo::FORMAT_RGBA;
TImageByteBuffer ByteBuffer; Image.m_pData = static_cast<uint8_t *>(Reader.GetData(pItem->m_ImageData));
SImageByteBuffer ImageByteBuffer(&ByteBuffer);
if(SavePng(IMAGE_FORMAT_RGBA, (const uint8_t *)Reader.GetData(pItem->m_ImageData), ImageByteBuffer, pItem->m_Width, pItem->m_Height)) log_info("map_extract", "writing image: %s (%dx%d)", aBuf, pItem->m_Width, pItem->m_Height);
io_write(File, &ByteBuffer.front(), ByteBuffer.size()); if(!CImageLoader::SavePng(io_open(aBuf, IOFLAG_WRITE), aBuf, Image))
io_close(File);
Reader.UnloadData(pItem->m_ImageData);
}
else
{ {
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);
} }
} }

View file

@ -3,11 +3,13 @@
#include <base/logger.h> #include <base/logger.h>
#include <base/system.h> #include <base/system.h>
#include <engine/gfx/image_loader.h> #include <engine/gfx/image_loader.h>
#include <engine/graphics.h>
#include <engine/shared/datafile.h> #include <engine/shared/datafile.h>
#include <engine/storage.h> #include <engine/storage.h>
#include <game/mapitems.h> #include <game/mapitems.h>
/* /*
Usage: map_replace_image <source map filepath> <dest map filepath> <current image name> <new image filepath> Usage: map_replace_image <source map filepath> <dest map filepath> <current image name> <new image filepath>
Notes: map filepath must be relative to user default teeworlds folder Notes: map filepath must be relative to user default teeworlds folder
@ -23,55 +25,6 @@ int g_NewDataId = -1;
int g_NewDataSize = 0; int g_NewDataSize = 0;
void *g_pNewData = nullptr; 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) void *ReplaceImageItem(int Index, CMapItemImage *pImgItem, const char *pImgName, const char *pImgFile, CMapItemImage *pNewImgItem)
{ {
const char *pName = g_DataReader.GetDataString(pImgItem->m_ImageName); 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); dbg_msg("map_replace_image", "found image '%s'", pImgName);
CImageInfo ImgInfo; CImageInfo ImgInfo;
if(!LoadPng(&ImgInfo, pImgFile)) int PngliteIncompatible;
return 0; if(!CImageLoader::LoadPng(io_open(pImgName, IOFLAG_READ), pImgName, ImgInfo, PngliteIncompatible))
return nullptr;
if(ImgInfo.m_Format != CImageInfo::FORMAT_RGBA) if(ImgInfo.m_Format != CImageInfo::FORMAT_RGBA)
{ {
dbg_msg("map_replace_image", "image '%s' is not in RGBA format", pImgName); dbg_msg("map_replace_image", "ERROR: only RGBA PNG images are supported");
return 0; ImgInfo.Free();
return nullptr;
} }
*pNewImgItem = *pImgItem; *pNewImgItem = *pImgItem;