1#include "image_manipulation.h"
2
3#include <base/color.h>
4#include <base/dbg.h>
5#include <base/math.h>
6#include <base/mem.h>
7
8#include <engine/image.h>
9
10#include <cstdlib>
11
12bool ConvertToRgba(uint8_t *pDest, const CImageInfo &SourceImage)
13{
14 if(SourceImage.m_Format == CImageInfo::FORMAT_RGBA)
15 {
16 mem_copy(dest: pDest, source: SourceImage.m_pData, size: SourceImage.DataSize());
17 return true;
18 }
19 else
20 {
21 const size_t SrcChannelCount = CImageInfo::PixelSize(Format: SourceImage.m_Format);
22 const size_t DstChannelCount = CImageInfo::PixelSize(Format: CImageInfo::FORMAT_RGBA);
23 for(size_t Y = 0; Y < SourceImage.m_Height; ++Y)
24 {
25 for(size_t X = 0; X < SourceImage.m_Width; ++X)
26 {
27 size_t ImgOffsetSrc = (Y * SourceImage.m_Width * SrcChannelCount) + (X * SrcChannelCount);
28 size_t ImgOffsetDest = (Y * SourceImage.m_Width * DstChannelCount) + (X * DstChannelCount);
29 if(SourceImage.m_Format == CImageInfo::FORMAT_RGB)
30 {
31 mem_copy(dest: &pDest[ImgOffsetDest], source: &SourceImage.m_pData[ImgOffsetSrc], size: SrcChannelCount);
32 pDest[ImgOffsetDest + 3] = 255;
33 }
34 else if(SourceImage.m_Format == CImageInfo::FORMAT_RA)
35 {
36 pDest[ImgOffsetDest + 0] = SourceImage.m_pData[ImgOffsetSrc];
37 pDest[ImgOffsetDest + 1] = SourceImage.m_pData[ImgOffsetSrc];
38 pDest[ImgOffsetDest + 2] = SourceImage.m_pData[ImgOffsetSrc];
39 pDest[ImgOffsetDest + 3] = SourceImage.m_pData[ImgOffsetSrc + 1];
40 }
41 else if(SourceImage.m_Format == CImageInfo::FORMAT_R)
42 {
43 pDest[ImgOffsetDest + 0] = 255;
44 pDest[ImgOffsetDest + 1] = 255;
45 pDest[ImgOffsetDest + 2] = 255;
46 pDest[ImgOffsetDest + 3] = SourceImage.m_pData[ImgOffsetSrc];
47 }
48 else
49 {
50 dbg_assert_failed("SourceImage.m_Format invalid");
51 }
52 }
53 }
54 return false;
55 }
56}
57
58bool ConvertToRgbaAlloc(uint8_t *&pDest, const CImageInfo &SourceImage)
59{
60 pDest = static_cast<uint8_t *>(malloc(size: SourceImage.m_Width * SourceImage.m_Height * CImageInfo::PixelSize(Format: CImageInfo::FORMAT_RGBA)));
61 return ConvertToRgba(pDest, SourceImage);
62}
63
64bool ConvertToRgba(CImageInfo &Image)
65{
66 if(Image.m_Format == CImageInfo::FORMAT_RGBA)
67 return true;
68
69 uint8_t *pRgbaData;
70 ConvertToRgbaAlloc(pDest&: pRgbaData, SourceImage: Image);
71 free(ptr: Image.m_pData);
72 Image.m_pData = pRgbaData;
73 Image.m_Format = CImageInfo::FORMAT_RGBA;
74 return false;
75}
76
77static inline void ConvertToGrayscalePixel(const CImageInfo &Image, size_t PixelIndex, size_t Step)
78{
79 const uint8_t R = Image.m_pData[PixelIndex * Step];
80 const uint8_t G = Image.m_pData[PixelIndex * Step + 1];
81 const uint8_t B = Image.m_pData[PixelIndex * Step + 2];
82 const uint8_t Luma = (uint8_t)(0.2126f * R + 0.7152f * G + 0.0722f * B);
83
84 Image.m_pData[PixelIndex * Step] = Luma;
85 Image.m_pData[PixelIndex * Step + 1] = Luma;
86 Image.m_pData[PixelIndex * Step + 2] = Luma;
87}
88
89void ConvertToGrayscale(const CImageInfo &Image)
90{
91 if(Image.m_Format == CImageInfo::FORMAT_R || Image.m_Format == CImageInfo::FORMAT_RA)
92 return;
93
94 const size_t Step = Image.PixelSize();
95 for(size_t PixelIndex = 0; PixelIndex < Image.m_Width * Image.m_Height; ++PixelIndex)
96 {
97 ConvertToGrayscalePixel(Image, PixelIndex, Step);
98 }
99}
100
101void ConvertToGrayscaleRect(const CImageInfo &Image, size_t StartX, size_t StartY, size_t Width, size_t Height)
102{
103 if(Image.m_Format == CImageInfo::FORMAT_R || Image.m_Format == CImageInfo::FORMAT_RA)
104 return;
105
106 const size_t Step = Image.PixelSize();
107 for(size_t PixelY = StartY; PixelY < StartY + Height; ++PixelY)
108 {
109 for(size_t PixelX = StartX; PixelX < StartX + Width; ++PixelX)
110 {
111 const size_t PixelIndex = PixelY * Image.m_Width + PixelX;
112 ConvertToGrayscalePixel(Image, PixelIndex, Step);
113 }
114 }
115}
116
117void ColorizeWithHueRect(CImageInfo &Image, float Hue, float Sat, size_t StartX, size_t StartY, size_t Width, size_t Height)
118{
119 dbg_assert(Hue >= 0.0f && Hue <= 1.0f, "Invalid hue");
120 dbg_assert(Sat >= 0.0f && Sat <= 1.0f, "Invalid saturation");
121 dbg_assert(Image.m_Format == CImageInfo::FORMAT_RGB || Image.m_Format == CImageInfo::FORMAT_RGBA, "Invalid image format");
122 dbg_assert(StartX + Width <= Image.m_Width && StartY + Height <= Image.m_Height, "Image rect is out of range");
123
124 const size_t Step = Image.PixelSize();
125 for(size_t PixelY = StartY; PixelY < StartY + Height; ++PixelY)
126 {
127 for(size_t PixelX = StartX; PixelX < StartX + Width; ++PixelX)
128 {
129 const size_t PixelIndex = PixelY * Image.m_Width + PixelX;
130 uint8_t &R = Image.m_pData[PixelIndex * Step];
131 uint8_t &G = Image.m_pData[PixelIndex * Step + 1];
132 uint8_t &B = Image.m_pData[PixelIndex * Step + 2];
133
134 ColorRGBA PixelColor(R / 255.0f, G / 255.0f, B / 255.0f, 1.0f);
135 ColorHSLA PixelColorHSLA = color_cast<ColorHSLA>(rgb: PixelColor);
136 PixelColorHSLA.h = Hue;
137 PixelColorHSLA.s = Sat;
138 PixelColor = color_cast<ColorRGBA>(hsl: PixelColorHSLA);
139
140 R = static_cast<uint8_t>(PixelColor.r * 255.0f);
141 G = static_cast<uint8_t>(PixelColor.g * 255.0f);
142 B = static_cast<uint8_t>(PixelColor.b * 255.0f);
143 }
144 }
145}
146
147static constexpr int DILATE_BPP = 4; // RGBA assumed
148static constexpr uint8_t DILATE_ALPHA_THRESHOLD = 10;
149
150static void Dilate(int w, int h, const uint8_t *pSrc, uint8_t *pDest)
151{
152 const int aDirX[] = {0, -1, 1, 0};
153 const int aDirY[] = {-1, 0, 0, 1};
154
155 int m = 0;
156 for(int y = 0; y < h; y++)
157 {
158 for(int x = 0; x < w; x++, m += DILATE_BPP)
159 {
160 for(int i = 0; i < DILATE_BPP; ++i)
161 pDest[m + i] = pSrc[m + i];
162 if(pSrc[m + DILATE_BPP - 1] > DILATE_ALPHA_THRESHOLD)
163 continue;
164
165 // --- Implementation Note ---
166 // The sum and counter variable can be used to compute a smoother dilated image.
167 // In this reference implementation, the loop breaks as soon as Counter == 1.
168 // We break the loop here to match the selection of the previously used algorithm.
169 int aSumOfOpaque[] = {0, 0, 0};
170 int Counter = 0;
171 for(int c = 0; c < 4; c++)
172 {
173 const int ClampedX = std::clamp(val: x + aDirX[c], lo: 0, hi: w - 1);
174 const int ClampedY = std::clamp(val: y + aDirY[c], lo: 0, hi: h - 1);
175 const int SrcIndex = ClampedY * w * DILATE_BPP + ClampedX * DILATE_BPP;
176 if(pSrc[SrcIndex + DILATE_BPP - 1] > DILATE_ALPHA_THRESHOLD)
177 {
178 for(int p = 0; p < DILATE_BPP - 1; ++p)
179 aSumOfOpaque[p] += pSrc[SrcIndex + p];
180 ++Counter;
181 break;
182 }
183 }
184
185 if(Counter > 0)
186 {
187 for(int i = 0; i < DILATE_BPP - 1; ++i)
188 {
189 aSumOfOpaque[i] /= Counter;
190 pDest[m + i] = (uint8_t)aSumOfOpaque[i];
191 }
192
193 pDest[m + DILATE_BPP - 1] = 255;
194 }
195 }
196 }
197}
198
199static void CopyColorValues(int w, int h, const uint8_t *pSrc, uint8_t *pDest)
200{
201 int m = 0;
202 for(int y = 0; y < h; y++)
203 {
204 for(int x = 0; x < w; x++, m += DILATE_BPP)
205 {
206 if(pDest[m + DILATE_BPP - 1] == 0)
207 {
208 mem_copy(dest: &pDest[m], source: &pSrc[m], size: DILATE_BPP - 1);
209 }
210 }
211 }
212}
213
214void DilateImage(uint8_t *pImageBuff, int w, int h)
215{
216 DilateImageSub(pImageBuff, w, h, x: 0, y: 0, SubWidth: w, SubHeight: h);
217}
218
219void DilateImage(const CImageInfo &Image)
220{
221 dbg_assert(Image.m_Format == CImageInfo::FORMAT_RGBA, "Dilate requires RGBA format");
222 DilateImage(pImageBuff: Image.m_pData, w: Image.m_Width, h: Image.m_Height);
223}
224
225void DilateImageSub(uint8_t *pImageBuff, int w, int h, int x, int y, int SubWidth, int SubHeight)
226{
227 uint8_t *apBuffer[2] = {nullptr, nullptr};
228
229 const size_t ImageSize = (size_t)SubWidth * SubHeight * sizeof(uint8_t) * DILATE_BPP;
230 apBuffer[0] = (uint8_t *)malloc(size: ImageSize);
231 apBuffer[1] = (uint8_t *)malloc(size: ImageSize);
232 uint8_t *pBufferOriginal = (uint8_t *)malloc(size: ImageSize);
233
234 for(int Y = 0; Y < SubHeight; ++Y)
235 {
236 int SrcImgOffset = ((y + Y) * w * DILATE_BPP) + (x * DILATE_BPP);
237 int DstImgOffset = (Y * SubWidth * DILATE_BPP);
238 int CopySize = SubWidth * DILATE_BPP;
239 mem_copy(dest: &pBufferOriginal[DstImgOffset], source: &pImageBuff[SrcImgOffset], size: CopySize);
240 }
241
242 Dilate(w: SubWidth, h: SubHeight, pSrc: pBufferOriginal, pDest: apBuffer[0]);
243
244 for(int i = 0; i < 5; i++)
245 {
246 Dilate(w: SubWidth, h: SubHeight, pSrc: apBuffer[0], pDest: apBuffer[1]);
247 Dilate(w: SubWidth, h: SubHeight, pSrc: apBuffer[1], pDest: apBuffer[0]);
248 }
249
250 CopyColorValues(w: SubWidth, h: SubHeight, pSrc: apBuffer[0], pDest: pBufferOriginal);
251
252 free(ptr: apBuffer[0]);
253 free(ptr: apBuffer[1]);
254
255 for(int Y = 0; Y < SubHeight; ++Y)
256 {
257 int SrcImgOffset = ((y + Y) * w * DILATE_BPP) + (x * DILATE_BPP);
258 int DstImgOffset = (Y * SubWidth * DILATE_BPP);
259 int CopySize = SubWidth * DILATE_BPP;
260 mem_copy(dest: &pImageBuff[SrcImgOffset], source: &pBufferOriginal[DstImgOffset], size: CopySize);
261 }
262
263 free(ptr: pBufferOriginal);
264}
265
266static float CubicHermite(float A, float B, float C, float D, float t)
267{
268 float a = -A / 2.0f + (3.0f * B) / 2.0f - (3.0f * C) / 2.0f + D / 2.0f;
269 float b = A - (5.0f * B) / 2.0f + 2.0f * C - D / 2.0f;
270 float c = -A / 2.0f + C / 2.0f;
271 float d = B;
272
273 return (a * t * t * t) + (b * t * t) + (c * t) + d;
274}
275
276static void GetPixelClamped(const uint8_t *pSourceImage, int x, int y, uint32_t W, uint32_t H, size_t BPP, uint8_t aSample[4])
277{
278 x = std::clamp<int>(val: x, lo: 0, hi: (int)W - 1);
279 y = std::clamp<int>(val: y, lo: 0, hi: (int)H - 1);
280
281 mem_copy(dest: aSample, source: &pSourceImage[x * BPP + (W * BPP * y)], size: BPP);
282}
283
284static void SampleBicubic(const uint8_t *pSourceImage, float u, float v, uint32_t W, uint32_t H, size_t BPP, uint8_t aSample[4])
285{
286 float X = (u * W) - 0.5f;
287 const int RoundedX = (int)X;
288 const float FractionX = X - std::floor(x: X);
289
290 float Y = (v * H) - 0.5f;
291 const int RoundedY = (int)Y;
292 const float FractionY = Y - std::floor(x: Y);
293
294 uint8_t aaaSamples[4][4][4];
295 for(int y = 0; y < 4; ++y)
296 {
297 for(int x = 0; x < 4; ++x)
298 {
299 GetPixelClamped(pSourceImage, x: RoundedX + x - 1, y: RoundedY + y - 1, W, H, BPP, aSample: aaaSamples[x][y]);
300 }
301 }
302
303 for(size_t i = 0; i < BPP; i++)
304 {
305 float aRows[4];
306 for(int y = 0; y < 4; ++y)
307 {
308 aRows[y] = CubicHermite(A: aaaSamples[0][y][i], B: aaaSamples[1][y][i], C: aaaSamples[2][y][i], D: aaaSamples[3][y][i], t: FractionX);
309 }
310 aSample[i] = (uint8_t)std::clamp<float>(val: CubicHermite(A: aRows[0], B: aRows[1], C: aRows[2], D: aRows[3], t: FractionY), lo: 0.0f, hi: 255.0f);
311 }
312}
313
314static void ResizeImage(const uint8_t *pSourceImage, uint32_t SW, uint32_t SH, uint8_t *pDestinationImage, uint32_t W, uint32_t H, size_t BPP)
315{
316 for(int y = 0; y < (int)H; ++y)
317 {
318 float v = (float)y / (float)(H - 1);
319 for(int x = 0; x < (int)W; ++x)
320 {
321 float u = (float)x / (float)(W - 1);
322 uint8_t aSample[4];
323 SampleBicubic(pSourceImage, u, v, W: SW, H: SH, BPP, aSample);
324 mem_copy(dest: &pDestinationImage[x * BPP + ((W * BPP) * y)], source: aSample, size: BPP);
325 }
326 }
327}
328
329uint8_t *ResizeImage(const uint8_t *pImageData, int Width, int Height, int NewWidth, int NewHeight, int BPP)
330{
331 uint8_t *pTmpData = (uint8_t *)malloc(size: (size_t)NewWidth * NewHeight * BPP);
332 ResizeImage(pSourceImage: pImageData, SW: Width, SH: Height, pDestinationImage: pTmpData, W: NewWidth, H: NewHeight, BPP);
333 return pTmpData;
334}
335
336void ResizeImage(CImageInfo &Image, int NewWidth, int NewHeight)
337{
338 uint8_t *pNewData = ResizeImage(pImageData: Image.m_pData, Width: Image.m_Width, Height: Image.m_Height, NewWidth, NewHeight, BPP: Image.PixelSize());
339 free(ptr: Image.m_pData);
340 Image.m_pData = pNewData;
341 Image.m_Width = NewWidth;
342 Image.m_Height = NewHeight;
343}
344
345int HighestBit(int OfVar)
346{
347 if(!OfVar)
348 return 0;
349
350 int RetV = 1;
351
352 while(OfVar >>= 1)
353 RetV <<= 1;
354
355 return RetV;
356}
357