You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Editor/Util/ImageUtil.cpp

527 lines
15 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
// Description : Image utilities implementation.
#include "EditorDefs.h"
#include "ImageUtil.h"
// Editor
#include "Util/ImageGif.h"
#include "Util/ImageTIF.h"
//////////////////////////////////////////////////////////////////////////
bool CImageUtil::Save(const QString& strFileName, CImageEx& inImage)
{
QImage imgBitmap;
ImageToQImage(inImage, imgBitmap);
// Explicitly set the pixels per meter in our images to a consistent default.
// The normal default is 96 pixels per inch, or 3780 pixels per meter.
// However, the Windows scaling display setting can cause these numbers to vary
// on different machines, producing output files that have slightly different
// headers from machine to machine, which often isn't desirable.
const int defaultPixelsPerMeter = 3780;
imgBitmap.setDotsPerMeterX(defaultPixelsPerMeter);
imgBitmap.setDotsPerMeterY(defaultPixelsPerMeter);
return imgBitmap.save(strFileName);
}
//////////////////////////////////////////////////////////////////////////
bool CImageUtil::SaveBitmap(const QString& szFileName, CImageEx& inImage)
{
return Save(szFileName, inImage);
}
//////////////////////////////////////////////////////////////////////////
bool CImageUtil::SaveJPEG(const QString& strFileName, CImageEx& inImage)
{
return Save(strFileName, inImage);
}
//////////////////////////////////////////////////////////////////////////
bool CImageUtil::Load(const QString& fileName, CImageEx& image)
{
QImage imgBitmap(fileName);
if (imgBitmap.isNull())
{
CLogFile::FormatLine("Invalid file: %s", fileName.toUtf8().data());
return false;
}
return QImageToImage(imgBitmap, image);
}
//////////////////////////////////////////////////////////////////////////
bool CImageUtil::LoadJPEG(const QString& strFileName, CImageEx& outImage)
{
return CImageUtil::Load(strFileName, outImage);
}
//===========================================================================
bool CImageUtil::LoadBmp(const QString& fileName, CImageEx& image)
{
return CImageUtil::Load(fileName, image);
}
//////////////////////////////////////////////////////////////////////////
bool CImageUtil::SavePGM(const QString& fileName, const CImageEx& image)
{
// There are two types of PGM ("Portable Grey Map") files - "raw" (binary) and "plain" (ASCII). This function supports the "plain PGM" format.
// See http://netpbm.sourceforge.net/doc/pgm.html or https://en.wikipedia.org/wiki/Netpbm_format for the definition.
uint32 width = image.GetWidth();
uint32 height = image.GetHeight();
uint32* pixels = image.GetData();
// Create the file header.
AZStd::string fileHeader = AZStd::string::format(
// P2 = PGM header for ASCII output. (P5 is PGM header for binary output)
"P2\n"
// width and height of the image
"%d %d\n"
// The maximum grey value in the file. (i.e. the max value for any given pixel below)
"65535\n"
, width, height);
FILE* file = nullptr;
azfopen(&file, fileName.toUtf8().data(), "wt");
if (!file)
{
return false;
}
// First print the file header
fprintf(file, "%s", fileHeader.c_str());
// Then print all the pixels.
for (uint32 y = 0; y < height; y++)
{
for (uint32 x = 0; x < width; x++)
{
fprintf(file, "%d ", pixels[x + (y * width)]);
}
fprintf(file, "\n");
}
fclose(file);
return true;
}
//////////////////////////////////////////////////////////////////////////
bool CImageUtil::LoadPGM(const QString& fileName, CImageEx& image)
{
FILE* file = nullptr;
azfopen(&file, fileName.toUtf8().data(), "rt");
if (!file)
{
return false;
}
const char seps[] = " \n\t\r";
char* token;
int32 width = 0;
int32 height = 0;
int32 numColors = 1;
fseek(file, 0, SEEK_END);
int fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
char* str = new char[fileSize];
[[maybe_unused]] auto bytesRead = fread(str, fileSize, 1, file);
[[maybe_unused]] char* nextToken = nullptr;
token = azstrtok(str, 0, seps, &nextToken);
while (token != nullptr && token[0] == '#')
{
if (token != nullptr && token[0] == '#')
{
azstrtok(nullptr, 0, "\n", &nextToken);
}
token = azstrtok(nullptr, 0, seps, &nextToken);
}
if (azstricmp(token, "P2") != 0)
{
// Bad file. not supported pgm.
delete[]str;
fclose(file);
return false;
}
do
{
token = azstrtok(nullptr, 0, seps, &nextToken);
if (token != nullptr && token[0] == '#')
{
azstrtok(nullptr, 0, "\n", &nextToken);
}
} while (token != nullptr && token[0] == '#');
width = atoi(token);
do
{
token = azstrtok(nullptr, 0, seps, &nextToken);
if (token != nullptr && token[0] == '#')
{
azstrtok(nullptr, 0, "\n", &nextToken);
}
} while (token != nullptr && token[0] == '#');
height = atoi(token);
do
{
token = azstrtok(nullptr, 0, seps, &nextToken);
if (token != nullptr && token[0] == '#')
{
azstrtok(nullptr, 0, "\n", &nextToken);
}
} while (token != nullptr && token[0] == '#');
numColors = atoi(token);
image.Allocate(width, height);
uint32* p = image.GetData();
int size = width * height;
int i = 0;
while (token != nullptr && i < size)
{
do
{
token = azstrtok(nullptr, 0, seps, &nextToken);
} while (token != nullptr && token[0] == '#');
*p++ = atoi(token);
i++;
}
delete[]str;
fclose(file);
// If we have 16-bit greyscale values that we're storing into 32-bit pixels, denote it with an appropriate texture type.
if (numColors > 255)
{
image.SetFormat(eTF_R16G16);
}
return true;
}
//////////////////////////////////////////////////////////////////////////
bool CImageUtil::LoadImage(const QString& fileName, CImageEx& image, bool* pQualityLoss)
{
char drive[_MAX_DRIVE];
char dir[_MAX_DIR];
char fname[_MAX_FNAME];
char ext[_MAX_EXT];
if (pQualityLoss)
{
*pQualityLoss = false;
}
_splitpath_s(fileName.toUtf8().data(), drive, dir, fname, ext);
// Only DDS has explicit sRGB flag - we'll assume by default all formats are stored in gamma space
image.SetSRGB(true);
if (azstricmp(ext, ".bmp") == 0)
{
return LoadBmp(fileName, image);
}
else if (azstricmp(ext, ".tif") == 0)
{
return CImageTIF().Load(fileName, image);
}
else if (azstricmp(ext, ".jpg") == 0)
{
if (pQualityLoss)
{
*pQualityLoss = true; // we assume JPG has quality loss
}
return LoadJPEG(fileName, image);
}
else if (azstricmp(ext, ".gif") == 0)
{
return CImageGif().Load(fileName, image);
}
else if (azstricmp(ext, ".pgm") == 0)
{
return LoadPGM(fileName, image);
}
else if (azstricmp(ext, ".png") == 0)
{
return CImageUtil::Load(fileName, image);
}
else
{
return CImageUtil::Load(fileName, image);
}
}
//////////////////////////////////////////////////////////////////////////
bool CImageUtil::SaveImage(const QString& fileName, CImageEx& image)
{
char drive[_MAX_DRIVE];
char dir[_MAX_DIR];
char fname[_MAX_FNAME];
char ext[_MAX_EXT];
// Remove the read-only attribute so the file can be overwritten.
QFile(fileName).setPermissions(QFile::ReadUser | QFile::WriteUser);
_splitpath_s(fileName.toUtf8().data(), drive, dir, fname, ext);
if (azstricmp(ext, ".bmp") == 0)
{
return SaveBitmap(fileName, image);
}
else if (azstricmp(ext, ".jpg") == 0)
{
return SaveJPEG(fileName, image);
}
else if (azstricmp(ext, ".pgm") == 0)
{
return SavePGM(fileName, image);
}
else
{
return Save(fileName, image);
}
}
//////////////////////////////////////////////////////////////////////////
void CImageUtil::ScaleToFit(const CByteImage& srcImage, CByteImage& trgImage)
{
trgImage.ScaleToFit(srcImage);
}
//////////////////////////////////////////////////////////////////////////
void CImageUtil::DownScaleSquareTextureTwice(const CImageEx& srcImage, CImageEx& trgImage, IImageUtil::_EAddrMode eAddressingMode)
{
uint32* pSrcData = srcImage.GetData();
int nSrcWidth = srcImage.GetWidth();
int nSrcHeight = srcImage.GetHeight();
int nTrgWidth = srcImage.GetWidth() >> 1;
int nTrgHeight = srcImage.GetHeight() >> 1;
// reallocate target
trgImage.Release();
trgImage.Allocate(nTrgWidth, nTrgHeight);
uint32* pDstData = trgImage.GetData();
// values in this filter are the log2 of the actual multiplicative values .. see DXCFILTER_BLUR3X3 for the used 3x3 filter
static int filter[3][3] =
{
{0, 1, 0},
{1, 2, 1},
{0, 1, 0}
};
for (int i = 0; i < nTrgHeight; i++)
{
for (int j = 0; j < nTrgWidth; j++)
{
// filter3x3
int x = j << 1;
int y = i << 1;
int r, g, b, a;
r = b = g = a = 0;
uint32 col;
if (eAddressingMode == IImageUtil::WRAP) // TODO: this condition could be compile-time static by making it a template arg
{
for (int ii = 0; ii < 3; ii++)
{
for (int jj = 0; jj < 3; jj++)
{
col = pSrcData[((y + nSrcHeight + ii - 1) % nSrcHeight) * nSrcWidth + ((x + nSrcWidth + jj - 1) % nSrcWidth)];
r += (col & 0xff) << filter[ii][jj];
g += ((col >> 8) & 0xff) << filter[ii][jj];
b += ((col >> 16) & 0xff) << filter[ii][jj];
a += ((col >> 24) & 0xff) << filter[ii][jj];
}
}
}
else
{
assert(eAddressingMode == IImageUtil::CLAMP);
for (int ii = 0; ii < 3; ii++)
{
for (int jj = 0; jj < 3; jj++)
{
int x1 = clamp_tpl<int>((x + jj), 0, nSrcWidth - 1);
int y1 = clamp_tpl<int>((y + ii), 0, nSrcHeight - 1);
col = pSrcData[ y1 * nSrcWidth + x1];
r += (col & 0xff) << filter[ii][jj];
g += ((col >> 8) & 0xff) << filter[ii][jj];
b += ((col >> 16) & 0xff) << filter[ii][jj];
a += ((col >> 24) & 0xff) << filter[ii][jj];
}
}
}
// the sum of the multiplicative values here is 16 so we shift by 4 bits
r >>= 4;
g >>= 4;
b >>= 4;
a >>= 4;
uint32 res = r + (g << 8) + (b << 16) + (a << 24);
*pDstData++ = res;
}
}
}
//////////////////////////////////////////////////////////////////////////
void CImageUtil::ScaleToFit(const CImageEx& srcImage, CImageEx& trgImage)
{
trgImage.ScaleToFit(srcImage);
}
//////////////////////////////////////////////////////////////////////////
void CImageUtil::ScaleToDoubleFit(const CImageEx& srcImage, CImageEx& trgImage)
{
uint32 x, y, u, v;
unsigned int* destRow, * dest, * src, * sourceRow;
uint32 srcW = srcImage.GetWidth();
uint32 srcH = srcImage.GetHeight();
uint32 trgHalfW = trgImage.GetWidth() / 2;
uint32 trgH = trgImage.GetHeight();
uint32 xratio = trgHalfW > 0 ? (srcW << 16) / trgHalfW : 1;
uint32 yratio = trgH > 0 ? (srcH << 16) / trgH : 1;
src = srcImage.GetData();
destRow = trgImage.GetData();
v = 0;
for (y = 0; y < trgH; y++)
{
u = 0;
sourceRow = src + (v >> 16) * srcW;
dest = destRow;
for (x = 0; x < trgHalfW; x++)
{
*(dest + trgHalfW) = sourceRow[u >> 16];
*dest++ = sourceRow[u >> 16];
u += xratio;
}
v += yratio;
destRow += trgHalfW * 2;
}
}
//////////////////////////////////////////////////////////////////////////
void CImageUtil::SmoothImage(CByteImage& image, int numSteps)
{
assert(numSteps > 0);
uint8* buf = image.GetData();
int w = image.GetWidth();
int h = image.GetHeight();
for (int steps = 0; steps < numSteps; steps++)
{
// Smooth the image.
for (int y = 1; y < h - 1; y++)
{
// Precalculate for better speed
uint8* ptr = &buf[y * w + 1];
for (int x = 1; x < w - 1; x++)
{
// Smooth it out
*ptr =
(
(uint32)ptr[1] +
ptr[w] +
ptr[-1] +
ptr[-w] +
ptr[w + 1] +
ptr[w - 1] +
ptr[-w + 1] +
ptr[-w - 1]
) >> 3;
// Next pixel
ptr++;
}
}
}
}
unsigned char CImageUtil::GetBilinearFilteredAt(const int iniX256, const int iniY256, const CByteImage& image)
{
// assert(image.IsValid()); if(!image.IsValid())return(0); // this shouldn't be
DWORD x = (DWORD)(iniX256) >> 8;
DWORD y = (DWORD)(iniY256) >> 8;
if (x >= static_cast<DWORD>(image.GetWidth() - 1) || y >= static_cast<DWORD>(image.GetHeight() - 1))
{
return image.ValueAt(x, y); // border is not filtered, 255 to get in range 0..1
}
DWORD rx = (DWORD)(iniX256) & 0xff; // fractional aprt
DWORD ry = (DWORD)(iniY256) & 0xff; // fractional aprt
DWORD top = (DWORD)image.ValueAt((int)x, (int)y) * (256 - rx) // left top
+ (DWORD)image.ValueAt((int)x + 1, (int)y) * rx; // right top
DWORD bottom = (DWORD)image.ValueAt((int)x, (int)y + 1) * (256 - rx) // left bottom
+ (DWORD)image.ValueAt((int)x + 1, (int)y + 1) * rx; // right bottom
return (unsigned char)((top * (256 - ry) + bottom * ry) >> 16);
}
bool CImageUtil::QImageToImage(const QImage& bitmap, CImageEx& image)
{
QImage convertedBitmap;
const QImage *srcBitmap = &bitmap;
if (bitmap.format() != QImage::Format_RGBA8888)
{
convertedBitmap = bitmap.convertToFormat(QImage::Format_RGBA8888);
srcBitmap = &convertedBitmap;
}
if (image.Allocate(srcBitmap->width(), srcBitmap->height()) == false)
{
return false;
}
AZStd::copy(srcBitmap->bits(), srcBitmap->bits() + (srcBitmap->width() * srcBitmap->height() * sizeof(uint32)), reinterpret_cast<uint8*>(image.GetData()));
return true;
}
bool CImageUtil::ImageToQImage(const CImageEx& image, QImage& bitmapObj)
{
bitmapObj = QImage(image.GetWidth(), image.GetHeight(), QImage::Format_RGBA8888);
AZStd::copy(image.GetData(), image.GetData() + image.GetWidth() * image.GetHeight(), reinterpret_cast<uint32*>(bitmapObj.bits()));
return true;
}