| 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>  |
| 29 | const 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 |   |
| 52 | using namespace tMath;  |
| 53 | using namespace tImage;  |
| 54 | using namespace tSystem;  |
| 55 |   |
| 56 |   |
| 57 | const char* tImage::Version_LibJpegTurbo = LIBJPEG_TURBO_VERSION;  |
| 58 | const char* tImage::Version_ASTCEncoder = ASTCENCODER_VERSION_STRING;  |
| 59 | const char* tImage::Version_OpenEXR = OPENEXR_VERSION_STRING;  |
| 60 | const char* tImage::Version_ZLIB = ZLIB_VERSION;  |
| 61 | const char* tImage::Version_LibPNG = PNG_LIBPNG_VER_STRING;  |
| 62 | const char* tImage::Version_ApngDis = APNGDIS_VERSION_STRING;  |
| 63 | const char* tImage::Version_ApngAsm = APNGASM_VERSION_STRING;  |
| 64 | const char* tImage::Version_LibTIFF = TIFFLIB_STANDARD_VERSION_STR;  |
| 65 | const char* tImage::Version_LibKTX = LIBKTX_VERSION_STRING;  |
| 66 | int tImage::Version_WEBP_Major = WEBP_DECODER_ABI_VERSION >> 8;  |
| 67 | int tImage::Version_WEBP_Minor = WEBP_DECODER_ABI_VERSION & 0xFF;  |
| 68 | int tImage::Version_BCDec_Major = BCDEC_VERSION_MAJOR;  |
| 69 | int tImage::Version_BCDec_Minor = BCDEC_VERSION_MINOR;  |
| 70 | int tImage::Version_ETCDec_Major = ETCDEC_VERSION_MAJOR;  |
| 71 | int tImage::Version_ETCDec_Minor = ETCDEC_VERSION_MINOR;  |
| 72 | int tImage::Version_LibSPNG_Major = SPNG_VERSION_MAJOR;  |
| 73 | int tImage::Version_LibSPNG_Minor = SPNG_VERSION_MINOR;  |
| 74 | int tImage::Version_LibSPNG_Patch = SPNG_VERSION_PATCH;  |
| 75 | int tImage::Version_TinyXML2_Major = TINYXML2_MAJOR_VERSION;  |
| 76 | int tImage::Version_TinyXML2_Minor = TINYXML2_MINOR_VERSION;  |
| 77 | int tImage::Version_TinyXML2_Patch = TINYXML2_PATCH_VERSION;  |
| 78 | int tImage::Version_TinyEXIF_Major = TINYEXIF_MAJOR_VERSION;  |
| 79 | int tImage::Version_TinyEXIF_Minor = TINYEXIF_MINOR_VERSION;  |
| 80 | int tImage::Version_TinyEXIF_Patch = TINYEXIF_PATCH_VERSION;  |
| 81 |   |
| 82 |   |
| 83 | void 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 |   |
| 104 | void 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 |   |
| 134 | void 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 |   |
| 152 | void 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 |   |
| 175 | void 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 |   |
| 231 | void 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 |   |
| 271 | void 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 |   |
| 289 | bool 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 |   |
| 313 | bool 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 |   |
| 348 | bool 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 |   |
| 382 | bool 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 |   |
| 406 | bool tPicture::  |
| 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 |   |
| 500 | bool 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 |   |
| 518 | bool 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 |   |
| 528 | bool 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 |   |
| 552 | bool 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 |   |
| 576 | bool 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 |   |
| 600 | bool 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 |   |
| 624 | bool 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 |   |
| 680 | bool 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 |   |
| 707 | bool 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 |   |
| 719 | bool 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 |   |
| 742 | bool tPicture::AdjustGetDefaultContrast(float& contrast)  |
| 743 | {  |
| 744 | if (!IsValid() || !OriginalPixels)  |
| 745 | return false;  |
| 746 |   |
| 747 | contrast = 0.5f;  |
| 748 | return true;  |
| 749 | }  |
| 750 |   |
| 751 |   |
| 752 | bool 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 |   |
| 823 | bool 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 |   |
| 837 | bool 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 |   |
| 847 | bool tPicture::AdjustmentEnd()  |
| 848 | {  |
| 849 | if (!IsValid() || !OriginalPixels)  |
| 850 | return false;  |
| 851 |   |
| 852 | delete[] OriginalPixels;  |
| 853 | OriginalPixels = nullptr;  |
| 854 | return true;  |
| 855 | }  |
| 856 |   |
| 857 |   |
| 858 | bool 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 |   |
| 953 | bool 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 |   |
| 978 | int 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 | |