| 1 | // tImageQOI.cpp  |
| 2 | //  |
| 3 | // This class knows how to load and save Quite OK Images (.qoi) files into tPixel arrays. These tPixels may be 'stolen'  |
| 4 | // by the tPicture's constructor if a targa file is specified. After the array is stolen the tImageQOI is invalid. This  |
| 5 | // is purely for performance.  |
| 6 | //  |
| 7 | // Copyright (c) 2022-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/tImageQOI.h"  |
| 19 | #include "Image/tPicture.h"  |
| 20 | #define QOI_NO_STDIO  |
| 21 | #define QOI_IMPLEMENTATION  |
| 22 | #include <QOI/qoi.h>  |
| 23 | namespace tImage  |
| 24 | {  |
| 25 |   |
| 26 |   |
| 27 | bool tImageQOI::Load(const tString& qoiFile)  |
| 28 | {  |
| 29 | Clear();  |
| 30 |   |
| 31 | if (tSystem::tGetFileType(file: qoiFile) != tSystem::tFileType::QOI)  |
| 32 | return false;  |
| 33 |   |
| 34 | if (!tSystem::tFileExists(file: qoiFile))  |
| 35 | return false;  |
| 36 |   |
| 37 | int numBytes = 0;  |
| 38 | uint8* qoiFileInMemory = tSystem::tLoadFile(file: qoiFile, buffer: nullptr, fileSize: &numBytes);  |
| 39 | bool success = Load(qoiFileInMemory, numBytes);  |
| 40 | delete[] qoiFileInMemory;  |
| 41 |   |
| 42 | return success;  |
| 43 | }  |
| 44 |   |
| 45 |   |
| 46 | bool tImageQOI::Load(const uint8* qoiFileInMemory, int numBytes)  |
| 47 | {  |
| 48 | Clear();  |
| 49 | if ((numBytes <= 0) || !qoiFileInMemory)  |
| 50 | return false;  |
| 51 |   |
| 52 | // Decode a QOI image from memory. The function either returns NULL on failure (invalid parameters or malloc failed)  |
| 53 | // or a pointer to the decoded pixels. On success, the qoi_desc struct is filled with the description from the file  |
| 54 | // header. The returned pixel data should be free()d after use.  |
| 55 | qoi_desc results;  |
| 56 | void* reversedPixels = qoi_decode(data: qoiFileInMemory, size: numBytes, desc: &results, channels: 4);  |
| 57 | if (!reversedPixels)  |
| 58 | return false;  |
| 59 |   |
| 60 | Width = results.width;   |
| 61 | Height = results.height;  |
| 62 | PixelFormatSrc = (results.channels == 3) ? tPixelFormat::R8G8B8 : tPixelFormat::R8G8B8A8;  |
| 63 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 64 | ColourProfileSrc = (results.colorspace == QOI_LINEAR) ? tColourProfile::lRGB : tColourProfile::sRGB;  |
| 65 | ColourProfile = ColourProfileSrc;  |
| 66 | tAssert((Width > 0) && (Height > 0));  |
| 67 |   |
| 68 | // Reverse rows.  |
| 69 | Pixels = new tPixel4b[Width*Height];  |
| 70 | int bytesPerRow = Width*4;  |
| 71 | for (int y = Height-1; y >= 0; y--)  |
| 72 | tStd::tMemcpy(dest: (uint8*)Pixels + ((Height-1)-y)*bytesPerRow, src: (uint8*)reversedPixels + y*bytesPerRow, numBytes: bytesPerRow);  |
| 73 | free(ptr: reversedPixels);  |
| 74 |   |
| 75 | return true;  |
| 76 | }  |
| 77 |   |
| 78 |   |
| 79 | bool tImageQOI::Set(tPixel4b* pixels, int width, int height, bool steal)  |
| 80 | {  |
| 81 | Clear();  |
| 82 | if (!pixels || (width <= 0) || (height <= 0))  |
| 83 | return false;  |
| 84 |   |
| 85 | Width = width;  |
| 86 | Height = height;  |
| 87 |   |
| 88 | if (steal)  |
| 89 | {  |
| 90 | Pixels = pixels;  |
| 91 | }  |
| 92 | else  |
| 93 | {  |
| 94 | Pixels = new tPixel4b[Width*Height];  |
| 95 | tStd::tMemcpy(dest: Pixels, src: pixels, numBytes: Width*Height*sizeof(tPixel4b));  |
| 96 | }  |
| 97 |   |
| 98 | PixelFormatSrc = tPixelFormat::R8G8B8A8;  |
| 99 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 100 | ColourProfileSrc = tColourProfile::sRGB; // We assume pixels must be sRGB.  |
| 101 | ColourProfile = tColourProfile::sRGB;  |
| 102 |   |
| 103 | return true;  |
| 104 | }  |
| 105 |   |
| 106 |   |
| 107 | bool tImageQOI::Set(tFrame* frame, bool steal)  |
| 108 | {  |
| 109 | Clear();  |
| 110 | if (!frame || !frame->IsValid())  |
| 111 | return false;  |
| 112 |   |
| 113 | PixelFormatSrc = frame->PixelFormatSrc;  |
| 114 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 115 | ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB.  |
| 116 | ColourProfile = tColourProfile::sRGB;  |
| 117 |   |
| 118 | Set(pixels: frame->GetPixels(steal), width: frame->Width, height: frame->Height, steal);  |
| 119 | if (steal)  |
| 120 | delete frame;  |
| 121 |   |
| 122 | return true;  |
| 123 | }  |
| 124 |   |
| 125 |   |
| 126 | bool tImageQOI::Set(tPicture& picture, bool steal)  |
| 127 | {  |
| 128 | Clear();  |
| 129 | if (!picture.IsValid())  |
| 130 | return false;  |
| 131 |   |
| 132 | PixelFormatSrc = picture.PixelFormatSrc;  |
| 133 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 134 | // We don't know colour profile of tPicture.  |
| 135 |   |
| 136 | // This is worth some explanation. If steal is true the picture becomes invalid and the  |
| 137 | // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the  |
| 138 | // 'set' call will memcpy them out... which makes sure the picture is still valid after and  |
| 139 | // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must  |
| 140 | // succeed if picture was valid.  |
| 141 | tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels();  |
| 142 | bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal);  |
| 143 | tAssert(success);  |
| 144 | return true;  |
| 145 | }  |
| 146 |   |
| 147 |   |
| 148 | tFrame* tImageQOI::GetFrame(bool steal)  |
| 149 | {  |
| 150 | if (!IsValid())  |
| 151 | return nullptr;  |
| 152 |   |
| 153 | tFrame* frame = new tFrame();  |
| 154 | frame->PixelFormatSrc = PixelFormatSrc;  |
| 155 |   |
| 156 | if (steal)  |
| 157 | {  |
| 158 | frame->StealFrom(src: Pixels, width: Width, height: Height);  |
| 159 | Pixels = nullptr;  |
| 160 | }  |
| 161 | else  |
| 162 | {  |
| 163 | frame->Set(srcPixels: Pixels, width: Width, height: Height);  |
| 164 | }  |
| 165 |   |
| 166 | return frame;  |
| 167 | }  |
| 168 |   |
| 169 |   |
| 170 | tImageQOI::tFormat tImageQOI::Save(const tString& qoiFile, tFormat format, tColourProfile profile) const  |
| 171 | {  |
| 172 | SaveParams params;  |
| 173 | params.Format = format;  |
| 174 | params.ColourProfile = profile;  |
| 175 | return Save(qoiFile, params);  |
| 176 | }  |
| 177 |   |
| 178 |   |
| 179 | tImageQOI::tFormat tImageQOI::Save(const tString& qoiFile, const SaveParams& params) const  |
| 180 | {  |
| 181 | tFormat format = params.Format;  |
| 182 | tColourProfile profile = params.ColourProfile;  |
| 183 | if (!IsValid() || (format == tFormat::Invalid))  |
| 184 | return tFormat::Invalid;  |
| 185 |   |
| 186 | if (tSystem::tGetFileType(file: qoiFile) != tSystem::tFileType::QOI)  |
| 187 | return tFormat::Invalid;  |
| 188 |   |
| 189 | if (format == tFormat::Auto)  |
| 190 | {  |
| 191 | if (IsOpaque())  |
| 192 | format = tFormat::BPP24;  |
| 193 | else  |
| 194 | format = tFormat::BPP32;  |
| 195 | }  |
| 196 | if (profile == tColourProfile::Auto)  |
| 197 | profile = ColourProfileSrc;  |
| 198 |   |
| 199 | tFileHandle file = tSystem::tOpenFile(file: qoiFile.Chr(), mode: "wb" );  |
| 200 | if (!file)  |
| 201 | return tFormat::Invalid;  |
| 202 |   |
| 203 | qoi_desc qoiDesc;  |
| 204 | qoiDesc.channels = (format == tFormat::BPP24) ? 3 : 4;  |
| 205 |   |
| 206 | // This also catches space being set to invalid. Basically if it's not linear, it's sRGB.  |
| 207 | qoiDesc.colorspace = (profile == tColourProfile::lRGB) ? QOI_LINEAR : QOI_SRGB;  |
| 208 | qoiDesc.height = Height;  |
| 209 | qoiDesc.width = Width;  |
| 210 |   |
| 211 | // No matter the format, we need to reverse the rows before saving.  |
| 212 | tPixel4b* reversedRows = new tPixel4b[Width*Height];  |
| 213 | int bytesPerRow = Width*4;  |
| 214 | for (int y = Height-1; y >= 0; y--)  |
| 215 | tStd::tMemcpy(dest: (uint8*)reversedRows + ((Height-1)-y)*bytesPerRow, src: (uint8*)Pixels + y*bytesPerRow, numBytes: bytesPerRow);  |
| 216 |   |
| 217 | // If we're saving in 24bit we need to convert our source data to 24bit.  |
| 218 | uint8* pixels = (uint8*)reversedRows;  |
| 219 | bool deletePixels = false;  |
| 220 | if (format == tFormat::BPP24)  |
| 221 | {  |
| 222 | int numPixels = Width*Height;  |
| 223 | pixels = new uint8[numPixels*3];  |
| 224 | for (int p = 0; p < numPixels; p++)  |
| 225 | {  |
| 226 | pixels[p*3 + 0] = reversedRows[p].R;  |
| 227 | pixels[p*3 + 1] = reversedRows[p].G;  |
| 228 | pixels[p*3 + 2] = reversedRows[p].B;  |
| 229 | }  |
| 230 | deletePixels = true;  |
| 231 | }  |
| 232 |   |
| 233 | // Encode raw RGB or RGBA pixels into a QOI image in memory. The function either returns NULL on failure (invalid  |
| 234 | // parameters or malloc failed) or a pointer to the encoded data on success. On success the out_len is set to the  |
| 235 | // size in bytes of the encoded data. The returned qoi data should be free()d after use.  |
| 236 | int outLength = 0;  |
| 237 | void* memImage = qoi_encode(data: pixels, desc: &qoiDesc, out_len: &outLength);  |
| 238 | if (deletePixels)  |
| 239 | delete[] pixels;  |
| 240 | delete[] reversedRows;  |
| 241 |   |
| 242 | if (!memImage)  |
| 243 | return tFormat::Invalid;  |
| 244 |   |
| 245 | tAssert(outLength);  |
| 246 | int numWritten = tSystem::tWriteFile(handle: file, buffer: memImage, sizeBytes: outLength);  |
| 247 | tSystem::tCloseFile(f: file);  |
| 248 | free(ptr: memImage);  |
| 249 |   |
| 250 | if (numWritten != outLength)  |
| 251 | return tFormat::Invalid;  |
| 252 |   |
| 253 | return format;  |
| 254 | }  |
| 255 |   |
| 256 |   |
| 257 | bool tImageQOI::IsOpaque() const  |
| 258 | {  |
| 259 | for (int p = 0; p < (Width*Height); p++)  |
| 260 | {  |
| 261 | if (Pixels[p].A < 255)  |
| 262 | return false;  |
| 263 | }  |
| 264 |   |
| 265 | return true;  |
| 266 | }  |
| 267 |   |
| 268 |   |
| 269 | tPixel4b* tImageQOI::StealPixels()  |
| 270 | {  |
| 271 | tPixel4b* pixels = Pixels;  |
| 272 | Pixels = nullptr;  |
| 273 | Width = 0;  |
| 274 | Height = 0;  |
| 275 |   |
| 276 | return pixels;  |
| 277 | }  |
| 278 |   |
| 279 |   |
| 280 | }  |
| 281 | |