| 1 | // tImageJPG.cpp  |
| 2 | //  |
| 3 | // This class knows how to load and save a JPeg (.jpg and .jpeg) file. It does zero processing of image data. It knows  |
| 4 | // the details of the jpg file format and loads the data into a tPixel array. These tPixels may be 'stolen' by the  |
| 5 | // tPicture's constructor if a jpg file is specified. After the array is stolen the tImageJPG is invalid. This is  |
| 6 | // purely for performance. The loading and saving uses libjpeg-turbo. See Licence_LibJpegTurbo.txt for more info.  |
| 7 | //  |
| 8 | // Copyright (c) 2020, 2022-2024 Tristan Grimmer.  |
| 9 | // Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby  |
| 10 | // granted, provided that the above copyright notice and this permission notice appear in all copies.  |
| 11 | //  |
| 12 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL  |
| 13 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,  |
| 14 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN  |
| 15 | // AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR  |
| 16 | // PERFORMANCE OF THIS SOFTWARE.  |
| 17 |   |
| 18 | #include <System/tFile.h>  |
| 19 | #include "Image/tImageJPG.h"  |
| 20 | #include "Image/tPicture.h"  |
| 21 | #include "turbojpeg.h"  |
| 22 |   |
| 23 |   |
| 24 | using namespace tSystem;  |
| 25 | namespace tImage  |
| 26 | {  |
| 27 |   |
| 28 |   |
| 29 | void tImageJPG::Clear()  |
| 30 | {  |
| 31 | Width = 0;  |
| 32 | Height = 0;  |
| 33 | delete[] Pixels;  |
| 34 | Pixels = nullptr;  |
| 35 | if (MemImage) tjFree(buffer: MemImage);  |
| 36 | MemImage = nullptr;  |
| 37 | MemImageSize = 0;  |
| 38 |   |
| 39 | tBaseImage::Clear();  |
| 40 | }  |
| 41 |   |
| 42 |   |
| 43 | bool tImageJPG::Load(const tString& jpgFile, const LoadParams& params)  |
| 44 | {  |
| 45 | Clear();  |
| 46 |   |
| 47 | if (tSystem::tGetFileType(file: jpgFile) != tSystem::tFileType::JPG)  |
| 48 | return false;  |
| 49 |   |
| 50 | if (!tFileExists(file: jpgFile))  |
| 51 | return false;  |
| 52 |   |
| 53 | int numBytes = tGetFileSize(file: jpgFile);  |
| 54 | if (numBytes <= 0)  |
| 55 | return false;  |
| 56 |   |
| 57 | uint8* jpgFileInMemory = tjAlloc(bytes: numBytes);  |
| 58 | tLoadFile(file: jpgFile, buffer: jpgFileInMemory);  |
| 59 | bool success = Load(jpgFileInMemory, numBytes, params);  |
| 60 | delete[] jpgFileInMemory;  |
| 61 |   |
| 62 | return success;  |
| 63 | }  |
| 64 |   |
| 65 |   |
| 66 | bool tImageJPG::Load(const uint8* jpgFileInMemory, int numBytes, const LoadParams& params)  |
| 67 | {  |
| 68 | Clear();  |
| 69 | if ((numBytes <= 0) || !jpgFileInMemory)  |
| 70 | return false;  |
| 71 |   |
| 72 | // If no decompress we simply set the MemImage members and we're done.  |
| 73 | if ((params.Flags & LoadFlag_NoDecompress))  |
| 74 | {  |
| 75 | MemImage = tjAlloc(bytes: numBytes);  |
| 76 | tStd::tMemcpy(dest: MemImage, src: jpgFileInMemory, numBytes);  |
| 77 | MemImageSize = numBytes;  |
| 78 | return true;  |
| 79 | }  |
| 80 |   |
| 81 | PopulateMetaData(jpgFileInMemory, numBytes);  |
| 82 |   |
| 83 | tjhandle tjInstance = tjInitDecompress();  |
| 84 | if (!tjInstance)  |
| 85 | return false;  |
| 86 |   |
| 87 | int subSamp = 0;  |
| 88 | int colourSpace = 0;  |
| 89 | int = tjDecompressHeader3(handle: tjInstance, jpegBuf: jpgFileInMemory, jpegSize: numBytes, width: &Width, height: &Height, jpegSubsamp: &subSamp, jpegColorspace: &colourSpace);  |
| 90 | if (headerResult < 0)  |
| 91 | return false;  |
| 92 |   |
| 93 | int numPixels = Width * Height;  |
| 94 | Pixels = new tPixel4b[numPixels];  |
| 95 |   |
| 96 | int jpgPixelFormat = TJPF_RGBA;  |
| 97 | int flags = 0;  |
| 98 | flags |= TJFLAG_BOTTOMUP;  |
| 99 | //flags |= TJFLAG_FASTUPSAMPLE;  |
| 100 | //flags |= TJFLAG_FASTDCT;  |
| 101 | flags |= TJFLAG_ACCURATEDCT;  |
| 102 |   |
| 103 | int decomResult = tjDecompress2  |
| 104 | (  |
| 105 | handle: tjInstance, jpegBuf: jpgFileInMemory, jpegSize: numBytes, dstBuf: (uint8*)Pixels,  |
| 106 | width: Width, pitch: 0, height: Height, pixelFormat: jpgPixelFormat, flags  |
| 107 | );  |
| 108 |   |
| 109 | bool abortLoad = false;  |
| 110 | if (decomResult < 0)  |
| 111 | {  |
| 112 | int errorSeverity = tjGetErrorCode(handle: tjInstance);  |
| 113 | switch (errorSeverity)  |
| 114 | {  |
| 115 | case TJERR_WARNING:  |
| 116 | if (params.Flags & tImageJPG::LoadFlag_Strict)  |
| 117 | abortLoad = true;  |
| 118 | break;  |
| 119 |   |
| 120 | case TJERR_FATAL:  |
| 121 | abortLoad = true;  |
| 122 | break;  |
| 123 | }  |
| 124 | }  |
| 125 |   |
| 126 | tjDestroy(handle: tjInstance);  |
| 127 | if (abortLoad)  |
| 128 | {  |
| 129 | Clear();  |
| 130 | return false;  |
| 131 | }  |
| 132 |   |
| 133 | // The flips and rotates below do not clear the pixel format.  |
| 134 | if ((params.Flags & LoadFlag_ExifOrient))  |
| 135 | {  |
| 136 | const tMetaDatum& datum = MetaData[tMetaTag::Orientation];  |
| 137 | if (datum.IsSet())  |
| 138 | {  |
| 139 | switch (datum.Uint32)  |
| 140 | {  |
| 141 | case 0: // Unspecified  |
| 142 | case 1: // NoTransform  |
| 143 | break;  |
| 144 |   |
| 145 | case 2: // Flip-Y.  |
| 146 | Flip(horizontal: true);  |
| 147 | break;  |
| 148 |   |
| 149 | case 3: // Flip-XY  |
| 150 | Flip(horizontal: false);  |
| 151 | Flip(horizontal: true);  |
| 152 | break;  |
| 153 |   |
| 154 | case 4: // Flip-X  |
| 155 | Flip(horizontal: false);  |
| 156 | break;  |
| 157 |   |
| 158 | case 5: // Rot-CW90 Flip-Y  |
| 159 | Flip(horizontal: true);  |
| 160 | Rotate90(antiClockWise: true);  |
| 161 | break;  |
| 162 |   |
| 163 | case 6: // Rot-ACW90  |
| 164 | Rotate90(antiClockWise: false);  |
| 165 | break;  |
| 166 |   |
| 167 | case 7: // Rot-ACW90 Flip-Y  |
| 168 | Flip(horizontal: true);  |
| 169 | Rotate90(antiClockWise: false);  |
| 170 | break;  |
| 171 |   |
| 172 | case 8: // Rot-CW90  |
| 173 | Rotate90(antiClockWise: true);  |
| 174 | break;  |
| 175 | }  |
| 176 | }  |
| 177 | }  |
| 178 |   |
| 179 | PixelFormatSrc = tPixelFormat::R8G8B8;  |
| 180 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 181 |   |
| 182 | // JPG file are assumed to be in sRGB.  |
| 183 | ColourProfileSrc = tColourProfile::sRGB;  |
| 184 | ColourProfile = tColourProfile::sRGB;  |
| 185 |   |
| 186 | return true;  |
| 187 | }  |
| 188 |   |
| 189 |   |
| 190 | bool tImageJPG::Set(tPixel4b* pixels, int width, int height, bool steal)  |
| 191 | {  |
| 192 | Clear();  |
| 193 | if (!pixels || (width <= 0) || (height <= 0))  |
| 194 | return false;  |
| 195 |   |
| 196 | Width = width;  |
| 197 | Height = height;  |
| 198 | if (steal)  |
| 199 | {  |
| 200 | Pixels = pixels;  |
| 201 | }  |
| 202 | else  |
| 203 | {  |
| 204 | Pixels = new tPixel4b[Width*Height];  |
| 205 | tStd::tMemcpy(dest: Pixels, src: pixels, numBytes: Width*Height*sizeof(tPixel4b));  |
| 206 | }  |
| 207 |   |
| 208 | PixelFormatSrc = tPixelFormat::R8G8B8A8;  |
| 209 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 210 | ColourProfileSrc = tColourProfile::sRGB; // We assume pixels must be sRGB.  |
| 211 | ColourProfile = tColourProfile::sRGB;  |
| 212 |   |
| 213 | return true;  |
| 214 | }  |
| 215 |   |
| 216 |   |
| 217 | bool tImageJPG::Set(tFrame* frame, bool steal)  |
| 218 | {  |
| 219 | Clear();  |
| 220 | if (!frame || !frame->IsValid())  |
| 221 | return false;  |
| 222 |   |
| 223 | PixelFormatSrc = frame->PixelFormatSrc;  |
| 224 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 225 | ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB.  |
| 226 | ColourProfile = tColourProfile::sRGB;  |
| 227 |   |
| 228 | Set(pixels: frame->GetPixels(steal), width: frame->Width, height: frame->Height, steal);  |
| 229 | if (steal)  |
| 230 | delete frame;  |
| 231 |   |
| 232 | // We don't know the colour space of the pixels.  |
| 233 | return true;  |
| 234 | }  |
| 235 |   |
| 236 |   |
| 237 | bool tImageJPG::Set(tPicture& picture, bool steal)  |
| 238 | {  |
| 239 | Clear();  |
| 240 | if (!picture.IsValid())  |
| 241 | return false;  |
| 242 |   |
| 243 | PixelFormatSrc = picture.PixelFormatSrc;  |
| 244 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 245 | // We don't know colour profile of tPicture.  |
| 246 |   |
| 247 | // This is worth some explanation. If steal is true the picture becomes invalid and the  |
| 248 | // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the  |
| 249 | // 'set' call will memcpy them out... which makes sure the picture is still valid after and  |
| 250 | // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must  |
| 251 | // succeed if picture was valid.  |
| 252 | tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels();  |
| 253 | bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal);  |
| 254 | tAssert(success);  |
| 255 | return true;  |
| 256 | }  |
| 257 |   |
| 258 |   |
| 259 | tFrame* tImageJPG::GetFrame(bool steal)  |
| 260 | {  |
| 261 | if (!Pixels)  |
| 262 | return nullptr;  |
| 263 |   |
| 264 | tFrame* frame = new tFrame();  |
| 265 | frame->PixelFormatSrc = PixelFormatSrc;  |
| 266 |   |
| 267 | if (steal)  |
| 268 | {  |
| 269 | frame->StealFrom(src: Pixels, width: Width, height: Height);  |
| 270 | Pixels = nullptr;  |
| 271 | }  |
| 272 | else  |
| 273 | {  |
| 274 | frame->Set(srcPixels: Pixels, width: Width, height: Height);  |
| 275 | }  |
| 276 |   |
| 277 | return frame;  |
| 278 | }  |
| 279 |   |
| 280 |   |
| 281 | void tImageJPG::Rotate90(bool antiClockwise)  |
| 282 | {  |
| 283 | tAssert((Width > 0) && (Height > 0) && Pixels);  |
| 284 | int newW = Height;  |
| 285 | int newH = Width;  |
| 286 | tPixel4b* newPixels = new tPixel4b[newW * newH];  |
| 287 |   |
| 288 | for (int y = 0; y < Height; y++)  |
| 289 | for (int x = 0; x < Width; x++)  |
| 290 | newPixels[ GetIndex(x: y, y: x, w: newW, h: newH) ] = Pixels[ GetIndex(x: antiClockwise ? x : Width-1-x, y: antiClockwise ? Height-1-y : y) ];  |
| 291 |   |
| 292 | ClearPixelData();  |
| 293 | Width = newW;  |
| 294 | Height = newH;  |
| 295 | Pixels = newPixels;  |
| 296 | }  |
| 297 |   |
| 298 |   |
| 299 | void tImageJPG::Flip(bool horizontal)  |
| 300 | {  |
| 301 | tAssert((Width > 0) && (Height > 0) && Pixels);  |
| 302 | int newW = Width;  |
| 303 | int newH = Height;  |
| 304 | tPixel4b* newPixels = new tPixel4b[newW * newH];  |
| 305 |   |
| 306 | for (int y = 0; y < Height; y++)  |
| 307 | for (int x = 0; x < Width; x++)  |
| 308 | newPixels[ GetIndex(x, y) ] = Pixels[ GetIndex(x: horizontal ? Width-1-x : x, y: horizontal ? y : Height-1-y) ];  |
| 309 |   |
| 310 | ClearPixelData();  |
| 311 | Width = newW;  |
| 312 | Height = newH;  |
| 313 | Pixels = newPixels;  |
| 314 | }  |
| 315 |   |
| 316 |   |
| 317 | bool tImageJPG::CanDoPerfectLosslessTransform(Transform trans) const  |
| 318 | {  |
| 319 | if (!MemImage || (MemImageSize <= 0))  |
| 320 | return false;  |
| 321 |   |
| 322 | tjhandle handle = tjInitTransform();  |
| 323 | if (!handle)  |
| 324 | return false;  |
| 325 |   |
| 326 | int width = 0;  |
| 327 | int height = 0;  |
| 328 | int subsamp = 0;  |
| 329 | tjDecompressHeader2(handle, jpegBuf: MemImage, jpegSize: MemImageSize, width: &width, height: &height, jpegSubsamp: &subsamp);  |
| 330 | tjDestroy(handle);  |
| 331 |   |
| 332 | tMath::tiClamp(val&: subsamp, min: 0, TJ_NUMSAMP-1);  |
| 333 | int mcuWidth = tjMCUWidth[subsamp];  |
| 334 | int mcuHeight = tjMCUHeight[subsamp];  |
| 335 | bool widthOK = (width % mcuWidth) == 0;  |
| 336 | bool heightOK = (height % mcuHeight) == 0;  |
| 337 |   |
| 338 | // Regardless of the transform, if both width and height are multiples of mcu size, we're ok.  |
| 339 | if (widthOK && heightOK)  |
| 340 | return true;  |
| 341 |   |
| 342 | switch (trans)  |
| 343 | {  |
| 344 | case Transform::Rotate90ACW:  |
| 345 | case Transform::FlipH:  |
| 346 | if (!widthOK)  |
| 347 | return false;  |
| 348 | break;  |
| 349 |   |
| 350 | case Transform::Rotate90CW:  |
| 351 | case Transform::FlipV:  |
| 352 | if (!heightOK)  |
| 353 | return false;  |
| 354 | break;  |
| 355 | }  |
| 356 |   |
| 357 | return true;  |
| 358 | }  |
| 359 |   |
| 360 |   |
| 361 | bool tImageJPG::LosslessTransform(Transform trans, bool allowImperfect)  |
| 362 | {  |
| 363 | if (!MemImage || (MemImageSize <= 0))  |
| 364 | return false;  |
| 365 |   |
| 366 | tjhandle handle = tjInitTransform();  |
| 367 | if (!handle)  |
| 368 | return false;  |
| 369 |   |
| 370 | TJXOP oper = TJXOP_NONE;  |
| 371 | switch (trans)  |
| 372 | {  |
| 373 | case Transform::Rotate90ACW: oper = TJXOP_ROT270; break;  |
| 374 | case Transform::Rotate90CW: oper = TJXOP_ROT90; break;  |
| 375 | case Transform::FlipH: oper = TJXOP_HFLIP; break;  |
| 376 | case Transform::FlipV: oper = TJXOP_VFLIP; break;  |
| 377 | }  |
| 378 | int options = allowImperfect ? TJXOPT_TRIM : TJXOPT_PERFECT;  |
| 379 |   |
| 380 | uint8* dstBufs[1];  |
| 381 | dstBufs[0] = nullptr;  |
| 382 | ulong dstSizes[1];  |
| 383 | dstSizes[0] = 0;  |
| 384 | tjtransform transforms[1];  |
| 385 | transforms[0].r.x = 0;  |
| 386 | transforms[0].r.y = 0;  |
| 387 | transforms[0].r.w = 0;  |
| 388 | transforms[0].r.h = 0;  |
| 389 | transforms[0].op = oper;  |
| 390 | transforms[0].options = options;  |
| 391 | transforms[0].data = nullptr;  |
| 392 | transforms[0].customFilter = nullptr;  |
| 393 | int flags = 0;  |
| 394 |   |
| 395 | int errorCode = tjTransform  |
| 396 | (  |
| 397 | handle, jpegBuf: MemImage, jpegSize: MemImageSize, n: 1,  |
| 398 | dstBufs, dstSizes, transforms, flags  |
| 399 | );  |
| 400 | if ((errorCode != 0) || (dstBufs[0] == nullptr) || (dstSizes[0] <= 0))  |
| 401 | {  |
| 402 | tjDestroy(handle);  |
| 403 | return false;  |
| 404 | }  |
| 405 |   |
| 406 | // Success. Simply hand the buffer over to the MemImage.  |
| 407 | if (MemImage)  |
| 408 | tjFree(buffer: MemImage);  |
| 409 | MemImage = dstBufs[0];  |
| 410 | MemImageSize = dstSizes[0];  |
| 411 |   |
| 412 | tjDestroy(handle);  |
| 413 | return true;  |
| 414 | }  |
| 415 |   |
| 416 |   |
| 417 | bool tImageJPG::PopulateMetaData(const uint8* jpgFileInMemory, int numBytes)  |
| 418 | {  |
| 419 | tAssert(jpgFileInMemory && (numBytes > 0));  |
| 420 | MetaData.Set(rawJpgImageData: jpgFileInMemory, numBytes);  |
| 421 | return MetaData.IsValid();  |
| 422 | }  |
| 423 |   |
| 424 |   |
| 425 | bool tImageJPG::Save(const tString& jpgFile, int quality) const  |
| 426 | {  |
| 427 | SaveParams params;  |
| 428 | params.Quality = quality;  |
| 429 | return Save(jpgFile, params);  |
| 430 | }  |
| 431 |   |
| 432 |   |
| 433 | bool tImageJPG::Save(const tString& jpgFile, const SaveParams& params) const  |
| 434 | {  |
| 435 | if (!IsValid())  |
| 436 | return false;  |
| 437 |   |
| 438 | if (tSystem::tGetFileType(file: jpgFile) != tSystem::tFileType::JPG)  |
| 439 | return false;  |
| 440 |   |
| 441 | // If we're a simple memory image, just save the bytes to disk and we're done.  |
| 442 | if (MemImage && (MemImageSize > 0))  |
| 443 | {  |
| 444 | tFileHandle fileHandle = tOpenFile(file: jpgFile, mode: "wb" );  |
| 445 | if (!fileHandle)  |
| 446 | return false;  |
| 447 | int numWritten = tWriteFile(handle: fileHandle, buffer: MemImage, sizeBytes: MemImageSize);  |
| 448 | tCloseFile(f: fileHandle);  |
| 449 | return (numWritten == MemImageSize);  |
| 450 | }  |
| 451 |   |
| 452 | tjhandle tjInstance = tjInitCompress();  |
| 453 | if (!tjInstance)  |
| 454 | return false;  |
| 455 |   |
| 456 | uint8* jpegBuf = nullptr;  |
| 457 | ulong jpegSize = 0;  |
| 458 |   |
| 459 | int flags = 0;  |
| 460 | flags |= TJFLAG_BOTTOMUP;  |
| 461 | //flags |= TJFLAG_FASTUPSAMPLE;  |
| 462 | //flags |= TJFLAG_FASTDCT;  |
| 463 | flags |= TJFLAG_ACCURATEDCT;  |
| 464 |   |
| 465 | int compResult = tjCompress2(handle: tjInstance, srcBuf: (uint8*)Pixels, width: Width, pitch: 0, height: Height, pixelFormat: TJPF_RGBA,  |
| 466 | jpegBuf: &jpegBuf, jpegSize: &jpegSize, jpegSubsamp: TJSAMP_444, jpegQual: params.Quality, flags);  |
| 467 |   |
| 468 | tjDestroy(handle: tjInstance);  |
| 469 | if (compResult < 0)  |
| 470 | {  |
| 471 | tjFree(buffer: jpegBuf);  |
| 472 | return false;  |
| 473 | }  |
| 474 |   |
| 475 | tFileHandle fileHandle = tOpenFile(file: jpgFile.Chars(), mode: "wb" );  |
| 476 | if (!fileHandle)  |
| 477 | {  |
| 478 | tjFree(buffer: jpegBuf);  |
| 479 | return false;  |
| 480 | }  |
| 481 | bool success = tWriteFile(handle: fileHandle, buffer: jpegBuf, sizeBytes: jpegSize);  |
| 482 | tCloseFile(f: fileHandle);  |
| 483 | tjFree(buffer: jpegBuf);  |
| 484 |   |
| 485 | return success;  |
| 486 | }  |
| 487 |   |
| 488 |   |
| 489 | tPixel4b* tImageJPG::StealPixels()  |
| 490 | {  |
| 491 | tPixel4b* pixels = Pixels;  |
| 492 | Pixels = nullptr;  |
| 493 | Width = 0;  |
| 494 | Height = 0;  |
| 495 | return pixels;  |
| 496 | }  |
| 497 |   |
| 498 |   |
| 499 | }  |
| 500 | |