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 
24using namespace tSystem
25namespace tImage 
26
27 
28 
29void 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 
43bool 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 
66bool 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 headerResult = 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 
190bool 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 
217bool 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 
237bool 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 
259tFrame* 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 
281void 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 
299void 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 
317bool 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 
361bool 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 
417bool 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 
425bool tImageJPG::Save(const tString& jpgFile, int quality) const 
426
427 SaveParams params
428 params.Quality = quality
429 return Save(jpgFile, params); 
430
431 
432 
433bool 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 
489tPixel4b* tImageJPG::StealPixels() 
490
491 tPixel4b* pixels = Pixels
492 Pixels = nullptr
493 Width = 0
494 Height = 0
495 return pixels
496
497 
498 
499
500