1// tImagePKM.cpp 
2// 
3// This class knows how to load and save Ericsson's ETC1/ETC2/EAC PKM (.pkm) files. The pixel data is stored in a 
4// tLayer. If decode was requested the layer will store raw pixel data. The layer may be 'stolen'. IF it is the 
5// tImagePKM is invalid afterwards. This is purely for performance. 
6// 
7// Copyright (c) 2023, 2024 Tristan Grimmer. 
8// Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby 
9// granted, provided that the above copyright notice and this permission notice appear in all copies. 
10// 
11// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 
12// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 
13// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 
14// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 
15// PERFORMANCE OF THIS SOFTWARE. 
16 
17#include <System/tFile.h> 
18#include "Image/tImagePKM.h" 
19#include "Image/tPixelUtil.h" 
20#include "Image/tPicture.h" 
21#include "etcdec/etcdec.h" 
22using namespace tSystem
23namespace tImage 
24
25 
26 
27namespace tPKM 
28
29 #pragma pack(push, 1) 
30 struct Header 
31
32 char FourCCMagic[4]; // PKM files should have 'P', 'K', 'M', ' ' as the first four characters. 
33 char Version[2]; // Will be '1', '0' for ETC1 and '2', '0' for ETC2. 
34 uint8 FormatMSB; // The header is big endian. This is the MSB of the format. 
35 uint8 FormatLSB
36 uint8 EncodedWidthMSB; // This is the width in terms of number of 4x4 blocks used. It is always divisible by 4. 
37 uint8 EncodedWidthLSB
38 uint8 EncodedHeightMSB; // This is the height in terms of number of 4x4 blocks used. It is always divisible by 4. 
39 uint8 EncodedHeightLSB
40 uint8 WidthMSB; // This is the 'real' image width. Any value >= 1 works. 
41 uint8 WidthLSB
42 uint8 HeightMSB; // This is the 'real' image height. Any value >= 1 works. 
43 uint8 HeightLSB
44 
45 int GetVersion() const { return (Version[0] == '1') ? 1 : 2; } 
46 uint32 GetFormat() const { return (FormatMSB << 8) | FormatLSB; } 
47 uint32 GetEncodedWidth() const { return (EncodedWidthMSB << 8) | EncodedWidthLSB; } 
48 uint32 GetEncodedHeight() const { return (EncodedHeightMSB << 8) | EncodedHeightLSB; } 
49 uint32 GetWidth() const { return (WidthMSB << 8) | WidthLSB; } 
50 uint32 GetHeight() const { return (HeightMSB << 8) | HeightLSB; } 
51 }; 
52 #pragma pack(pop) 
53 
54 // Format codes. These are what will be found in the pkm header. The corresponding OpenGL texture format ID 
55 // is listed next to each one. 
56 // Note1: ETC1 pkm files should assume ETC1_RGB8 even if the format is not set to that. 
57 // Note2: The sRGB formats are decoded the same as the non-sRGB formats. It is only the interpretation 
58 // of the pixel values that changes. 
59 // Note3: ETC1_RGB8 and ETC2_RGB8 and ETC2_sRGB8 are all decoded with the same RGB decode. This is 
60 // because ETC2 is backwards compatible with ETC1. 
61 enum class PKMFMT 
62
63 ETC1_RGB, // GL_ETC1_RGB8_OES. OES just means the format internal ID was developed by the working group (Kronos I assume). 
64 ETC2_RGB, // GL_COMPRESSED_RGB8_ETC2. 
65 ETC2_RGBA_OLD, // GL_COMPRESSED_RGBA8_ETC2_EAC. Should not be encountered. Interpret as RGBA if it is. 
66 ETC2_RGBA, // GL_COMPRESSED_RGBA8_ETC2_EAC. 
67 ETC2_RGBA1, // GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2. 
68 ETC2_R, // GL_COMPRESSED_R11_EAC. 
69 ETC2_RG, // GL_COMPRESSED_RG11_EAC. 
70 ETC2_R_SIGNED, // GL_COMPRESSED_SIGNED_R11_EAC. 
71 ETC2_RG_SIGNED, // GL_COMPRESSED_SIGNED_RG11_EAC. 
72 ETC2_sRGB, // GL_COMPRESSED_SRGB8_ETC2. 
73 ETC2_sRGBA, // GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC. 
74 ETC2_sRGBA1 // GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2. 
75 }; 
76 
77 bool IsHeaderValid(const Header&); 
78 
79 // These figure out the pixel format and the colour-profile. tPixelFormat does not specify ancilllary 
80 // properties of the data -- it specified the encoding of the data. The extra information, like the colour-space it 
81 // was authored in, is stored in tColourProfile. In many cases this satellite information cannot be determined, in 
82 // which case colour-profile will be set to their 'unspecified' enumerant. 
83 void GetFormatInfo_FromPKMFormat(tPixelFormat&, tColourProfile&, uint32 pkmFmt, int version); 
84
85 
86 
87void tPKM::GetFormatInfo_FromPKMFormat(tPixelFormat& fmt, tColourProfile& pro, uint32 pkmFmt, int version
88
89 fmt = tPixelFormat::Invalid
90 pro = tColourProfile::sRGB
91 switch (PKMFMT(pkmFmt)) 
92
93 case PKMFMT::ETC1_RGB: fmt = tPixelFormat::ETC1; pro = tColourProfile::sRGB; break
94 case PKMFMT::ETC2_RGB: fmt = tPixelFormat::ETC2RGB; pro = tColourProfile::lRGB; break
95 case PKMFMT::ETC2_sRGB: fmt = tPixelFormat::ETC2RGB; pro = tColourProfile::sRGB; break
96 case PKMFMT::ETC2_RGBA_OLD
97 case PKMFMT::ETC2_RGBA: fmt = tPixelFormat::ETC2RGBA; pro = tColourProfile::lRGB; break
98 case PKMFMT::ETC2_sRGBA: fmt = tPixelFormat::ETC2RGBA; pro = tColourProfile::sRGB; break
99 case PKMFMT::ETC2_RGBA1: fmt = tPixelFormat::ETC2RGBA1; pro = tColourProfile::lRGB; break
100 case PKMFMT::ETC2_sRGBA1: fmt = tPixelFormat::ETC2RGBA1; pro = tColourProfile::sRGB; break
101 case PKMFMT::ETC2_R: fmt = tPixelFormat::EACR11U; pro = tColourProfile::sRGB; break
102 case PKMFMT::ETC2_RG: fmt = tPixelFormat::EACRG11U; pro = tColourProfile::sRGB; break
103 case PKMFMT::ETC2_R_SIGNED: fmt = tPixelFormat::EACR11S; pro = tColourProfile::sRGB; break
104 case PKMFMT::ETC2_RG_SIGNED:fmt = tPixelFormat::EACRG11S; pro = tColourProfile::sRGB; break
105
106 
107 // If the format is still invalid we encountered an invalid format in the PKM header. 
108 // In this case we base the format on the header version number only. 
109 if (fmt == tPixelFormat::Invalid
110
111 pro = tColourProfile::sRGB
112 if (version == 2
113 fmt = tPixelFormat::ETC2RGB
114 else 
115 fmt = tPixelFormat::ETC1
116
117
118 
119 
120bool tPKM::IsHeaderValid(const Header& header
121
122 if ((header.FourCCMagic[0] != 'P') || (header.FourCCMagic[1] != 'K') || (header.FourCCMagic[2] != 'M') || (header.FourCCMagic[3] != ' ')) 
123 return false
124 
125 uint32 format = header.GetFormat(); 
126 uint32 encodedWidth = header.GetEncodedWidth(); 
127 uint32 encodedHeight = header.GetEncodedHeight(); 
128 uint32 width = header.GetWidth(); 
129 uint32 height = header.GetHeight(); 
130 
131 // I'm not sure why the header stores the encrypted sizes as they can be computed from 
132 // the width and height. They can, however, be used for validation. 
133 int blocksW = tImage::tGetNumBlocks(blockWH: 4, imageWH: width); 
134 if (blocksW*4 != encodedWidth
135 return false
136 
137 int blocksH = tImage::tGetNumBlocks(blockWH: 4, imageWH: height); 
138 if (blocksH*4 != encodedHeight
139 return false
140 
141 return true
142
143 
144 
145bool tImagePKM::Load(const tString& pkmFile, const LoadParams& params
146
147 Clear(); 
148 
149 if (tSystem::tGetFileType(file: pkmFile) != tSystem::tFileType::PKM
150 return false
151 
152 if (!tFileExists(file: pkmFile)) 
153 return false
154 
155 int numBytes = 0
156 uint8* pkmFileInMemory = tLoadFile(file: pkmFile, buffer: nullptr, fileSize: &numBytes); 
157 bool success = Load(pkmFileInMemory, numBytes, params); 
158 delete[] pkmFileInMemory
159 
160 return success
161
162 
163 
164bool tImagePKM::Load(const uint8* pkmFileInMemory, int numBytes, const LoadParams& paramsIn
165
166 Clear(); 
167 if ((numBytes <= 0) || !pkmFileInMemory
168 return false
169 
170 const tPKM::Header* header = (const tPKM::Header*)pkmFileInMemory
171 bool valid = tPKM::IsHeaderValid(header: *header); 
172 if (!valid
173 return false
174 
175 int width = header->GetWidth(); 
176 int height = header->GetHeight(); 
177 if ((width <= 0) || (height <= 0)) 
178 return false
179 
180 tPixelFormat format
181 tColourProfile profile
182 tPKM::GetFormatInfo_FromPKMFormat(fmt&: format, pro&: profile, pkmFmt: header->GetFormat(), version: header->GetVersion()); 
183 if (!tIsBCFormat(format)) 
184 return false
185 
186 PixelFormat = format
187 PixelFormatSrc = format
188 ColourProfile = profile
189 ColourProfileSrc = profile
190 
191 const uint8* pkmData = pkmFileInMemory + sizeof(tPKM::Header); 
192 int pkmDataSize = numBytes - sizeof(tPKM::Header); 
193 tAssert(!Layer); 
194 LoadParams params(paramsIn); 
195 
196 // If we were not asked to decode we just get the data over to the Layer and we're done. 
197 if (!(params.Flags & LoadFlag_Decode)) 
198
199 Layer = new tLayer(PixelFormatSrc, width, height, (uint8*)pkmData); 
200 return true
201
202 
203 // Decode to 32-bit RGBA. 
204 // Spread only applies to the single-channel (R-only) format. 
205 bool spread = params.Flags & LoadFlag_SpreadLuminance
206 
207 // If the gamma mode is auto, we determine here whether to apply sRGB compression. 
208 // If the space is linear and a format that often encodes colours, we apply it. 
209 if (params.Flags & LoadFlag_AutoGamma
210
211 // Clear all related flags. 
212 params.Flags &= ~(LoadFlag_AutoGamma | LoadFlag_SRGBCompression | LoadFlag_GammaCompression); 
213 if (tMath::tIsProfileLinearInRGB(profile: ColourProfileSrc)) 
214 params.Flags |= LoadFlag_SRGBCompression
215
216 
217 tColour4f* decoded4f = nullptr
218 tColour4b* decoded4b = nullptr
219 DecodeResult result = tImage::DecodePixelData_Block(format, data: (uint8*)pkmData, dataSize: pkmDataSize, w: width, h: height, decoded4b, decoded4f); 
220 if (result != DecodeResult::Success
221 return false
222 
223 // Apply any decode flags. 
224 tAssert(decoded4f || decoded4b); 
225 bool flagSRGB = (params.Flags & tImagePKM::LoadFlag_SRGBCompression) ? true : false
226 bool flagGama = (params.Flags & tImagePKM::LoadFlag_GammaCompression)? true : false
227 if (decoded4f && (flagSRGB || flagGama)) 
228
229 for (int p = 0; p < width*height; p++) 
230
231 tColour4f& colour = decoded4f[p]; 
232 if (flagSRGB
233 colour.LinearToSRGB(chans: tCompBit_RGB); 
234 if (flagGama
235 colour.LinearToGamma(gamma: params.Gamma, chans: tCompBit_RGB); 
236
237
238 if (decoded4b && (flagSRGB || flagGama)) 
239
240 for (int p = 0; p < width*height; p++) 
241
242 tColour4f colour(decoded4b[p]); 
243 if (flagSRGB
244 colour.LinearToSRGB(chans: tCompBit_RGB); 
245 if (flagGama
246 colour.LinearToGamma(gamma: params.Gamma, chans: tCompBit_RGB); 
247 decoded4b[p].SetR(colour.R); 
248 decoded4b[p].SetG(colour.G); 
249 decoded4b[p].SetB(colour.B); 
250
251
252 
253 // Converts to RGBA32 into the decoded4b array. 
254 if (decoded4f
255
256 tAssert(!decoded4b); 
257 decoded4b = new tColour4b[width*height]; 
258 for (int p = 0; p < width*height; p++) 
259 decoded4b[p].Set(decoded4f[p]); 
260 delete[] decoded4f
261
262 
263 // Possibly spread the L/Red channel. 
264 if (spread && tIsLuminanceFormat(format: PixelFormatSrc)) 
265
266 for (int p = 0; p < width*height; p++) 
267
268 decoded4b[p].G = decoded4b[p].R
269 decoded4b[p].B = decoded4b[p].R
270
271
272 
273 // All images decoded. Can now set the object's pixel format. We do _not_ set the PixelFormatSrc here! 
274 PixelFormat = tPixelFormat::R8G8B8A8
275 
276 // Give decoded pixelData to layer. 
277 tAssert(!Layer); 
278 Layer = new tLayer(PixelFormat, width, height, (uint8*)decoded4b, true); 
279 tAssert(Layer->OwnsData); 
280 
281 // We've got one more chance to reverse the rows here (if we still need to) because we were asked to decode. 
282 if (params.Flags & tImagePKM::LoadFlag_ReverseRowOrder
283
284 // This shouldn't ever fail. Too easy to reverse RGBA 32-bit. 
285 uint8* reversedRowData = tImage::CreateReversedRowData(pixelData: Layer->Data, pixelDataFormat: Layer->PixelFormat, numBlocksW: width, numBlocksH: height); 
286 tAssert(reversedRowData); 
287 delete[] Layer->Data
288 Layer->Data = reversedRowData
289
290 
291 // Maybe update the current colour profile. 
292 if (params.Flags & LoadFlag_SRGBCompression) ColourProfile = tColourProfile::sRGB
293 if (params.Flags & LoadFlag_GammaCompression) ColourProfile = tColourProfile::gRGB
294 
295 // Finally update the current pixel format -- but not the source format. 
296 tAssert(IsValid()); 
297 return true
298
299 
300 
301bool tImagePKM::Set(tPixel4b* pixels, int width, int height, bool steal
302
303 Clear(); 
304 if (!pixels || (width <= 0) || (height <= 0)) 
305 return false
306 
307 Layer = new tLayer(tPixelFormat::R8G8B8A8, width, height, (uint8*)pixels, steal); 
308 
309 PixelFormatSrc = tPixelFormat::R8G8B8A8
310 PixelFormat = tPixelFormat::R8G8B8A8
311 ColourProfileSrc = tColourProfile::sRGB; // We assume pixels must be sRGB. 
312 ColourProfile = tColourProfile::sRGB
313 
314 return true
315
316 
317 
318bool tImagePKM::Set(tFrame* frame, bool steal
319
320 Clear(); 
321 if (!frame || !frame->IsValid()) 
322 return false
323 
324 PixelFormatSrc = frame->PixelFormatSrc
325 PixelFormat = tPixelFormat::R8G8B8A8
326 ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB. 
327 ColourProfile = tColourProfile::sRGB
328 
329 Set(pixels: frame->GetPixels(steal), width: frame->Width, height: frame->Height, steal); 
330 if (steal
331 delete frame
332 
333 return true
334
335 
336 
337bool tImagePKM::Set(tPicture& picture, bool steal
338
339 Clear(); 
340 if (!picture.IsValid()) 
341 return false
342 
343 PixelFormatSrc = picture.PixelFormatSrc
344 PixelFormat = tPixelFormat::R8G8B8A8
345 // We don't know colour profile of tPicture. 
346 
347 // This is worth some explanation. If steal is true the picture becomes invalid and the 
348 // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the 
349 // 'set' call will memcpy them out... which makes sure the picture is still valid after and 
350 // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must 
351 // succeed if picture was valid. 
352 tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels(); 
353 bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal); 
354 tAssert(success); 
355 return true
356
357 
358 
359tFrame* tImagePKM::GetFrame(bool steal
360
361 // Data must be decoded for this to work. 
362 if (!IsValid() || (PixelFormat != tPixelFormat::R8G8B8A8)) 
363 return nullptr
364 
365 tFrame* frame = new tFrame(); 
366 frame->Width = Layer->Width
367 frame->Height = Layer->Height
368 frame->PixelFormatSrc = PixelFormatSrc
369 
370 if (steal
371
372 frame->Pixels = (tPixel4b*)Layer->StealData(); 
373 delete Layer
374 Layer = nullptr
375
376 else 
377
378 frame->Pixels = new tPixel4b[frame->Width * frame->Height]; 
379 tStd::tMemcpy(dest: frame->Pixels, src: (tPixel4b*)Layer->Data, numBytes: frame->Width * frame->Height * sizeof(tPixel4b)); 
380
381 
382 return frame
383
384 
385 
386bool tImagePKM::IsOpaque() const 
387
388 if (!IsValid()) 
389 return false
390  
391 switch (Layer->PixelFormat
392
393 case tPixelFormat::R8G8B8A8
394
395 tPixel4b* pixels = (tPixel4b*)Layer->Data
396 for (int p = 0; p < (Layer->Width * Layer->Height); p++) 
397
398 if (pixels[p].A < 255
399 return false
400
401 break
402
403 
404 case tPixelFormat::EACR11U
405 case tPixelFormat::EACR11S
406 case tPixelFormat::EACRG11U
407 case tPixelFormat::EACRG11S
408 case tPixelFormat::ETC1
409 case tPixelFormat::ETC2RGB
410 return true
411 
412 case tPixelFormat::ETC2RGBA1
413 case tPixelFormat::ETC2RGBA
414 return false
415
416 
417 return true
418
419 
420 
421
422