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 | |