| 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 images may be animated and have more than one frame. A tPicture can only prepresent _one_ of   |
| 10 | // these frames.  |
| 11 | //  |
| 12 | // Copyright (c) 2006, 2016, 2017, 2019, 2020 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 "Foundation/tStandard.h"  |
| 23 | #include "Image/tPicture.h"  |
| 24 | #include "Math/tMatrix2.h"  |
| 25 | #include <OpenEXR/loadImage.h>  |
| 26 | #include <zlib.h>  |
| 27 | #include <png.h>  |
| 28 | #include <apngdis.h>  |
| 29 | #include "LibTIFF/include/tiffvers.h"  |
| 30 | #ifdef PLATFORM_WINDOWS  |
| 31 | #include "TurboJpeg/Windows/jconfig.h"  |
| 32 | #include "WebP/Windows/include/demux.h"  |
| 33 | #elif defined(PLATFORM_LINUX)  |
| 34 | #include "TurboJpeg/Linux/jconfig.h"  |
| 35 | #include "WebP/Linux/include/demux.h"  |
| 36 | #endif  |
| 37 | #include "Image/tResample.h"  |
| 38 |   |
| 39 |   |
| 40 | using namespace tMath;  |
| 41 | using namespace tImage;  |
| 42 | using namespace tSystem;  |
| 43 |   |
| 44 |   |
| 45 | const char* tImage::Version_LibJpegTurbo = LIBJPEG_TURBO_VERSION;  |
| 46 | const char* tImage::Version_OpenEXR = OPENEXR_VERSION_STRING;  |
| 47 | const char* tImage::Version_ZLIB = ZLIB_VERSION;  |
| 48 | const char* tImage::Version_LibPNG = PNG_LIBPNG_VER_STRING;  |
| 49 | const char* tImage::Version_ApngDis = APNGDIS_VERSION_STRING;  |
| 50 | const char* tImage::Version_LibTIFF = TIFFLIB_STANDARD_VERSION_STR;  |
| 51 | int tImage::Version_WEBP_Major = WEBP_DECODER_ABI_VERSION >> 8;  |
| 52 | int tImage::Version_WEBP_Minor = WEBP_DECODER_ABI_VERSION & 0xFF;  |
| 53 |   |
| 54 |   |
| 55 | void tPicture::Set(int width, int height, const tPixel& colour)  |
| 56 | {  |
| 57 | tAssert((width > 0) && (height > 0));  |
| 58 |   |
| 59 | // Reuse the existing buffer if possible.  |
| 60 | if (width*height != Width*Height)  |
| 61 | {  |
| 62 | delete[] Pixels;  |
| 63 | Pixels = new tPixel[width*height];  |
| 64 | }  |
| 65 | Width = width;  |
| 66 | Height = height;  |
| 67 | for (int pixel = 0; pixel < (Width*Height); pixel++)  |
| 68 | Pixels[pixel] = colour;  |
| 69 |   |
| 70 | SrcPixelFormat = tPixelFormat::R8G8B8A8;  |
| 71 | }  |
| 72 |   |
| 73 |   |
| 74 | void tPicture::Set(int width, int height, tPixel* pixelBuffer, bool copyPixels)  |
| 75 | {  |
| 76 | tAssert((width > 0) && (height > 0) && pixelBuffer);  |
| 77 |   |
| 78 | // If we're copying the pixels we may be able to reuse the existing buffer if it's the right size. If we're not  |
| 79 | // copying and the buffer is being handed to us, we just need to free our current buffer.  |
| 80 | if (copyPixels)  |
| 81 | {  |
| 82 | if (width*height != Width*Height)  |
| 83 | {  |
| 84 | delete[] Pixels;  |
| 85 | Pixels = new tPixel[width*height];  |
| 86 | }  |
| 87 | }  |
| 88 | else  |
| 89 | {  |
| 90 | delete[] Pixels;  |
| 91 | Pixels = pixelBuffer;  |
| 92 | }  |
| 93 | Width = width;  |
| 94 | Height = height;  |
| 95 |   |
| 96 | if (copyPixels)  |
| 97 | tStd::tMemcpy(Pixels, pixelBuffer, Width*Height*sizeof(tPixel));  |
| 98 |   |
| 99 | SrcPixelFormat = tPixelFormat::R8G8B8A8;  |
| 100 | }  |
| 101 |   |
| 102 |   |
| 103 | bool tPicture::CanSave(tFileType fileType)  |
| 104 | {  |
| 105 | switch (fileType)  |
| 106 | {  |
| 107 | case tFileType::TGA:  |
| 108 | case tFileType::BMP:  |
| 109 | case tFileType::JPG:  |
| 110 | case tFileType::PNG:  |
| 111 | return true;  |
| 112 | }  |
| 113 |   |
| 114 | return false;  |
| 115 | }  |
| 116 |   |
| 117 |   |
| 118 | bool tPicture::CanLoad(tFileType fileType)  |
| 119 | {  |
| 120 | switch (fileType)  |
| 121 | {  |
| 122 | case tFileType::APNG:  |
| 123 | case tFileType::BMP:  |
| 124 | case tFileType::EXR:  |
| 125 | case tFileType::GIF:  |
| 126 | case tFileType::HDR:  |
| 127 | case tFileType::ICO:  |
| 128 | case tFileType::JPG:  |
| 129 | case tFileType::PNG:  |
| 130 | case tFileType::TGA:  |
| 131 | case tFileType::TIFF:  |
| 132 | case tFileType::WEBP:  |
| 133 | case tFileType::XPM:  |
| 134 | return true;  |
| 135 | }  |
| 136 |   |
| 137 | return false;  |
| 138 | }  |
| 139 |   |
| 140 |   |
| 141 | bool tPicture::Save(const tString& imageFile, tPicture::tColourFormat colourFmt, int quality)  |
| 142 | {  |
| 143 | if (!IsValid())  |
| 144 | return false;  |
| 145 |   |
| 146 | tFileType fileType = tGetFileType(imageFile);  |
| 147 | if (!CanSave(fileType))  |
| 148 | return false;  |
| 149 |   |
| 150 | // Native formats.  |
| 151 | switch (fileType)  |
| 152 | {  |
| 153 | case tFileType::BMP:  |
| 154 | return SaveBMP(imageFile);  |
| 155 |   |
| 156 | case tFileType::JPG:  |
| 157 | return SaveJPG(imageFile, quality);  |
| 158 |   |
| 159 | case tFileType::PNG:  |
| 160 | return SavePNG(imageFile);  |
| 161 |   |
| 162 | case tFileType::TGA:  |
| 163 | return SaveTGA(imageFile, tImage::tImageTGA::tFormat(colourFmt), tImage::tImageTGA::tCompression::None);  |
| 164 | }  |
| 165 |   |
| 166 | return false;  |
| 167 | }  |
| 168 |   |
| 169 |   |
| 170 | bool tPicture::SaveBMP(const tString& bmpFile) const  |
| 171 | {  |
| 172 | tFileType fileType = tGetFileType(bmpFile);  |
| 173 | if (!IsValid() || (fileType != tFileType::BMP))  |
| 174 | return false;  |
| 175 |   |
| 176 | tImageBMP bmp(Pixels, Width, Height);  |
| 177 | tImageBMP::tFormat savedFormat = bmp.Save(bmpFile);  |
| 178 | return int(savedFormat) ? true : false;  |
| 179 | }  |
| 180 |   |
| 181 |   |
| 182 | bool tPicture::SaveJPG(const tString& jpgFile, int quality) const  |
| 183 | {  |
| 184 | tFileType fileType = tGetFileType(jpgFile);  |
| 185 | if (!IsValid() || (fileType != tFileType::JPG))  |
| 186 | return false;  |
| 187 |   |
| 188 | tImageJPG jpeg(Pixels, Width, Height);  |
| 189 | bool success = jpeg.Save(jpgFile, quality);  |
| 190 | return success;  |
| 191 | }  |
| 192 |   |
| 193 |   |
| 194 | bool tPicture::SavePNG(const tString& pngFile) const  |
| 195 | {  |
| 196 | tFileType fileType = tGetFileType(pngFile);  |
| 197 | if (!IsValid() || (fileType != tFileType::PNG))  |
| 198 | return false;  |
| 199 |   |
| 200 | tImagePNG png(Pixels, Width, Height);  |
| 201 | bool success = png.Save(pngFile);  |
| 202 | return success;  |
| 203 | }  |
| 204 |   |
| 205 |   |
| 206 | bool tPicture::SaveTGA(const tString& tgaFile, tImageTGA::tFormat format, tImageTGA::tCompression compression) const  |
| 207 | {  |
| 208 | tFileType fileType = tGetFileType(tgaFile);  |
| 209 | if (!IsValid() || (fileType != tFileType::TGA))  |
| 210 | return false;  |
| 211 |   |
| 212 | tImageTGA targa(Pixels, Width, Height);  |
| 213 | tImageTGA::tFormat savedFormat = targa.Save(tgaFile, format, compression);  |
| 214 | if (savedFormat == tImageTGA::tFormat::Invalid)  |
| 215 | return false;  |
| 216 |   |
| 217 | return true;  |
| 218 | }  |
| 219 |   |
| 220 |   |
| 221 | bool tPicture::Load(const tString& imageFile, int , LoadParams params)  |
| 222 | {  |
| 223 | tMath::tiClampMin(frameNum, 0);  |
| 224 |   |
| 225 | Clear();  |
| 226 | if (!tFileExists(imageFile))  |
| 227 | return false;  |
| 228 |   |
| 229 | tFileType fileType = tGetFileType(imageFile);  |
| 230 | if (!CanLoad(fileType))  |
| 231 | return false;  |
| 232 |   |
| 233 | Filename = imageFile;  |
| 234 |   |
| 235 | // Native handlers.  |
| 236 | // @todo It's looking like we should make an abstract tImageBase class at this point to get rid of all this duplication.  |
| 237 | switch (fileType)  |
| 238 | {  |
| 239 | case tFileType::APNG:  |
| 240 | {  |
| 241 | tImageAPNG apng;  |
| 242 | bool ok = apng.Load(imageFile);  |
| 243 | if (!ok || !apng.IsValid())  |
| 244 | return false;  |
| 245 |   |
| 246 | if (frameNum >= apng.GetNumFrames())  |
| 247 | return false;  |
| 248 |   |
| 249 | tImageAPNG::Frame* stolenFrame = apng.StealFrame(frameNum);  |
| 250 | Width = stolenFrame->Width;  |
| 251 | Height = stolenFrame->Height;  |
| 252 | SrcPixelFormat = stolenFrame->SrcPixelFormat;  |
| 253 |   |
| 254 | Pixels = stolenFrame->Pixels;  |
| 255 |   |
| 256 | // This is safe as the frame does not own/delete the pixels.  |
| 257 | delete stolenFrame;  |
| 258 | return true;  |
| 259 | }  |
| 260 |   |
| 261 | case tFileType::BMP:  |
| 262 | {  |
| 263 | // BMPs can only have one frame.  |
| 264 | if (frameNum != 0)  |
| 265 | return false;  |
| 266 | tImageBMP bmp;  |
| 267 | bmp.Load(imageFile);  |
| 268 | if (!bmp.IsValid())  |
| 269 | return false;  |
| 270 | Width = bmp.GetWidth();  |
| 271 | Height = bmp.GetHeight();  |
| 272 | Pixels = bmp.StealPixels();  |
| 273 | SrcPixelFormat = bmp.SrcPixelFormat;  |
| 274 | return true;  |
| 275 | }  |
| 276 |   |
| 277 | case tFileType::EXR:  |
| 278 | {  |
| 279 | tImageEXR exr;  |
| 280 | bool ok = exr.Load  |
| 281 | (  |
| 282 | imageFile,  |
| 283 | frameNum,  |
| 284 | params.GammaValue,  |
| 285 | params.EXR_Exposure,  |
| 286 | params.EXR_Defog,  |
| 287 | params.EXR_KneeLow,  |
| 288 | params.EXR_KneeHigh  |
| 289 | );  |
| 290 | if (!ok || !exr.IsValid())  |
| 291 | return false;  |
| 292 | Width = exr.GetWidth();  |
| 293 | Height = exr.GetHeight();  |
| 294 | Pixels = exr.StealPixels();  |
| 295 | SrcPixelFormat = exr.SrcPixelFormat;  |
| 296 | return true;  |
| 297 | }  |
| 298 |   |
| 299 | case tFileType::GIF:  |
| 300 | {  |
| 301 | tImageGIF gif;  |
| 302 | bool ok = gif.Load(imageFile);  |
| 303 | if (!ok || !gif.IsValid())  |
| 304 | return false;  |
| 305 |   |
| 306 | if (frameNum >= gif.GetNumFrames())  |
| 307 | return false;  |
| 308 |   |
| 309 | Width = gif.GetWidth();  |
| 310 | Height = gif.GetHeight();  |
| 311 |   |
| 312 | tImageGIF::Frame* stolenFrame = gif.StealFrame(frameNum);  |
| 313 | Pixels = stolenFrame->Pixels;  |
| 314 |   |
| 315 | // This is safe as the frame does not own/delete the pixels.  |
| 316 | delete stolenFrame;  |
| 317 |   |
| 318 | SrcPixelFormat = gif.SrcPixelFormat;  |
| 319 | return true;  |
| 320 | }  |
| 321 |   |
| 322 | case tFileType::HDR:  |
| 323 | {  |
| 324 | // HDRs can only have one frame.  |
| 325 | if (frameNum != 0)  |
| 326 | return false;  |
| 327 | tImageHDR hdr;  |
| 328 | hdr.Load  |
| 329 | (  |
| 330 | imageFile,  |
| 331 | params.GammaValue,  |
| 332 | params.HDR_Exposure  |
| 333 | );  |
| 334 | if (!hdr.IsValid())  |
| 335 | return false;  |
| 336 | Width = hdr.GetWidth();  |
| 337 | Height = hdr.GetHeight();  |
| 338 | Pixels = hdr.StealPixels();  |
| 339 | SrcPixelFormat = hdr.SrcPixelFormat;  |
| 340 | return true;  |
| 341 | }  |
| 342 |   |
| 343 | case tFileType::ICO:  |
| 344 | {  |
| 345 | tImageICO ico;  |
| 346 | bool ok = ico.Load(imageFile);  |
| 347 | if (!ok || !ico.IsValid())  |
| 348 | return false;  |
| 349 |   |
| 350 | if (frameNum >= ico.GetNumFrames())  |
| 351 | return false;  |
| 352 |   |
| 353 | tImageICO::Frame* stolenFrame = ico.StealFrame(frameNum);  |
| 354 | Pixels = stolenFrame->Pixels;  |
| 355 |   |
| 356 | Width = stolenFrame->Width;  |
| 357 | Height = stolenFrame->Height;   |
| 358 | SrcPixelFormat = stolenFrame->SrcPixelFormat;  |
| 359 |   |
| 360 | // This is safe as the frame does not own/delete the pixels.  |
| 361 | delete stolenFrame;  |
| 362 | return true;  |
| 363 | }  |
| 364 |   |
| 365 | case tFileType::JPG:  |
| 366 | {  |
| 367 | // JPGs can only have one frame.  |
| 368 | if (frameNum != 0)  |
| 369 | return false;  |
| 370 | tImageJPG jpeg(imageFile);  |
| 371 | if (!jpeg.IsValid())  |
| 372 | return false;  |
| 373 | Width = jpeg.GetWidth();  |
| 374 | Height = jpeg.GetHeight();  |
| 375 | Pixels = jpeg.StealPixels();  |
| 376 | SrcPixelFormat = jpeg.SrcPixelFormat;  |
| 377 | return true;  |
| 378 | }  |
| 379 |   |
| 380 | case tFileType::PNG:  |
| 381 | {  |
| 382 | // PNGs can only have one frame.  |
| 383 | if (frameNum != 0)  |
| 384 | return false;  |
| 385 | tImagePNG png(imageFile);  |
| 386 | if (!png.IsValid())  |
| 387 | return false;  |
| 388 | Width = png.GetWidth();  |
| 389 | Height = png.GetHeight();  |
| 390 | Pixels = png.StealPixels();  |
| 391 | SrcPixelFormat = png.SrcPixelFormat;  |
| 392 | return true;  |
| 393 | }  |
| 394 |   |
| 395 | case tFileType::TGA:  |
| 396 | {  |
| 397 | // TGAs can only have one frame.  |
| 398 | if (frameNum != 0)  |
| 399 | return false;  |
| 400 | tImageTGA targa(imageFile);  |
| 401 | if (!targa.IsValid())  |
| 402 | return false;  |
| 403 | Width = targa.GetWidth();  |
| 404 | Height = targa.GetHeight();  |
| 405 | Pixels = targa.StealPixels();  |
| 406 | SrcPixelFormat = targa.SrcPixelFormat;  |
| 407 | return true;  |
| 408 | }  |
| 409 |   |
| 410 | case tFileType::TIFF:  |
| 411 | {  |
| 412 | tImageTIFF tiff;  |
| 413 | bool ok = tiff.Load(imageFile);  |
| 414 | if (!ok || !tiff.IsValid())  |
| 415 | return false;  |
| 416 |   |
| 417 | if (frameNum >= tiff.GetNumFrames())  |
| 418 | return false;  |
| 419 |   |
| 420 | tImageTIFF::Frame* stolenFrame = tiff.StealFrame(frameNum);  |
| 421 | Width = stolenFrame->Width;  |
| 422 | Height = stolenFrame->Height;  |
| 423 | SrcPixelFormat = stolenFrame->SrcPixelFormat;  |
| 424 |   |
| 425 | Pixels = stolenFrame->Pixels;  |
| 426 |   |
| 427 | // This is safe as the frame does not own/delete the pixels.  |
| 428 | delete stolenFrame;  |
| 429 | return true;  |
| 430 | }  |
| 431 |   |
| 432 | case tFileType::WEBP:  |
| 433 | {  |
| 434 | tImageWEBP webp;  |
| 435 | bool ok = webp.Load(imageFile);  |
| 436 | if (!ok || !webp.IsValid())  |
| 437 | return false;  |
| 438 |   |
| 439 | if (frameNum >= webp.GetNumFrames())  |
| 440 | return false;  |
| 441 |   |
| 442 | tImageWEBP::Frame* stolenFrame = webp.StealFrame(frameNum);  |
| 443 | Width = stolenFrame->Width;  |
| 444 | Height = stolenFrame->Height;  |
| 445 | SrcPixelFormat = stolenFrame->SrcPixelFormat;  |
| 446 |   |
| 447 | Pixels = stolenFrame->Pixels;  |
| 448 |   |
| 449 | // This is safe as the frame does not own/delete the pixels.  |
| 450 | delete stolenFrame;  |
| 451 | return true;  |
| 452 | }  |
| 453 |   |
| 454 | case tFileType::XPM:  |
| 455 | {  |
| 456 | // XPMs can only have one frame.  |
| 457 | if (frameNum != 0)  |
| 458 | return false;  |
| 459 | tImageXPM xpm(imageFile);  |
| 460 | if (!xpm.IsValid())  |
| 461 | return false;  |
| 462 | Width = xpm.GetWidth();  |
| 463 | Height = xpm.GetHeight();  |
| 464 | Pixels = xpm.StealPixels();  |
| 465 | SrcPixelFormat = xpm.SrcPixelFormat;  |
| 466 | return true;  |
| 467 | }  |
| 468 | }  |
| 469 |   |
| 470 | return false;  |
| 471 | }  |
| 472 |   |
| 473 |   |
| 474 | void tPicture::Save(tChunkWriter& chunk) const  |
| 475 | {  |
| 476 | chunk.Begin(tChunkID::Image_Picture);  |
| 477 | {  |
| 478 | chunk.Begin(tChunkID::Image_PictureProperties);  |
| 479 | {  |
| 480 | chunk.Write(Width);  |
| 481 | chunk.Write(Height);  |
| 482 | }  |
| 483 | chunk.End();  |
| 484 |   |
| 485 | chunk.Begin(tChunkID::Image_PicturePixels);  |
| 486 | {  |
| 487 | chunk.Write(Pixels, GetNumPixels());  |
| 488 | }  |
| 489 | chunk.End();  |
| 490 | }  |
| 491 | chunk.End();  |
| 492 | }  |
| 493 |   |
| 494 |   |
| 495 | void tPicture::Load(const tChunk& chunk)  |
| 496 | {  |
| 497 | Clear();  |
| 498 | if (chunk.ID() != tChunkID::Image_Picture)  |
| 499 | return;  |
| 500 |   |
| 501 | for (tChunk ch = chunk.First(); ch.IsValid(); ch = ch.Next())  |
| 502 | {  |
| 503 | switch (ch.ID())  |
| 504 | {  |
| 505 | case tChunkID::Image_PictureProperties:  |
| 506 | {  |
| 507 | ch.GetItem(Width);  |
| 508 | ch.GetItem(Height);  |
| 509 | break;  |
| 510 | }  |
| 511 |   |
| 512 | case tChunkID::Image_PicturePixels:  |
| 513 | {  |
| 514 | tAssert(!Pixels && (GetNumPixels() > 0));  |
| 515 | Pixels = new tPixel[GetNumPixels()];  |
| 516 | ch.GetItems(Pixels, GetNumPixels());  |
| 517 | break;  |
| 518 | }  |
| 519 | }  |
| 520 | }  |
| 521 | SrcPixelFormat = tPixelFormat::R8G8B8A8;  |
| 522 | }  |
| 523 |   |
| 524 |   |
| 525 | void tPicture::Crop(int newW, int newH, Anchor anchor, const tColouri& fill)  |
| 526 | {  |
| 527 | int originx = 0;  |
| 528 | int originy = 0;  |
| 529 |   |
| 530 | switch (anchor)  |
| 531 | {  |
| 532 | case Anchor::LeftTop: originx = 0; originy = Height-newH; break;  |
| 533 | case Anchor::MiddleTop: originx = Width/2 - newW/2; originy = Height-newH; break;  |
| 534 | case Anchor::RightTop: originx = Width - newW; originy = Height-newH; break;  |
| 535 |   |
| 536 | case Anchor::LeftMiddle: originx = 0; originy = Height/2-newH/2; break;  |
| 537 | case Anchor::MiddleMiddle: originx = Width/2 - newW/2; originy = Height/2-newH/2; break;  |
| 538 | case Anchor::RightMiddle: originx = Width - newW; originy = Height/2-newH/2; break;  |
| 539 |   |
| 540 | case Anchor::LeftBottom: originx = 0; originy = 0; break;  |
| 541 | case Anchor::MiddleBottom: originx = Width/2 - newW/2; originy = 0; break;  |
| 542 | case Anchor::RightBottom: originx = Width - newW; originy = 0; break;  |
| 543 | }  |
| 544 |   |
| 545 | Crop(newW, newH, originx, originy, fill);  |
| 546 | }  |
| 547 |   |
| 548 |   |
| 549 | void tPicture::Crop(int newW, int newH, int originX, int originY, const tColouri& fill)  |
| 550 | {  |
| 551 | if ((newW <= 0) || (newH <= 0))  |
| 552 | {  |
| 553 | Clear();  |
| 554 | return;  |
| 555 | }  |
| 556 |   |
| 557 | if ((newW == Width) && (newH == Height) && (originX == 0) && (originY == 0))  |
| 558 | return;  |
| 559 |   |
| 560 | tPixel* newPixels = new tPixel[newW * newH];  |
| 561 |   |
| 562 | // Set the new pixel colours.  |
| 563 | for (int y = 0; y < newH; y++)  |
| 564 | {  |
| 565 | for (int x = 0; x < newW; x++)  |
| 566 | {  |
| 567 | // If we're in range of the old picture we just copy the colour. If the old image is invalid no problem, as  |
| 568 | // we'll fall through to the else and the pixel will be set to black.  |
| 569 | if (tMath::tInIntervalIE(originX + x, 0, Width) && tMath::tInIntervalIE(originY + y, 0, Height))  |
| 570 | newPixels[y * newW + x] = GetPixel(originX + x, originY + y);  |
| 571 | else  |
| 572 | newPixels[y * newW + x] = fill;  |
| 573 | }  |
| 574 | }  |
| 575 |   |
| 576 | Clear();  |
| 577 | Width = newW;  |
| 578 | Height = newH;  |
| 579 | Pixels = newPixels;  |
| 580 | }  |
| 581 |   |
| 582 |   |
| 583 | void tPicture::Crop(const tColouri& colour, uint32 channels)  |
| 584 | {  |
| 585 | // Count bottom rows to crop.  |
| 586 | int numBottomRows = 0;  |
| 587 | for (int y = 0; y < Height; y++)  |
| 588 | {  |
| 589 | bool allMatch = true;  |
| 590 | for (int x = 0; x < Width; x++)  |
| 591 | {  |
| 592 | if (!colour.Equal(Pixels[ GetIndex(x, y) ], channels))  |
| 593 | {  |
| 594 | allMatch = false;  |
| 595 | break;  |
| 596 | }  |
| 597 | }  |
| 598 | if (allMatch)  |
| 599 | numBottomRows++;  |
| 600 | else  |
| 601 | break;  |
| 602 | }  |
| 603 |   |
| 604 | // Count top rows to crop.  |
| 605 | int numTopRows = 0;  |
| 606 | for (int y = Height-1; y >= 0; y--)  |
| 607 | {  |
| 608 | bool allMatch = true;  |
| 609 | for (int x = 0; x < Width; x++)  |
| 610 | {  |
| 611 | if (!colour.Equal(Pixels[ GetIndex(x, y) ], channels))  |
| 612 | {  |
| 613 | allMatch = false;  |
| 614 | break;  |
| 615 | }  |
| 616 | }  |
| 617 | if (allMatch)  |
| 618 | numTopRows++;  |
| 619 | else  |
| 620 | break;  |
| 621 | }  |
| 622 |   |
| 623 | // Count left columns to crop.  |
| 624 | int numLeftCols = 0;  |
| 625 | for (int x = 0; x < Width; x++)  |
| 626 | {  |
| 627 | bool allMatch = true;  |
| 628 | for (int y = 0; y < Height; y++)  |
| 629 | {  |
| 630 | if (!colour.Equal(Pixels[ GetIndex(x, y) ], channels))  |
| 631 | {  |
| 632 | allMatch = false;  |
| 633 | break;  |
| 634 | }  |
| 635 | }  |
| 636 | if (allMatch)  |
| 637 | numLeftCols++;  |
| 638 | else  |
| 639 | break;  |
| 640 | }  |
| 641 |   |
| 642 | // Count right columns to crop.  |
| 643 | int numRightCols = 0;  |
| 644 | for (int x = Width-1; x >= 0; x--)  |
| 645 | {  |
| 646 | bool allMatch = true;  |
| 647 | for (int y = 0; y < Height; y++)  |
| 648 | {  |
| 649 | if (!colour.Equal(Pixels[ GetIndex(x, y) ], channels))  |
| 650 | {  |
| 651 | allMatch = false;  |
| 652 | break;  |
| 653 | }  |
| 654 | }  |
| 655 | if (allMatch)  |
| 656 | numRightCols++;  |
| 657 | else  |
| 658 | break;  |
| 659 | }  |
| 660 |   |
| 661 | Crop(Width - numLeftCols - numRightCols, Height - numBottomRows - numTopRows, numLeftCols, numBottomRows);  |
| 662 | }  |
| 663 |   |
| 664 |   |
| 665 | void tPicture::Rotate90(bool antiClockwise)  |
| 666 | {  |
| 667 | tAssert((Width > 0) && (Height > 0) && Pixels);  |
| 668 | int newW = Height;  |
| 669 | int newH = Width;  |
| 670 | tPixel* newPixels = new tPixel[newW * newH];  |
| 671 |   |
| 672 | for (int y = 0; y < Height; y++)  |
| 673 | for (int x = 0; x < Width; x++)  |
| 674 | newPixels[ GetIndex(y, x, newW, newH) ] = Pixels[ GetIndex(antiClockwise ? x : Width-1-x, antiClockwise ? Height-1-y : y) ];  |
| 675 |   |
| 676 | Clear();  |
| 677 | Width = newW;  |
| 678 | Height = newH;  |
| 679 | Pixels = newPixels;  |
| 680 | }  |
| 681 |   |
| 682 |   |
| 683 | void tPicture::RotateCenter(float angle, const tPixel& fill, tResampleFilter filter)  |
| 684 | {  |
| 685 | if (!IsValid())  |
| 686 | return;  |
| 687 |   |
| 688 | tMatrix2 rotMat;  |
| 689 | rotMat.MakeRotateZ(angle);  |
| 690 |   |
| 691 | // Matrix is orthonormal so inverse is transpose.  |
| 692 | tMatrix2 invRot(rotMat);  |
| 693 | invRot.Transpose();  |
| 694 |   |
| 695 | if (filter != tResampleFilter::Invalid)  |
| 696 | RotateCenterResampled(rotMat, invRot, fill, filter);  |
| 697 | else  |
| 698 | RotateCenterNearest(rotMat, invRot, fill);  |
| 699 | }  |
| 700 |   |
| 701 |   |
| 702 | void tPicture::RotateCenterNearest(const tMatrix2& rotMat, const tMatrix2& invRot, const tPixel& fill)  |
| 703 | {  |
| 704 | int srcW = Width;  |
| 705 | int srcH = Height;  |
| 706 |   |
| 707 | // Rotate all corners to get new size. Memfill it with fill colour. Map from old to new.  |
| 708 | float srcHalfW = float(Width)/2.0f;  |
| 709 | float srcHalfH = float(Height)/2.0f;  |
| 710 | tPixel* srcPixels = Pixels;  |
| 711 |   |
| 712 | tVector2 tl(-srcHalfW, srcHalfH);  |
| 713 | tVector2 tr( srcHalfW, srcHalfH);  |
| 714 | tVector2 bl(-srcHalfW, -srcHalfH);  |
| 715 | tVector2 br( srcHalfW, -srcHalfH);  |
| 716 | tl = rotMat*tl; tr = rotMat*tr; bl = rotMat*bl; br = rotMat*br;  |
| 717 | float epsilon = 0.0002f;  |
| 718 | int minx = int(tFloor(tRound(tMin(tl.x, tr.x, bl.x, br.x), epsilon)));  |
| 719 | int miny = int(tFloor(tRound(tMin(tl.y, tr.y, bl.y, br.y), epsilon)));  |
| 720 | int maxx = int(tCeiling(tRound(tMax(tl.x, tr.x, bl.x, br.x), epsilon)));  |
| 721 | int maxy = int(tCeiling(tRound(tMax(tl.y, tr.y, bl.y, br.y), epsilon)));  |
| 722 | Width = maxx - minx;  |
| 723 | Height = maxy - miny;  |
| 724 |   |
| 725 | Pixels = new tPixel[Width*Height];  |
| 726 | float halfW = float(Width)/2.0f;  |
| 727 | float halfH = float(Height)/2.0f;  |
| 728 |   |
| 729 | // We now need to loop through every pixel in the new image and do a weighted sample of  |
| 730 | // the pixels it maps to in the original image. Actually weighted is not implemented yet  |
| 731 | // so do nearest.  |
| 732 | for (int y = 0; y < Height; y++)  |
| 733 | {  |
| 734 | for (int x = 0; x < Width; x++)  |
| 735 | {  |
| 736 | // Lets start with nearest pixel. We can get fancier after.  |
| 737 | // dstPos is the middle of the pixel we are writing to. srcPos is the original we are coming from.  |
| 738 | // The origin' of a pixel is the lower-left corner. The 0.5s get us to the center (and back).  |
| 739 | tVector2 dstPos(float(x)+0.5f - halfW, float(y)+0.5f - halfH);  |
| 740 | tVector2 srcPos = invRot*dstPos;  |
| 741 | srcPos += tVector2(srcHalfW, srcHalfH);  |
| 742 | srcPos -= tVector2(0.5f, 0.5f);  |
| 743 |   |
| 744 | tPixel srcCol = tPixel::black;  |
| 745 |   |
| 746 | int srcX = int(tRound(srcPos.x));  |
| 747 | int srcY = int(tRound(srcPos.y));  |
| 748 | bool useFill = (srcX < 0) || (srcX >= srcW) || (srcY < 0) || (srcY >= srcH);  |
| 749 | srcCol = useFill ? fill : srcPixels[ GetIndex(srcX, srcY, srcW, srcH) ];  |
| 750 | Pixels[ GetIndex(x, y) ] = srcCol;  |
| 751 | }  |
| 752 | }  |
| 753 |   |
| 754 | delete[] srcPixels;  |
| 755 | }  |
| 756 |   |
| 757 |   |
| 758 | void tPicture::RotateCenterResampled(const tMatrix2& rotMat, const tMatrix2& invRot, const tPixel& fill, tResampleFilter filter)  |
| 759 | {  |
| 760 | tAssert(filter != tResampleFilter::Invalid);  |
| 761 | if (filter == tResampleFilter::Nearest)  |
| 762 | {  |
| 763 | Resample(Width*2, Height*2, filter);  |
| 764 | Resample(Width*2, Height*2, filter);  |
| 765 | }  |
| 766 | else  |
| 767 | {  |
| 768 | Resample(Width*4, Height*4, filter);  |
| 769 | }  |
| 770 |   |
| 771 | RotateCenterNearest(rotMat, invRot, fill);  |
| 772 | ScaleHalf();  |
| 773 | ScaleHalf();  |
| 774 | }  |
| 775 |   |
| 776 |   |
| 777 | void tPicture::Flip(bool horizontal)  |
| 778 | {  |
| 779 | tAssert((Width > 0) && (Height > 0) && Pixels);  |
| 780 | int newW = Width;  |
| 781 | int newH = Height;  |
| 782 | tPixel* newPixels = new tPixel[newW * newH];  |
| 783 |   |
| 784 | for (int y = 0; y < Height; y++)  |
| 785 | for (int x = 0; x < Width; x++)  |
| 786 | newPixels[ GetIndex(x, y) ] = Pixels[ GetIndex(horizontal ? Width-1-x : x, horizontal ? y : Height-1-y) ];  |
| 787 |   |
| 788 | Clear();  |
| 789 | Width = newW;  |
| 790 | Height = newH;  |
| 791 | Pixels = newPixels;  |
| 792 | }  |
| 793 |   |
| 794 |   |
| 795 | bool tPicture::ScaleHalf()  |
| 796 | {  |
| 797 | if (!IsValid())  |
| 798 | return false;  |
| 799 |   |
| 800 | // A 1x1 image is defined as already being rescaled.  |
| 801 | if ((Width == 1) && (Height == 1))  |
| 802 | return true;  |
| 803 |   |
| 804 | // We only allow non-divisible-by-2 dimensions if that dimension is exactly 1.  |
| 805 | if ( ((Width & 1) && (Width != 1)) || ((Height & 1) && (Height != 1)) )  |
| 806 | return false;  |
| 807 |   |
| 808 | int newWidth = Width >> 1;  |
| 809 | int newHeight = Height >> 1;  |
| 810 | if (newWidth == 0)  |
| 811 | newWidth = 1;  |
| 812 | if (newHeight == 0)  |
| 813 | newHeight = 1;  |
| 814 |   |
| 815 | int numNewPixels = newWidth*newHeight;  |
| 816 | tPixel* newPixels = new tPixel[numNewPixels];  |
| 817 |   |
| 818 | // 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  |
| 819 | // divisible by 2. Image is either a row or column vector in this case.  |
| 820 | if ((Height == 1) || (Width == 1))  |
| 821 | {  |
| 822 | for (int p = 0; p < numNewPixels; p++)  |
| 823 | {  |
| 824 | int p2 = 2*p;  |
| 825 |   |
| 826 | int p0r = Pixels[p2].R;  |
| 827 | int p1r = Pixels[p2 + 1].R;  |
| 828 | newPixels[p].R = tMath::tClamp((p0r + p1r)>>1, 0, 255);  |
| 829 |   |
| 830 | int p0g = Pixels[p2].G;  |
| 831 | int p1g = Pixels[p2 + 1].G;  |
| 832 | newPixels[p].G = tMath::tClamp((p0g + p1g)>>1, 0, 255);  |
| 833 |   |
| 834 | int p0b = Pixels[p2].B;  |
| 835 | int p1b = Pixels[p2 + 1].B;  |
| 836 | newPixels[p].B = tMath::tClamp((p0b + p1b)>>1, 0, 255);  |
| 837 |   |
| 838 | int p0a = Pixels[p2].A;  |
| 839 | int p1a = Pixels[p2 + 1].A;  |
| 840 | newPixels[p].A = tMath::tClamp((p0a + p1a)>>1, 0, 255);  |
| 841 | }  |
| 842 | }  |
| 843 |   |
| 844 | // Handle the case where both width and height are both divisible by 2.  |
| 845 | else  |
| 846 | {  |
| 847 | for (int x = 0; x < newWidth; x++)  |
| 848 | {  |
| 849 | int x2 = 2*x;  |
| 850 | for (int y = 0; y < newHeight; y++)  |
| 851 | {  |
| 852 | int y2 = 2*y;  |
| 853 |   |
| 854 | // @todo Use SSE/SIMD here?  |
| 855 | int p0r = Pixels[y2*Width + x2].R;  |
| 856 | int p1r = Pixels[y2*Width + x2 + 1].R;  |
| 857 | int p2r = Pixels[(y2+1)*Width + x2].R;  |
| 858 | int p3r = Pixels[(y2+1)*Width + x2 + 1].R;  |
| 859 | newPixels[y*newWidth + x].R = tMath::tClamp((p0r + p1r + p2r + p3r)>>2, 0, 255);  |
| 860 |   |
| 861 | int p0g = Pixels[y2*Width + x2].G;  |
| 862 | int p1g = Pixels[y2*Width + x2 + 1].G;  |
| 863 | int p2g = Pixels[(y2+1)*Width + x2].G;  |
| 864 | int p3g = Pixels[(y2+1)*Width + x2 + 1].G;  |
| 865 | newPixels[y*newWidth + x].G = tMath::tClamp((p0g + p1g + p2g + p3g)>>2, 0, 255);  |
| 866 |   |
| 867 | int p0b = Pixels[y2*Width + x2].B;  |
| 868 | int p1b = Pixels[y2*Width + x2 + 1].B;  |
| 869 | int p2b = Pixels[(y2+1)*Width + x2].B;  |
| 870 | int p3b = Pixels[(y2+1)*Width + x2 + 1].B;  |
| 871 | newPixels[y*newWidth + x].B = tMath::tClamp((p0b + p1b + p2b + p3b)>>2, 0, 255);  |
| 872 |   |
| 873 | int p0a = Pixels[y2*Width + x2].A;  |
| 874 | int p1a = Pixels[y2*Width + x2 + 1].A;  |
| 875 | int p2a = Pixels[(y2+1)*Width + x2].A;  |
| 876 | int p3a = Pixels[(y2+1)*Width + x2 + 1].A;  |
| 877 | newPixels[y*newWidth + x].A = tMath::tClamp((p0a + p1a + p2a + p3a)>>2, 0, 255);  |
| 878 | }  |
| 879 | }  |
| 880 | }  |
| 881 |   |
| 882 | Clear();  |
| 883 | Pixels = newPixels;  |
| 884 | Width = newWidth;  |
| 885 | Height = newHeight;  |
| 886 | return true;  |
| 887 | }  |
| 888 |   |
| 889 |   |
| 890 | bool tPicture::Resample(int width, int height, tResampleFilter filter, tResampleEdgeMode edgeMode)  |
| 891 | {  |
| 892 | if (!IsValid() || (width <= 0) || (height <= 0))  |
| 893 | return false;  |
| 894 |   |
| 895 | if ((width == Width) && (height == Height))  |
| 896 | return true;  |
| 897 |   |
| 898 | tPixel* newPixels = new tPixel[width*height];  |
| 899 | bool success = tImage::Resample(Pixels, Width, Height, newPixels, width, height, filter, edgeMode);  |
| 900 | if (!success)  |
| 901 | {  |
| 902 | delete newPixels;  |
| 903 | return false;  |
| 904 | }  |
| 905 |   |
| 906 | delete[] Pixels;  |
| 907 | Pixels = newPixels;  |
| 908 | Width = width;  |
| 909 | Height = height;  |
| 910 |   |
| 911 | return true;  |
| 912 | }  |
| 913 | |