1// tPicture.cpp 
2// 
3// This class represents a simple one-frame image. It is a collection of raw uncompressed 32-bit tPixels. It can load 
4// various formats from disk such as jpg, tga, png, etc. It intentionally _cannot_ load a dds file. More on that later. 
5// Image manipulation (excluding compression) is supported in a tPicture, so there are crop, scale, rotate, etc 
6// functions in this class. 
7// 
8// Some image disk formats have more than one 'frame' or image inside them. For example, tiff files can have more than 
9// layer, and gif/webp/apng images may be animated and have more than one frame. A tPicture can only prepresent _one_ 
10// of these frames. 
11// 
12// Copyright (c) 2006, 2016, 2017, 2020-2024 Tristan Grimmer. 
13// Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby 
14// granted, provided that the above copyright notice and this permission notice appear in all copies. 
15// 
16// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 
17// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 
18// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 
19// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 
20// PERFORMANCE OF THIS SOFTWARE. 
21 
22#include <LibKTX/include/version.h> 
23#define KTXSTRINGIFY(x) #x 
24#define KTXTOSTRING(x) KTXSTRINGIFY(x) 
25#define LIBKTX_VERSION_STRING KTXTOSTRING(LIBKTX_VERSION); 
26 
27// It says cli, but it's only the version number. Nothing to do with a CLI. 
28#include <astcenccli_version.h> 
29const char* ASTCENCODER_VERSION_STRING = VERSION_STRING
30#undef VERSION_STRING 
31 
32#include "Image/tPicture.h" 
33#include "Image/tQuantize.h" 
34#include "Math/tMatrix2.h" 
35#include "Math/tLinearAlgebra.h" 
36#include <OpenEXR/loadImage.h> 
37#include <zlib.h> 
38#include <spng.h> 
39#include <png.h> 
40#include <apngdis.h> 
41#include <apngasm.h> 
42#include <bcdec.h> 
43#include <etcdec.h> 
44#include <tiffvers.h> 
45#include <jconfig.h> // JpegTurbo 
46#include <demux.h> // WebP 
47#include <tinyxml2.h> 
48#include <TinyEXIF.h> 
49#include "Image/tResample.h" 
50 
51 
52using namespace tMath
53using namespace tImage
54using namespace tSystem
55 
56 
57const char* tImage::Version_LibJpegTurbo = LIBJPEG_TURBO_VERSION
58const char* tImage::Version_ASTCEncoder = ASTCENCODER_VERSION_STRING
59const char* tImage::Version_OpenEXR = OPENEXR_VERSION_STRING
60const char* tImage::Version_ZLIB = ZLIB_VERSION
61const char* tImage::Version_LibPNG = PNG_LIBPNG_VER_STRING
62const char* tImage::Version_ApngDis = APNGDIS_VERSION_STRING
63const char* tImage::Version_ApngAsm = APNGASM_VERSION_STRING
64const char* tImage::Version_LibTIFF = TIFFLIB_STANDARD_VERSION_STR
65const char* tImage::Version_LibKTX = LIBKTX_VERSION_STRING
66int tImage::Version_WEBP_Major = WEBP_DECODER_ABI_VERSION >> 8
67int tImage::Version_WEBP_Minor = WEBP_DECODER_ABI_VERSION & 0xFF
68int tImage::Version_BCDec_Major = BCDEC_VERSION_MAJOR
69int tImage::Version_BCDec_Minor = BCDEC_VERSION_MINOR
70int tImage::Version_ETCDec_Major = ETCDEC_VERSION_MAJOR
71int tImage::Version_ETCDec_Minor = ETCDEC_VERSION_MINOR
72int tImage::Version_LibSPNG_Major = SPNG_VERSION_MAJOR
73int tImage::Version_LibSPNG_Minor = SPNG_VERSION_MINOR
74int tImage::Version_LibSPNG_Patch = SPNG_VERSION_PATCH
75int tImage::Version_TinyXML2_Major = TINYXML2_MAJOR_VERSION
76int tImage::Version_TinyXML2_Minor = TINYXML2_MINOR_VERSION
77int tImage::Version_TinyXML2_Patch = TINYXML2_PATCH_VERSION
78int tImage::Version_TinyEXIF_Major = TINYEXIF_MAJOR_VERSION
79int tImage::Version_TinyEXIF_Minor = TINYEXIF_MINOR_VERSION
80int tImage::Version_TinyEXIF_Patch = TINYEXIF_PATCH_VERSION
81 
82 
83void tPicture::Save(tChunkWriter& chunk) const 
84
85 chunk.Begin(chunkID: tChunkID::Image_Picture); 
86
87 chunk.Begin(chunkID: tChunkID::Image_PictureProperties); 
88
89 chunk.Write(obj: Width); 
90 chunk.Write(obj: Height); 
91
92 chunk.End(); 
93 
94 chunk.Begin(chunkID: tChunkID::Image_PicturePixels); 
95
96 chunk.Write(data: Pixels, numItems: GetNumPixels()); 
97
98 chunk.End(); 
99
100 chunk.End(); 
101
102 
103 
104void tPicture::Load(const tChunk& chunk
105
106 Clear(); 
107 if (chunk.ID() != tChunkID::Image_Picture
108 return
109 
110 for (tChunk ch = chunk.First(); ch.IsValid(); ch = ch.Next()) 
111
112 switch (ch.ID()) 
113
114 case tChunkID::Image_PictureProperties
115
116 ch.GetItem(item&: Width); 
117 ch.GetItem(item&: Height); 
118 break
119
120 
121 case tChunkID::Image_PicturePixels
122
123 tAssert(!Pixels && (GetNumPixels() > 0)); 
124 Pixels = new tPixel4b[GetNumPixels()]; 
125 ch.GetItems(dest: Pixels, numItems: GetNumPixels()); 
126 break
127
128
129
130 PixelFormatSrc = tPixelFormat::R8G8B8A8
131
132 
133 
134void tPicture::Rotate90(bool antiClockwise
135
136 tAssert((Width > 0) && (Height > 0) && Pixels); 
137 int newW = Height
138 int newH = Width
139 tPixel4b* newPixels = new tPixel4b[newW * newH]; 
140 
141 for (int y = 0; y < Height; y++) 
142 for (int x = 0; x < Width; x++) 
143 newPixels[ GetIndex(x: y, y: x, w: newW, h: newH) ] = Pixels[ GetIndex(x: antiClockwise ? x : Width-1-x, y: antiClockwise ? Height-1-y : y) ]; 
144 
145 Clear(); 
146 Width = newW
147 Height = newH
148 Pixels = newPixels
149
150 
151 
152void tPicture::RotateCenter(float angle, const tPixel4b& fill, tResampleFilter upFilter, tResampleFilter downFilter
153
154 if (!IsValid()) 
155 return
156 
157 tMatrix2 rotMat
158 rotMat.MakeRotateZ(angle); 
159 
160 // Matrix is orthonormal so inverse is transpose. 
161 tMatrix2 invRot(rotMat); 
162 invRot.Transpose(); 
163 
164 // UpFilter DownFilter Description 
165 // None NA No up/down scaling. Preserves colours. Nearest Neighbour. Fast. Good for pixel art. 
166 // Valid Valid Up/down scaling. Smooth. Good results with up=bilinear, down=box. 
167 // Valid None Up/down scaling. Use alternate (sharper) downscaling scheme (pad + 2 X ScaleHalf). 
168 if (upFilter == tResampleFilter::None
169 RotateCenterNearest(rotMat, invRot, fill); 
170 else 
171 RotateCenterResampled(rotMat, invRot, fill, upFilter, downFilter); 
172
173 
174 
175void tPicture::RotateCenterNearest(const tMatrix2& rotMat, const tMatrix2& invRot, const tPixel4b& fill
176
177 int srcW = Width
178 int srcH = Height
179 
180 // Rotate all corners to get new size. Memfill it with fill colour. Map from old to new. 
181 float srcHalfW = float(Width)/2.0f
182 float srcHalfH = float(Height)/2.0f
183 tPixel4b* srcPixels = Pixels
184 
185 tVector2 tl(-srcHalfW, srcHalfH); 
186 tVector2 tr( srcHalfW, srcHalfH); 
187 tVector2 bl(-srcHalfW, -srcHalfH); 
188 tVector2 br( srcHalfW, -srcHalfH); 
189 tl = rotMat*tl; tr = rotMat*tr; bl = rotMat*bl; br = rotMat*br
190 float epsilon = 0.0002f
191 int minx = int(tFloor(v: tRound(v: tMin(a: tl.x, b: tr.x, c: bl.x, d: br.x), nearest: epsilon))); 
192 int miny = int(tFloor(v: tRound(v: tMin(a: tl.y, b: tr.y, c: bl.y, d: br.y), nearest: epsilon))); 
193 int maxx = int(tCeiling(v: tRound(v: tMax(a: tl.x, b: tr.x, c: bl.x, d: br.x), nearest: epsilon))); 
194 int maxy = int(tCeiling(v: tRound(v: tMax(a: tl.y, b: tr.y, c: bl.y, d: br.y), nearest: epsilon))); 
195 Width = maxx - minx
196 Height = maxy - miny
197 
198 Pixels = new tPixel4b[Width*Height]; 
199 float halfW = float(Width)/2.0f
200 float halfH = float(Height)/2.0f
201 
202 // We now need to loop through every pixel in the new image and do a weighted sample of 
203 // the pixels it maps to in the original image. Actually weighted is not implemented yet 
204 // so do nearest. 
205 for (int y = 0; y < Height; y++) 
206
207 for (int x = 0; x < Width; x++) 
208
209 // Lets start with nearest pixel. We can get fancier after. 
210 // dstPos is the middle of the pixel we are writing to. srcPos is the original we are coming from. 
211 // The origin' of a pixel is the lower-left corner. The 0.5s get us to the center (and back). 
212 tVector2 dstPos(float(x)+0.5f - halfW, float(y)+0.5f - halfH); 
213 tVector2 srcPos = invRot*dstPos
214 srcPos += tVector2(srcHalfW, srcHalfH); 
215 srcPos -= tVector2(0.5f, 0.5f); 
216 
217 tPixel4b srcCol = tPixel4b::black
218 
219 int srcX = int(tRound(v: srcPos.x)); 
220 int srcY = int(tRound(v: srcPos.y)); 
221 bool useFill = (srcX < 0) || (srcX >= srcW) || (srcY < 0) || (srcY >= srcH); 
222 srcCol = useFill ? fill : srcPixels[ GetIndex(x: srcX, y: srcY, w: srcW, h: srcH) ]; 
223 Pixels[ GetIndex(x, y) ] = srcCol
224
225
226 
227 delete[] srcPixels
228
229 
230 
231void tPicture::RotateCenterResampled 
232
233 const tMatrix2& rotMat, const tMatrix2& invRot, const tPixel4b& fill
234 tResampleFilter upFilter, tResampleFilter downFilter 
235
236
237 tAssert(upFilter != tResampleFilter::None); 
238 if (upFilter == tResampleFilter::Nearest
239
240 Resample(width: Width*2, height: Height*2, upFilter); 
241 Resample(width: Width*2, height: Height*2, upFilter); 
242
243 else 
244
245 Resample(width: Width*4, height: Height*4, upFilter); 
246
247  
248 RotateCenterNearest(rotMat, invRot, fill); 
249 
250 // After this call we are not guaranteed that the width and height are multiples of 4. If the downFilder is None 
251 // we need to use the ScaleHalf procedure, in which case a padding/crop call mey need to be done in order to get 
252 // the dimensions as a multiple of 4. 
253 if (downFilter == tResampleFilter::None
254
255 int newW = (Width % 4) ? Width + (4 - (Width % 4)) : Width
256 int newH = (Height % 4) ? Height + (4 - (Height % 4)) : Height
257 if ((newW != Width) || (newH != Height)) 
258 Crop(newWidth: newW, newHeight: newH, Anchor::MiddleMiddle, fill); 
259  
260 bool scaleHalfSuccess
261 scaleHalfSuccess = ScaleHalf(); tAssert(scaleHalfSuccess); 
262 scaleHalfSuccess = ScaleHalf(); tAssert(scaleHalfSuccess); 
263
264 else 
265
266 Resample(width: Width/4, height: Height/4, downFilter); 
267
268
269 
270 
271void tPicture::Flip(bool horizontal
272
273 tAssert((Width > 0) && (Height > 0) && Pixels); 
274 int newW = Width
275 int newH = Height
276 tPixel4b* newPixels = new tPixel4b[newW * newH]; 
277 
278 for (int y = 0; y < Height; y++) 
279 for (int x = 0; x < Width; x++) 
280 newPixels[ GetIndex(x, y) ] = Pixels[ GetIndex(x: horizontal ? Width-1-x : x, y: horizontal ? y : Height-1-y) ]; 
281 
282 Clear(); 
283 Width = newW
284 Height = newH
285 Pixels = newPixels
286
287 
288 
289bool tPicture::Crop(int newW, int newH, Anchor anchor, const tColour4b& fill
290
291 int originx = 0
292 int originy = 0
293 
294 switch (anchor
295
296 case Anchor::LeftTop: originx = 0; originy = Height-newH; break
297 case Anchor::MiddleTop: originx = Width/2 - newW/2; originy = Height-newH; break
298 case Anchor::RightTop: originx = Width - newW; originy = Height-newH; break
299 
300 case Anchor::LeftMiddle: originx = 0; originy = Height/2-newH/2; break
301 case Anchor::MiddleMiddle: originx = Width/2 - newW/2; originy = Height/2-newH/2; break
302 case Anchor::RightMiddle: originx = Width - newW; originy = Height/2-newH/2; break
303 
304 case Anchor::LeftBottom: originx = 0; originy = 0; break
305 case Anchor::MiddleBottom: originx = Width/2 - newW/2; originy = 0; break
306 case Anchor::RightBottom: originx = Width - newW; originy = 0; break
307
308 
309 return Crop(newWidth: newW, newHeight: newH, originX: originx, originY: originy, fill); 
310
311 
312 
313bool tPicture::Crop(int newW, int newH, int originX, int originY, const tColour4b& fill
314
315 if ((newW <= 0) || (newH <= 0)) 
316
317 Clear(); 
318 return false
319
320 
321 if ((newW == Width) && (newH == Height) && (originX == 0) && (originY == 0)) 
322 return false
323 
324 tPixel4b* newPixels = new tPixel4b[newW * newH]; 
325 
326 // Set the new pixel colours. 
327 for (int y = 0; y < newH; y++) 
328
329 for (int x = 0; x < newW; x++) 
330
331 // If we're in range of the old picture we just copy the colour. If the old image is invalid no problem, as 
332 // we'll fall through to the else and the pixel will be set to black. 
333 if (tMath::tInIntervalIE(val: originX + x, min: 0, max: Width) && tMath::tInIntervalIE(val: originY + y, min: 0, max: Height)) 
334 newPixels[y * newW + x] = GetPixel(x: originX + x, y: originY + y); 
335 else 
336 newPixels[y * newW + x] = fill
337
338
339 
340 Clear(); 
341 Width = newW
342 Height = newH
343 Pixels = newPixels
344 return true
345
346 
347 
348bool tPicture::CopyRegion(int regionW, int regionH, const tColour4b* regionPixels, int originX, int originY, comp_t channels
349
350 if (!IsValid() || (regionW <= 0) || (regionH <= 0) || !regionPixels
351 return false
352 
353 if ((originX <= -regionW) || (originX >= Width)) 
354 return false
355 if ((originY <= -regionH) || (originY >= Height)) 
356 return false
357 
358 for (int y = 0; y < regionH; y++) 
359
360 int dstRow = originY + y
361 
362 // Is line above or below current canvas. 
363 if ((dstRow < 0) || (dstRow >= Height)) 
364 continue
365 
366 for (int x = 0; x < regionW; x++) 
367
368 int dstCol = originX + x
369 if ((dstCol < 0) || (dstCol >= Width)) 
370 continue
371 if (channels != tCompBit_RGBA
372 SetPixel(x: dstCol, y: dstRow, c: regionPixels[y*regionW + x], channels); 
373 else 
374 SetPixel(x: dstCol, y: dstRow, c: regionPixels[y*regionW + x]); 
375
376
377 
378 return true
379
380 
381 
382bool tPicture::CopyRegion(int regW, int regH, const tColour4b* regionPixels, Anchor anchor, comp_t channels
383
384 int originx = 0
385 int originy = 0
386 
387 switch (anchor
388
389 case Anchor::LeftTop: originx = 0; originy = Height-regH; break
390 case Anchor::MiddleTop: originx = Width/2 - regW/2; originy = Height-regH; break
391 case Anchor::RightTop: originx = Width - regW; originy = Height-regH; break
392 
393 case Anchor::LeftMiddle: originx = 0; originy = Height/2-regH/2; break
394 case Anchor::MiddleMiddle: originx = Width/2 - regW/2; originy = Height/2-regH/2; break
395 case Anchor::RightMiddle: originx = Width - regW; originy = Height/2-regH/2; break
396 
397 case Anchor::LeftBottom: originx = 0; originy = 0; break
398 case Anchor::MiddleBottom: originx = Width/2 - regW/2; originy = 0; break
399 case Anchor::RightBottom: originx = Width - regW; originy = 0; break
400
401 
402 return CopyRegion(regionW: regW, regionH: regH, regionPixels, originX: originx, originY: originy, channels); 
403
404 
405 
406bool tPicture::GetBordersSizes 
407
408 const tColour4b& colour, comp_t channels
409 int& numBottomRows, int& numTopRows, int& numLeftCols, int& numRightCols 
410) const 
411
412 // Count bottom rows to crop. 
413 numBottomRows = 0
414 for (int y = 0; y < Height; y++) 
415
416 bool allMatch = true
417 for (int x = 0; x < Width; x++) 
418
419 if (!colour.Equal(colour: Pixels[ GetIndex(x, y) ], channels)) 
420
421 allMatch = false
422 break
423
424
425 if (allMatch
426 numBottomRows++; 
427 else 
428 break
429
430 
431 // Count top rows to crop. 
432 numTopRows = 0
433 for (int y = Height-1; y >= 0; y--) 
434
435 bool allMatch = true
436 for (int x = 0; x < Width; x++) 
437
438 if (!colour.Equal(colour: Pixels[ GetIndex(x, y) ], channels)) 
439
440 allMatch = false
441 break
442
443
444 if (allMatch
445 numTopRows++; 
446 else 
447 break
448
449 
450 // Count left columns to crop. 
451 numLeftCols = 0
452 for (int x = 0; x < Width; x++) 
453
454 bool allMatch = true
455 for (int y = 0; y < Height; y++) 
456
457 if (!colour.Equal(colour: Pixels[ GetIndex(x, y) ], channels)) 
458
459 allMatch = false
460 break
461
462
463 if (allMatch
464 numLeftCols++; 
465 else 
466 break
467
468 
469 // Count right columns to crop. 
470 numRightCols = 0
471 for (int x = Width-1; x >= 0; x--) 
472
473 bool allMatch = true
474 for (int y = 0; y < Height; y++) 
475
476 if (!colour.Equal(colour: Pixels[ GetIndex(x, y) ], channels)) 
477
478 allMatch = false
479 break
480
481
482 if (allMatch
483 numRightCols++; 
484 else 
485 break
486
487 
488 if ((numLeftCols == 0) && (numRightCols == 0) && (numBottomRows == 0) && (numTopRows == 0)) 
489 return false
490 
491 int newWidth = Width - numLeftCols - numRightCols
492 int newHeight = Height - numBottomRows - numTopRows
493 if ((newWidth <= 0) || (newHeight <= 0)) 
494 return false
495 
496 return true
497
498 
499 
500bool tPicture::Deborder(const tColour4b& colour, comp_t channels
501
502 int numBottomRows = 0
503 int numTopRows = 0
504 int numLeftCols = 0
505 int numRightCols = 0
506 bool hasBorders = GetBordersSizes(colour, channels, numBottomRows, numTopRows, numLeftCols, numRightCols); 
507 if (!hasBorders
508 return false
509 
510 int newWidth = Width - numLeftCols - numRightCols
511 int newHeight = Height - numBottomRows - numTopRows
512 
513 Crop(newW: newWidth, newH: newHeight, originX: numLeftCols, originY: numBottomRows); 
514 return true
515
516 
517 
518bool tPicture::HasBorders(const tColour4b& colour, comp_t channels) const 
519
520 int numBottomRows = 0
521 int numTopRows = 0
522 int numLeftCols = 0
523 int numRightCols = 0
524 return GetBordersSizes(colour, channels, numBottomRows, numTopRows, numLeftCols, numRightCols); 
525
526 
527 
528bool tPicture::QuantizeFixed(int numColours, bool checkExact
529
530 if (!IsValid()) 
531 return false
532 
533 tColour3b* destPalette = new tColour3b[numColours]; 
534 uint8* destIndices = new uint8[Width*Height]; 
535 
536 bool ok = tQuantizeFixed::QuantizeImage(numColours, width: Width, height: Height, pixels: Pixels, destPalette, destIndices, checkExact); 
537 if (!ok
538
539 delete[] destIndices
540 delete[] destPalette
541 return false
542
543 
544 // Now that we have the palette we can convert back into the pixel array. 
545 ok = tQuantize::ConvertToPixels(destPixels: Pixels, width: Width, height: Height, srcPalette: destPalette, srcIndices: destIndices, preserveDestAlpha: true); 
546 delete[] destIndices
547 delete[] destPalette
548 return ok
549
550 
551 
552bool tPicture::QuantizeSpatial(int numColours, bool checkExact, double ditherLevel, int filterSize
553
554 if (!IsValid()) 
555 return false
556 
557 tColour3b* destPalette = new tColour3b[numColours]; 
558 uint8* destIndices = new uint8[Width*Height]; 
559 
560 bool ok = tQuantizeSpatial::QuantizeImage(numColours, width: Width, height: Height, pixels: Pixels, destPalette, destIndices, checkExact, ditherLevel, filterSize); 
561 if (!ok
562
563 delete[] destIndices
564 delete[] destPalette
565 return false
566
567 
568 // Now that we have the palette we can convert back into the pixel array (preserving alpha). 
569 ok = tQuantize::ConvertToPixels(destPixels: Pixels, width: Width, height: Height, srcPalette: destPalette, srcIndices: destIndices, preserveDestAlpha: checkExact); 
570 delete[] destIndices
571 delete[] destPalette
572 return ok
573
574 
575 
576bool tPicture::QuantizeNeu(int numColours, bool checkExact, int sampleFactor
577
578 if (!IsValid()) 
579 return false
580 
581 tColour3b* destPalette = new tColour3b[numColours]; 
582 uint8* destIndices = new uint8[Width*Height]; 
583 
584 bool ok = tQuantizeNeu::QuantizeImage(numColours, width: Width, height: Height, pixels: Pixels, destPalette, destIndices, checkExact, sampleFactor); 
585 if (!ok
586
587 delete[] destIndices
588 delete[] destPalette
589 return false
590
591 
592 // Now that we have the palette we can convert back into the pixel array (preserving alpha). 
593 ok = tQuantize::ConvertToPixels(destPixels: Pixels, width: Width, height: Height, srcPalette: destPalette, srcIndices: destIndices, preserveDestAlpha: true); 
594 delete[] destIndices
595 delete[] destPalette
596 return ok
597
598 
599 
600bool tPicture::QuantizeWu(int numColours, bool checkExact
601
602 if (!IsValid()) 
603 return false
604 
605 tColour3b* destPalette = new tColour3b[numColours]; 
606 uint8* destIndices = new uint8[Width*Height]; 
607 
608 bool ok = tQuantizeWu::QuantizeImage(numColours, width: Width, height: Height, pixels: Pixels, destPalette, destIndices, checkExact); 
609 if (!ok
610
611 delete[] destIndices
612 delete[] destPalette
613 return false
614
615 
616 // Now that we have the palette we can convert back into the pixel array (preserving alpha). 
617 ok = tQuantize::ConvertToPixels(destPixels: Pixels, width: Width, height: Height, srcPalette: destPalette, srcIndices: destIndices, preserveDestAlpha: true); 
618 delete[] destIndices
619 delete[] destPalette
620 return ok
621
622 
623 
624bool tPicture::AdjustmentBegin() 
625
626 if (!IsValid() || OriginalPixels
627 return false
628 
629 OriginalPixels = new tPixel4b[Width*Height]; 
630 
631 // We need to compute min and max component values so the extents of the brigtness parameter 
632 // exactly match all black at 0 and full white at 1. We do this as we copy the pixel values. 
633 BrightnessRGBMin = 256
634 BrightnessRGBMax = -1
635 
636 tStd::tMemset(dest: HistogramR, val: 0, numBytes: sizeof(HistogramR)); MaxRCount = 0.0f
637 tStd::tMemset(dest: HistogramG, val: 0, numBytes: sizeof(HistogramG)); MaxGCount = 0.0f
638 tStd::tMemset(dest: HistogramB, val: 0, numBytes: sizeof(HistogramB)); MaxBCount = 0.0f
639 tStd::tMemset(dest: HistogramA, val: 0, numBytes: sizeof(HistogramA)); MaxACount = 0.0f
640 tStd::tMemset(dest: HistogramI, val: 0, numBytes: sizeof(HistogramI)); MaxICount = 0.0f
641 for (int p = 0; p < Width*Height; p++) 
642
643 tColour4b& colour = Pixels[p]; 
644 
645 // Min/max. All RGB components considered. 
646 int minRGB = tMath::tMin(a: colour.R, b: colour.G, c: colour.B); 
647 int maxRGB = tMath::tMax(a: colour.R, b: colour.G, c: colour.B); 
648 if (minRGB < BrightnessRGBMin
649 BrightnessRGBMin = minRGB
650 if (maxRGB > BrightnessRGBMax
651 BrightnessRGBMax = maxRGB
652 
653 // Histograms. 
654 float alpha = colour.GetA(); 
655 HistogramR[colour.R] += alpha
656 HistogramG[colour.G] += alpha
657 HistogramB[colour.B] += alpha
658 HistogramA[colour.A] += 1.0f
659 HistogramI[colour.Intensity()] += alpha
660 
661 OriginalPixels[p] = colour
662
663 tiClamp(val&: BrightnessRGBMin, min: 0, max: 255); 
664 tiClamp(val&: BrightnessRGBMax, min: 0, max: 255); 
665 
666 // Find max counts for the histograms so we can normalize if needed. 
667 for (int g = 0; g < NumGroups; g++) 
668
669 if (HistogramR[g] > MaxRCount) MaxRCount = HistogramR[g]; 
670 if (HistogramG[g] > MaxGCount) MaxGCount = HistogramG[g]; 
671 if (HistogramB[g] > MaxBCount) MaxBCount = HistogramB[g]; 
672 if (HistogramA[g] > MaxACount) MaxACount = HistogramA[g]; 
673 if (HistogramI[g] > MaxICount) MaxICount = HistogramI[g]; 
674
675 
676 return true
677
678 
679 
680bool tPicture::AdjustBrightness(float brightness, comp_t comps
681
682 if (!IsValid() || !OriginalPixels
683 return false
684 
685 // We want to guarantee all black at brightness level 0 (and no higher) and 
686 // all white at brightness 1 (and no lower). As an example, say the min RGB 
687 // for the entire image is 2 and the max is 240 -- we need 0 (black) to offset 
688 // by -240 and 1 to offset by +(255-2) = +253. 
689 int zeroOffset = -BrightnessRGBMax
690 int fullOffset = 255 - BrightnessRGBMin
691 float offsetFlt = tMath::tLinearInterp(d: brightness, dA: 0.0f, dB: 1.0f, A: float(zeroOffset), B: float(fullOffset)); 
692 int offset = int(offsetFlt); 
693 for (int p = 0; p < Width*Height; p++) 
694
695 tColour4b& srcColour = OriginalPixels[p]; 
696 tColour4b& adjColour = Pixels[p]; 
697 if (comps & tCompBit_R) adjColour.R = tClamp(val: int(srcColour.R) + offset, min: 0, max: 255); 
698 if (comps & tCompBit_G) adjColour.G = tClamp(val: int(srcColour.G) + offset, min: 0, max: 255); 
699 if (comps & tCompBit_B) adjColour.B = tClamp(val: int(srcColour.B) + offset, min: 0, max: 255); 
700 if (comps & tCompBit_A) adjColour.A = tClamp(val: int(srcColour.A) + offset, min: 0, max: 255); 
701
702 
703 return true
704
705 
706 
707bool tPicture::AdjustGetDefaultBrightness(float& brightness
708
709 if (!IsValid() || !OriginalPixels
710 return false
711 
712 int zeroOffset = -BrightnessRGBMax
713 int fullOffset = 255 - BrightnessRGBMin
714 brightness = tMath::tLinearInterp(d: 0.0f, dA: float(zeroOffset), dB: float(fullOffset), A: 0.0f, B: 1.0f); 
715 return true;  
716
717 
718 
719bool tPicture::AdjustContrast(float contrastNorm, comp_t comps
720
721 if (!IsValid() || !OriginalPixels
722 return false
723 
724 float contrast = tMath::tLinearInterp(d: contrastNorm, dA: 0.0f, dB: 1.0f, A: -255.0f, B: 255.0f); 
725 
726 // The 259 is correct. Not a typo. 
727 float factor = (259.0f * (contrast + 255.0f)) / (255.0f * (259.0f - contrast)); 
728 for (int p = 0; p < Width*Height; p++) 
729
730 tColour4b& srcColour = OriginalPixels[p]; 
731 tColour4b& adjColour = Pixels[p]; 
732 if (comps & tCompBit_R) adjColour.R = tClamp(val: int(factor * (float(srcColour.R) - 128.0f) + 128.0f), min: 0, max: 255); 
733 if (comps & tCompBit_G) adjColour.G = tClamp(val: int(factor * (float(srcColour.G) - 128.0f) + 128.0f), min: 0, max: 255); 
734 if (comps & tCompBit_B) adjColour.B = tClamp(val: int(factor * (float(srcColour.B) - 128.0f) + 128.0f), min: 0, max: 255); 
735 if (comps & tCompBit_A) adjColour.A = tClamp(val: int(factor * (float(srcColour.A) - 128.0f) + 128.0f), min: 0, max: 255); 
736
737 
738 return true
739
740 
741 
742bool tPicture::AdjustGetDefaultContrast(float& contrast
743
744 if (!IsValid() || !OriginalPixels
745 return false
746 
747 contrast = 0.5f
748 return true
749
750 
751 
752bool tPicture::AdjustLevels(float blackPoint, float midPoint, float whitePoint, float blackOut, float whiteOut, bool powerMidGamma, comp_t comps
753
754 if (!IsValid() || !OriginalPixels
755 return false
756 
757 // We do all the calculations in floating point, and only convert back to denorm and clamp at the end. 
758 // First step is to ensure well-formed input values. 
759 tiSaturate(val&: blackPoint); tiSaturate(val&: midPoint); tiSaturate(val&: whitePoint); tiSaturate(val&: blackOut); tiSaturate(val&: whiteOut); 
760 tiClampMin(val&: midPoint, min: blackPoint); 
761 tiClampMin(val&: whitePoint, min: midPoint); 
762 tiClampMin(val&: whiteOut, min: blackOut); 
763 
764 // Midtone gamma. 
765 float gamma = 1.0f
766 if (powerMidGamma
767
768 // The first attempt at this was to use a quadratic bezier to interpolate the gamma points.The accepted answer at 
769 // https://stackoverflow.com/questions/6711707/draw-a-quadratic-b%C3%A9zier-curve-through-three-given-points 
770 // is, in fact, incorrect. There are not an infinite number of solutions, and there's only one CV that will 
771 // interpolate the middle point. The equation for this is given a bit later and it is not in general at t=1/2 
772 // and so is useless. This isn't surprising if you think about scaling, skewing, and translating a parabola. 
773 // 
774 // Instead we use a continuous pow function in base 10. The base is chosen as our max gamma value that we want 
775 // at the white point and it will be when input is 1.0. The min gamma we want is 0.1 so that will be at 10^-1. 
776 midPoint = tLinearInterp(d: midPoint, dA: blackPoint, dB: whitePoint, A: -1.0f, B: 1.0f); 
777 gamma = tMath::tPow(a: 10.0f, b: midPoint); 
778
779 else 
780
781 // We want the midPoint to have the full range from 0..1 for >0 blackPoints and <1 whitePoints. This is needed because 
782 // we simplified the interface to have mid-point between black and white. 
783 midPoint = tLinearInterp(d: midPoint, dA: blackPoint, dB: whitePoint, A: 0.0f, B: 1.0f); 
784 if (midPoint < 0.5f
785 gamma = tMin(a: 1.0f + (9.0f*(1.0f - 2.0f*midPoint)), b: 9.99f); 
786 else if (gamma > 0.5f
787 // 1 - ((MidtoneNormal*2) - 1) 
788 // 1 - MidtoneNormal*2 + 1 
789 // 2 - MidtoneNormal*2 
790 // 2*(1-MidtoneNormal) 
791 gamma = tMax(a: 2.0f*(1.0f - midPoint), b: 0.01f); 
792 gamma = 1.0f/gamma
793
794 
795 // Apply for every pixel. 
796 for (int p = 0; p < Width*Height; p++) 
797
798 tColour4b& srcColour = OriginalPixels[p]; 
799 tColour4b& dstColour = Pixels[p]; 
800 
801 for (int e = 0; e < 4; e++) 
802
803 if ((1 << e) & comps
804
805 float src = float(srcColour.E[e])/255.0f
806 
807 // Black/white levels. 
808 float adj = (src - blackPoint) / (whitePoint - blackPoint); 
809 
810 // Midtones. 
811 adj = tPow(a: adj, b: gamma); 
812 
813 // Output black/white levels. 
814 adj = blackOut + adj*(whiteOut - blackOut); 
815 dstColour.E[e] = tClamp(val: int(adj*255.0f), min: 0, max: 255); 
816
817
818
819 return true
820
821 
822 
823bool tPicture::AdjustGetDefaultLevels(float& blackPoint, float& midPoint, float& whitePoint, float& outBlack, float& outWhite
824
825 if (!IsValid() || !OriginalPixels
826 return false
827 
828 blackPoint = 0.0f
829 midPoint = 0.5f
830 whitePoint = 1.0f
831 outBlack = 0.0f
832 outWhite = 1.0f
833 return true
834
835 
836 
837bool tPicture::AdjustRestoreOriginal() 
838
839 if (!IsValid() || !OriginalPixels
840 return false
841 
842 tStd::tMemcpy(dest: Pixels, src: OriginalPixels, numBytes: Width*Height*sizeof(tPixel4b)); 
843 return true
844
845 
846 
847bool tPicture::AdjustmentEnd() 
848
849 if (!IsValid() || !OriginalPixels
850 return false
851 
852 delete[] OriginalPixels
853 OriginalPixels = nullptr
854 return true
855
856 
857 
858bool tPicture::ScaleHalf() 
859
860 if (!IsValid()) 
861 return false
862 
863 // A 1x1 image is defined as already being rescaled. 
864 if ((Width == 1) && (Height == 1)) 
865 return true
866 
867 // We only allow non-divisible-by-2 dimensions if that dimension is exactly 1. 
868 if ( ((Width & 1) && (Width != 1)) || ((Height & 1) && (Height != 1)) ) 
869 return false
870 
871 int newWidth = Width >> 1
872 int newHeight = Height >> 1
873 if (newWidth == 0
874 newWidth = 1
875 if (newHeight == 0
876 newHeight = 1
877 
878 int numNewPixels = newWidth*newHeight
879 tPixel4b* newPixels = new tPixel4b[numNewPixels]; 
880 
881 // Deal with case where src height is 1 and src width is divisible by 2 OR where src width is 1 and src height is 
882 // divisible by 2. Image is either a row or column vector in this case. 
883 if ((Height == 1) || (Width == 1)) 
884
885 for (int p = 0; p < numNewPixels; p++) 
886
887 int p2 = 2*p
888 
889 int p0r = Pixels[p2].R
890 int p1r = Pixels[p2 + 1].R
891 newPixels[p].R = tMath::tClamp(val: (p0r + p1r)>>1, min: 0, max: 255); 
892 
893 int p0g = Pixels[p2].G
894 int p1g = Pixels[p2 + 1].G
895 newPixels[p].G = tMath::tClamp(val: (p0g + p1g)>>1, min: 0, max: 255); 
896 
897 int p0b = Pixels[p2].B
898 int p1b = Pixels[p2 + 1].B
899 newPixels[p].B = tMath::tClamp(val: (p0b + p1b)>>1, min: 0, max: 255); 
900 
901 int p0a = Pixels[p2].A
902 int p1a = Pixels[p2 + 1].A
903 newPixels[p].A = tMath::tClamp(val: (p0a + p1a)>>1, min: 0, max: 255); 
904
905
906 
907 // Handle the case where both width and height are both divisible by 2. 
908 else 
909
910 for (int x = 0; x < newWidth; x++) 
911
912 int x2 = 2*x
913 for (int y = 0; y < newHeight; y++) 
914
915 int y2 = 2*y
916 
917 // @todo Use SSE/SIMD here? 
918 int p0r = Pixels[y2*Width + x2].R
919 int p1r = Pixels[y2*Width + x2 + 1].R
920 int p2r = Pixels[(y2+1)*Width + x2].R
921 int p3r = Pixels[(y2+1)*Width + x2 + 1].R
922 newPixels[y*newWidth + x].R = tMath::tClamp(val: (p0r + p1r + p2r + p3r)>>2, min: 0, max: 255); 
923 
924 int p0g = Pixels[y2*Width + x2].G
925 int p1g = Pixels[y2*Width + x2 + 1].G
926 int p2g = Pixels[(y2+1)*Width + x2].G
927 int p3g = Pixels[(y2+1)*Width + x2 + 1].G
928 newPixels[y*newWidth + x].G = tMath::tClamp(val: (p0g + p1g + p2g + p3g)>>2, min: 0, max: 255); 
929 
930 int p0b = Pixels[y2*Width + x2].B
931 int p1b = Pixels[y2*Width + x2 + 1].B
932 int p2b = Pixels[(y2+1)*Width + x2].B
933 int p3b = Pixels[(y2+1)*Width + x2 + 1].B
934 newPixels[y*newWidth + x].B = tMath::tClamp(val: (p0b + p1b + p2b + p3b)>>2, min: 0, max: 255); 
935 
936 int p0a = Pixels[y2*Width + x2].A
937 int p1a = Pixels[y2*Width + x2 + 1].A
938 int p2a = Pixels[(y2+1)*Width + x2].A
939 int p3a = Pixels[(y2+1)*Width + x2 + 1].A
940 newPixels[y*newWidth + x].A = tMath::tClamp(val: (p0a + p1a + p2a + p3a)>>2, min: 0, max: 255); 
941
942
943
944 
945 Clear(); 
946 Pixels = newPixels
947 Width = newWidth
948 Height = newHeight
949 return true
950
951 
952 
953bool tPicture::Resample(int width, int height, tResampleFilter filter, tResampleEdgeMode edgeMode
954
955 if (!IsValid() || (width <= 0) || (height <= 0)) 
956 return false
957 
958 if ((width == Width) && (height == Height)) 
959 return true
960 
961 tPixel4b* newPixels = new tPixel4b[width*height]; 
962 bool success = tImage::Resample(src: Pixels, srcW: Width, srcH: Height, dst: newPixels, dstW: width, dstH: height, filter, edgeMode); 
963 if (!success
964
965 delete[] newPixels
966 return false
967
968 
969 delete[] Pixels
970 Pixels = newPixels
971 Width = width
972 Height = height
973 
974 return true
975
976 
977 
978int tPicture::GenerateLayers(tList<tLayer>& layers, tResampleFilter filter, tResampleEdgeMode edgeMode, bool chain
979
980 if (!IsValid()) 
981 return 0
982 
983 int numAppended = 0
984 
985 // We always append a fullsize layer. 
986 layers.Append(item: new tLayer(tPixelFormat::R8G8B8A8, Width, Height, (uint8*)GetPixelPointer())); 
987 numAppended++; 
988 
989 if (filter == tResampleFilter::None
990 return numAppended
991 
992 int srcW = Width
993 int srcH = Height
994 uint8* srcPixels = (uint8*)GetPixelPointer(); 
995 
996 // We base the next mip level on previous -- mostly because it's faster than resampling from the full 
997 // image each time. It's unclear to me which would generate better results. 
998 while ((srcW > 1) || (srcH > 1)) 
999
1000 int dstW = srcW >> 1; tiClampMin(val&: dstW, min: 1); 
1001 int dstH = srcH >> 1; tiClampMin(val&: dstH, min: 1); 
1002 uint8* dstPixels = new uint8[dstW*dstH*sizeof(tPixel4b)]; 
1003 
1004 bool success = false
1005 if (chain
1006 success = tImage::Resample(src: (tPixel4b*)srcPixels, srcW, srcH, dst: (tPixel4b*)dstPixels, dstW, dstH, filter, edgeMode); 
1007 else 
1008 success = tImage::Resample(src: GetPixelPointer(), srcW: Width, srcH: Height, dst: (tPixel4b*)dstPixels, dstW, dstH, filter, edgeMode); 
1009 if (!success
1010 break
1011 
1012 layers.Append(item: new tLayer(tPixelFormat::R8G8B8A8, dstW, dstH, dstPixels, true)); 
1013 numAppended++; 
1014 
1015 // Het ready for next loop. 
1016 srcH = dstH
1017 srcW = dstW
1018 srcPixels = dstPixels
1019
1020 
1021 return numAppended
1022
1023