| 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"  |
| 22 | using namespace tSystem;  |
| 23 | namespace tImage  |
| 24 | {  |
| 25 |   |
| 26 |   |
| 27 | namespace tPKM  |
| 28 | {  |
| 29 | #pragma pack(push, 1)  |
| 30 | struct   |
| 31 | {  |
| 32 | char [4]; // PKM files should have 'P', 'K', 'M', ' ' as the first four characters.  |
| 33 | char [2]; // Will be '1', '0' for ETC1 and '2', '0' for ETC2.  |
| 34 | uint8 ; // The header is big endian. This is the MSB of the format.  |
| 35 | uint8 ;  |
| 36 | uint8 ; // This is the width in terms of number of 4x4 blocks used. It is always divisible by 4.  |
| 37 | uint8 ;  |
| 38 | uint8 ; // This is the height in terms of number of 4x4 blocks used. It is always divisible by 4.  |
| 39 | uint8 ;  |
| 40 | uint8 ; // This is the 'real' image width. Any value >= 1 works.  |
| 41 | uint8 ;  |
| 42 | uint8 ; // This is the 'real' image height. Any value >= 1 works.  |
| 43 | uint8 ;  |
| 44 |   |
| 45 | int () const { return (Version[0] == '1') ? 1 : 2; }  |
| 46 | uint32 () const { return (FormatMSB << 8) | FormatLSB; }  |
| 47 | uint32 () const { return (EncodedWidthMSB << 8) | EncodedWidthLSB; }  |
| 48 | uint32 () const { return (EncodedHeightMSB << 8) | EncodedHeightLSB; }  |
| 49 | uint32 () const { return (WidthMSB << 8) | WidthLSB; }  |
| 50 | uint32 () 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 |   |
| 87 | void 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 |   |
| 120 | bool tPKM::(const 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 |   |
| 145 | bool 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 |   |
| 164 | bool 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* = (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 |   |
| 301 | bool 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 |   |
| 318 | bool 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 |   |
| 337 | bool 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 |   |
| 359 | tFrame* 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 |   |
| 386 | bool 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 | |