| 1 | // tImageTIFF.cpp  |
| 2 | //  |
| 3 | // This knows how to load/save TIFFs. It knows the details of the tiff file format and loads the data into multiple  |
| 4 | // tPixel arrays, one for each frame (in a TIFF thay are called pages). These arrays may be 'stolen' by tPictures.  |
| 5 | //  |
| 6 | // Copyright (c) 2020-2024 Tristan Grimmer.  |
| 7 | // Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby  |
| 8 | // granted, provided that the above copyright notice and this permission notice appear in all copies.  |
| 9 | //  |
| 10 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL  |
| 11 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,  |
| 12 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN  |
| 13 | // AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR  |
| 14 | // PERFORMANCE OF THIS SOFTWARE.  |
| 15 |   |
| 16 | #include <Foundation/tVersion.cmake.h>  |
| 17 | #include <Foundation/tStandard.h>  |
| 18 | #include <Foundation/tString.h>  |
| 19 | #include <System/tFile.h>  |
| 20 | #include <System/tScript.h>  |
| 21 | #include "Image/tImageTIFF.h"  |
| 22 | #include "Image/tPicture.h"  |
| 23 | #include "LibTIFF/include/tiff.h"  |
| 24 | #include "LibTIFF/include/tiffio.h"  |
| 25 | #include "LibTIFF/include/tiffvers.h"  |
| 26 | using namespace tSystem;  |
| 27 | namespace tImage  |
| 28 | {  |
| 29 |   |
| 30 |   |
| 31 | int tImageTIFF::ReadSoftwarePageDuration(TIFF* tiff) const  |
| 32 | {  |
| 33 | void* data = nullptr;  |
| 34 | int durationMilliSec = -1;  |
| 35 | int success = TIFFGetField(tif: tiff, TIFFTAG_SOFTWARE, &data);  |
| 36 | if (!success || !data)  |
| 37 | return durationMilliSec;  |
| 38 |   |
| 39 | tString softwareStr((char*)data);  |
| 40 | tExprReader script(softwareStr, false);  |
| 41 | tExpression tacentView = script.First();  |
| 42 | if (tacentView.GetAtomString() == "TacentLibrary" )  |
| 43 | {  |
| 44 | tExpression tacentVers = tacentView.Next();  |
| 45 | tExpression durationEx = tacentVers.Next();  |
| 46 | tExpression durCmd = durationEx.Item0();  |
| 47 | if (durCmd.GetAtomString() == "PageDur" )  |
| 48 | {  |
| 49 | tExpression durVal = durationEx.Item1();  |
| 50 | durationMilliSec = durVal.GetAtomInt();  |
| 51 | }  |
| 52 | }  |
| 53 |   |
| 54 | return durationMilliSec;  |
| 55 | }  |
| 56 |   |
| 57 |   |
| 58 | bool tImageTIFF::WriteSoftwarePageDuration(TIFF* tiff, int milliseconds) const  |
| 59 | {  |
| 60 | tString softwareDesc;  |
| 61 | tsPrintf(dest&: softwareDesc, format: "TacentLibrary V%d.%d.%d [PageDur %d]" , tVersion::Major, tVersion::Minor, tVersion::Revision, milliseconds);  |
| 62 | int success = TIFFSetField(tiff, TIFFTAG_SOFTWARE, softwareDesc.Chr());  |
| 63 | return success ? true : false;  |
| 64 | }  |
| 65 |   |
| 66 |   |
| 67 | bool tImageTIFF::Load(const tString& tiffFile)  |
| 68 | {  |
| 69 | Clear();  |
| 70 |   |
| 71 | if (tSystem::tGetFileType(file: tiffFile) != tSystem::tFileType::TIFF)  |
| 72 | return false;  |
| 73 |   |
| 74 | if (!tFileExists(file: tiffFile))  |
| 75 | return false;  |
| 76 |   |
| 77 | TIFF* tiff = TIFFOpen(tiffFile.Chr(), "rb" );  |
| 78 | if (!tiff)  |
| 79 | return false;  |
| 80 |   |
| 81 | // Create all frames.  |
| 82 | tPixelFormat srcFormat = tPixelFormat::R8G8B8A8;  |
| 83 | do  |
| 84 | {  |
| 85 | int width = 0; int height = 0;  |
| 86 | TIFFGetField(tif: tiff, TIFFTAG_IMAGEWIDTH, &width);  |
| 87 | TIFFGetField(tif: tiff, TIFFTAG_IMAGELENGTH, &height);  |
| 88 | if ((width <= 0) || (height <= 0))  |
| 89 | break;  |
| 90 |   |
| 91 | int numPixels = width*height;  |
| 92 | int durationMilliSeconds = ReadSoftwarePageDuration(tiff);  |
| 93 |   |
| 94 | uint32* pixels = (uint32*)_TIFFmalloc(s: numPixels * sizeof(uint32));  |
| 95 | int successCode = TIFFReadRGBAImage(tiff, width, height, pixels, 0);  |
| 96 | if (!successCode)  |
| 97 | {  |
| 98 | _TIFFfree(p: pixels);  |
| 99 | break;  |
| 100 | }  |
| 101 |   |
| 102 | tFrame* frame = new tFrame;  |
| 103 | frame->Width = width;  |
| 104 | frame->Height = height;  |
| 105 | frame->Pixels = new tPixel4b[width*height];  |
| 106 | frame->PixelFormatSrc = srcFormat;  |
| 107 |   |
| 108 | // If duration not set we use a default of 1 second.  |
| 109 | frame->Duration = (durationMilliSeconds >= 0) ? float(durationMilliSeconds)/1000.0f : 1.0f;  |
| 110 |   |
| 111 | for (int p = 0; p < width*height; p++)  |
| 112 | frame->Pixels[p] = pixels[p];  |
| 113 |   |
| 114 | _TIFFfree(p: pixels);  |
| 115 | Frames.Append(item: frame);  |
| 116 | } while (TIFFReadDirectory(tif: tiff));  |
| 117 |   |
| 118 | TIFFClose(tif: tiff);  |
| 119 | if (Frames.GetNumItems() == 0)  |
| 120 | return false;  |
| 121 |   |
| 122 | PixelFormatSrc = srcFormat;  |
| 123 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 124 |   |
| 125 | // TIFF files are assumed to be in sRGB.  |
| 126 | ColourProfileSrc = tColourProfile::sRGB;  |
| 127 | ColourProfile = tColourProfile::sRGB;  |
| 128 |   |
| 129 | return true;  |
| 130 | }  |
| 131 |   |
| 132 |   |
| 133 | bool tImageTIFF::Set(tList<tFrame>& srcFrames, bool stealFrames)  |
| 134 | {  |
| 135 | Clear();  |
| 136 | if (srcFrames.GetNumItems() <= 0)  |
| 137 | return false;  |
| 138 |   |
| 139 | PixelFormatSrc = srcFrames.Head()->PixelFormatSrc;  |
| 140 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 141 | ColourProfileSrc = tColourProfile::sRGB; // We assume srcFrames must be sRGB.  |
| 142 | ColourProfile = tColourProfile::sRGB;  |
| 143 |   |
| 144 | if (stealFrames)  |
| 145 | {  |
| 146 | while (tFrame* frame = srcFrames.Remove())  |
| 147 | Frames.Append(item: frame);  |
| 148 | }  |
| 149 | else  |
| 150 | {  |
| 151 | for (tFrame* frame = srcFrames.Head(); frame; frame = frame->Next())  |
| 152 | Frames.Append(item: new tFrame(*frame));  |
| 153 | }  |
| 154 |   |
| 155 | return true;  |
| 156 | }  |
| 157 |   |
| 158 |   |
| 159 | bool tImageTIFF::Set(tPixel4b* pixels, int width, int height, bool steal)  |
| 160 | {  |
| 161 | Clear();  |
| 162 | if (!pixels || (width <= 0) || (height <= 0))  |
| 163 | return false;  |
| 164 |   |
| 165 | tFrame* frame = new tFrame();  |
| 166 | if (steal)  |
| 167 | frame->StealFrom(src: pixels, width, height);  |
| 168 | else  |
| 169 | frame->Set(srcPixels: pixels, width, height);  |
| 170 | Frames.Append(item: frame);  |
| 171 |   |
| 172 | PixelFormatSrc = tPixelFormat::R8G8B8A8;  |
| 173 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 174 | ColourProfileSrc = tColourProfile::sRGB; // We assume pixels must be sRGB.  |
| 175 | ColourProfile = tColourProfile::sRGB;  |
| 176 |   |
| 177 | return true;  |
| 178 | }  |
| 179 |   |
| 180 |   |
| 181 | bool tImageTIFF::Set(tFrame* frame, bool steal)  |
| 182 | {  |
| 183 | Clear();  |
| 184 | if (!frame || !frame->IsValid())  |
| 185 | return false;  |
| 186 |   |
| 187 | PixelFormatSrc = frame->PixelFormatSrc;  |
| 188 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 189 | ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB.  |
| 190 | ColourProfile = tColourProfile::sRGB;  |
| 191 |   |
| 192 | if (steal)  |
| 193 | Frames.Append(item: frame);  |
| 194 | else  |
| 195 | Frames.Append(item: new tFrame(*frame));  |
| 196 |   |
| 197 | return true;  |
| 198 | }  |
| 199 |   |
| 200 |   |
| 201 | bool tImageTIFF::Set(tPicture& picture, bool steal)  |
| 202 | {  |
| 203 | Clear();  |
| 204 | if (!picture.IsValid())  |
| 205 | return false;  |
| 206 |   |
| 207 | PixelFormatSrc = picture.PixelFormatSrc;  |
| 208 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 209 | // We don't know colour profile of tPicture.  |
| 210 |   |
| 211 | // This is worth some explanation. If steal is true the picture becomes invalid and the  |
| 212 | // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the  |
| 213 | // 'set' call will memcpy them out... which makes sure the picture is still valid after and  |
| 214 | // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must  |
| 215 | // succeed if picture was valid.  |
| 216 | tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels();  |
| 217 | bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal);  |
| 218 | tAssert(success);  |
| 219 | return true;  |
| 220 | }  |
| 221 |   |
| 222 |   |
| 223 | tFrame* tImageTIFF::GetFrame(bool steal)  |
| 224 | {  |
| 225 | if (!IsValid())  |
| 226 | return nullptr;  |
| 227 |   |
| 228 | return steal ? Frames.Remove() : new tFrame( *Frames.First() );  |
| 229 | }  |
| 230 |   |
| 231 |   |
| 232 | bool tImageTIFF::IsOpaque() const  |
| 233 | {  |
| 234 | for (tFrame* frame = Frames.Head(); frame; frame = frame->Next())  |
| 235 | if (!frame->IsOpaque())  |
| 236 | return false;  |
| 237 |   |
| 238 | return true;  |
| 239 | }  |
| 240 |   |
| 241 |   |
| 242 | bool tImageTIFF::Save(const tString& tiffFilename, tFormat format, bool useZLibComp, int overrideFrameDuration) const  |
| 243 | {  |
| 244 | SaveParams params;  |
| 245 | params.Format = format;  |
| 246 | params.UseZLibCompression = useZLibComp;  |
| 247 | params.OverrideFrameDuration = overrideFrameDuration;  |
| 248 | return Save(tiffFile: tiffFilename, params);  |
| 249 | }  |
| 250 |   |
| 251 |   |
| 252 | bool tImageTIFF::Save(const tString& tiffFile, const SaveParams& params) const  |
| 253 | {  |
| 254 | if (!IsValid() || (params.Format == tFormat::Invalid))  |
| 255 | return false;  |
| 256 |   |
| 257 | if (tSystem::tGetFileType(file: tiffFile) != tSystem::tFileType::TIFF)  |
| 258 | return false;  |
| 259 |   |
| 260 | TIFF* tiff = TIFFOpen(tiffFile.Chr(), "wb" );  |
| 261 | if (!tiff)  |
| 262 | return false;  |
| 263 |   |
| 264 | int rowSize = 0;  |
| 265 | uint8* rowBuf = nullptr;  |
| 266 | for (tFrame* frame = Frames.First(); frame; frame = frame->Next())  |
| 267 | {  |
| 268 | // Writes image from last loop and starts a new directory.  |
| 269 | if (frame != Frames.First())  |
| 270 | TIFFWriteDirectory(tiff);  |
| 271 |   |
| 272 | bool isOpaque = frame->IsOpaque();  |
| 273 | int bytesPerPixel = 0;  |
| 274 | switch (params.Format)  |
| 275 | {  |
| 276 | case tFormat::Auto: bytesPerPixel = isOpaque ? 3 : 4; break;  |
| 277 | case tFormat::BPP24: bytesPerPixel = 3; break;  |
| 278 | case tFormat::BPP32: bytesPerPixel = 4; break;  |
| 279 | }  |
| 280 | tAssert(bytesPerPixel);  |
| 281 |   |
| 282 | int w = frame->Width;  |
| 283 | int h = frame->Height;  |
| 284 |   |
| 285 | TIFFSetField(tiff, TIFFTAG_IMAGEWIDTH, w);  |
| 286 | TIFFSetField(tiff, TIFFTAG_IMAGELENGTH, h);  |
| 287 | TIFFSetField(tiff, TIFFTAG_SAMPLESPERPIXEL, bytesPerPixel);  |
| 288 | TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, 8);  |
| 289 | TIFFSetField(tiff, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);  |
| 290 | TIFFSetField(tiff, TIFFTAG_COMPRESSION, params.UseZLibCompression ? COMPRESSION_DEFLATE : COMPRESSION_NONE);  |
| 291 | TIFFSetField(tiff, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);  |
| 292 | TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);  |
| 293 | if (bytesPerPixel == 4)  |
| 294 | {  |
| 295 | // Unassociated alpha means the extra channel is not a premultiplied alpha channel.  |
| 296 | uint16 [] = { EXTRASAMPLE_UNASSALPHA };  |
| 297 | TIFFSetField(tiff, TIFFTAG_EXTRASAMPLES, tNumElements(extraSampleTypes), extraSampleTypes);  |
| 298 | }  |
| 299 |   |
| 300 | int pageDurMilliSec = (params.OverrideFrameDuration >= 0) ? params.OverrideFrameDuration : int(frame->Duration*1000.0f);  |
| 301 | WriteSoftwarePageDuration(tiff, milliseconds: pageDurMilliSec);  |
| 302 |   |
| 303 | int rowSizeNeeded = TIFFScanlineSize(tif: tiff);  |
| 304 | tAssert(rowSizeNeeded == (w*bytesPerPixel));  |
| 305 |   |
| 306 | // Let's not reallocate the line buffer every frame. Often all frames will be the same size.  |
| 307 | if (rowSize != rowSizeNeeded)  |
| 308 | {  |
| 309 | if (rowBuf)  |
| 310 | _TIFFfree(p: rowBuf);  |
| 311 | rowBuf = (uint8*)_TIFFmalloc(s: rowSizeNeeded);  |
| 312 | rowSize = rowSizeNeeded;  |
| 313 | }  |
| 314 |   |
| 315 | TIFFSetField(tiff, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(tif: tiff, request: w*bytesPerPixel));  |
| 316 | for (int r = 0; r < h; r++)  |
| 317 | {  |
| 318 | for (int x = 0; x < w; x++)  |
| 319 | {  |
| 320 | int idx = (h-r-1)*w + x;  |
| 321 | rowBuf[x*bytesPerPixel + 0] = frame->Pixels[idx].R;  |
| 322 | rowBuf[x*bytesPerPixel + 1] = frame->Pixels[idx].G;  |
| 323 | rowBuf[x*bytesPerPixel + 2] = frame->Pixels[idx].B;  |
| 324 | if (bytesPerPixel == 4)  |
| 325 | rowBuf[x*bytesPerPixel + 3] = frame->Pixels[idx].A;  |
| 326 | }  |
| 327 |   |
| 328 | int errCode = TIFFWriteScanline(tif: tiff, buf: rowBuf, row: r, sample: 0);  |
| 329 | if (errCode < 0)  |
| 330 | continue;  |
| 331 | }  |
| 332 | }  |
| 333 |   |
| 334 | if (rowBuf)  |
| 335 | _TIFFfree(p: rowBuf);  |
| 336 | TIFFClose(tif: tiff);  |
| 337 |   |
| 338 | return true;  |
| 339 | }  |
| 340 |   |
| 341 |   |
| 342 | }  |
| 343 | |