| 1 | // tPicture.h  |
| 2 | //  |
| 3 | // This class represents a simple one-frame image. It is a collection of raw uncompressed 32-bit tPixels. It can load  |
| 4 | // various formats from disk such as jpg, tga, png, etc. It intentionally _cannot_ load a dds file. More on that later.  |
| 5 | // Image manipulation (excluding compression) is supported in a tPicture, so there are crop, scale, rotate, etc  |
| 6 | // functions in this class.  |
| 7 | //  |
| 8 | // Some image disk formats have more than one 'frame' or image inside them. For example, tiff files can have more than  |
| 9 | // page, and gif/webp images may be animated and have more than one frame. A tPicture can only prepresent _one_ of   |
| 10 | // these frames.  |
| 11 | //  |
| 12 | // Copyright (c) 2006, 2016, 2017, 2020-2024 Tristan Grimmer.  |
| 13 | // Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby  |
| 14 | // granted, provided that the above copyright notice and this permission notice appear in all copies.  |
| 15 | //  |
| 16 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL  |
| 17 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,  |
| 18 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN  |
| 19 | // AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR  |
| 20 | // PERFORMANCE OF THIS SOFTWARE.  |
| 21 |   |
| 22 | #pragma once  |
| 23 | #include <Foundation/tList.h>  |
| 24 | #include <Foundation/tConstants.h>  |
| 25 | #include <Math/tColour.h>  |
| 26 | #include <System/tFile.h>  |
| 27 | #include <System/tChunk.h>  |
| 28 | #include "Image/tBaseImage.h"  |
| 29 | #include "Image/tPixelFormat.h"  |
| 30 | #include "Image/tResample.h"  |
| 31 | #include "Image/tLayer.h"  |
| 32 | namespace tImage  |
| 33 | {  |
| 34 |   |
| 35 |   |
| 36 | // Verion information for the image loaders and libraries they may use. This is all in the tImage namespace.  |
| 37 | extern const char* Version_LibJpegTurbo;  |
| 38 | extern const char* Version_ASTCEncoder;  |
| 39 | extern const char* Version_OpenEXR;  |
| 40 | extern const char* Version_ZLIB;  |
| 41 | extern const char* Version_LibPNG;  |
| 42 | extern const char* Version_LibTIFF;  |
| 43 | extern const char* Version_LibKTX;  |
| 44 | extern const char* Version_ApngDis;  |
| 45 | extern const char* Version_ApngAsm;  |
| 46 | extern int Version_WEBP_Major;  |
| 47 | extern int Version_WEBP_Minor;  |
| 48 | extern int Version_BCDec_Major;  |
| 49 | extern int Version_BCDec_Minor;  |
| 50 | extern int Version_ETCDec_Major;  |
| 51 | extern int Version_ETCDec_Minor;  |
| 52 | extern int Version_LibSPNG_Major;  |
| 53 | extern int Version_LibSPNG_Minor;  |
| 54 | extern int Version_LibSPNG_Patch;  |
| 55 | extern int Version_TinyXML2_Major;  |
| 56 | extern int Version_TinyXML2_Minor;  |
| 57 | extern int Version_TinyXML2_Patch;  |
| 58 | extern int Version_TinyEXIF_Major;  |
| 59 | extern int Version_TinyEXIF_Minor;  |
| 60 | extern int Version_TinyEXIF_Patch;  |
| 61 |   |
| 62 |   |
| 63 | // A tPicture is a single 2D image. A rectangular collection of R8G8B8A8 pixels (32bits per pixel). The origin is the  |
| 64 | // lower left, and the rows are ordered from bottom to top in memory. @todo At some point we need to support HDR images and  |
| 65 | // should represent the pixels as R32G32B32A32f.  |
| 66 | //  |
| 67 | // The main purpose of a tPicture is to allow manipulation of a single image. Things like setting pixel colours,  |
| 68 | // rotation, flips, resampling and resizing are found here.  |
| 69 | //  |
| 70 | // There is no saving and loading directly from image files because some types may have multiple frames. For example  |
| 71 | // a gif or webp may be animated. We could just choose a particular frame, but that would mean loading all frames only  |
| 72 | // to keep a single one. There is the same complexity with saving. Different image formats have drastically different  |
| 73 | // parameters that need to be specified for saving -- jpgs need a quality setting, astc files have a multitude of  |
| 74 | // compression parameters in addition to the block size, targas can be RLE encoded, ktx files can be supercompressed...  |
| 75 | // or not, etc. The purpose of the tImageNNN files is to deal with that complexity for each specific image type. From  |
| 76 | // these loaders you can construct one or more tPictures by passing in the pixels, width, and height.  |
| 77 | //  |
| 78 | // There is some save/load functionality directly for a tPicture. It has it's own file format based of tChunks. It can  |
| 79 | // save/load itself to/from a .tac file.  |
| 80 | class tPicture : public tLink<tPicture>  |
| 81 | {  |
| 82 | public:  |
| 83 | // Constructs an empty picture that is invalid. You must call Load yourself later.  |
| 84 | tPicture() { }  |
| 85 |   |
| 86 | // Constructs a picture that is width by height pixels. It will be all black pixels with an opaque alpha of 255.  |
| 87 | // That is, every pixel will be (0, 0, 0, 255).  |
| 88 | tPicture(int width, int height) { Set(width, height); }  |
| 89 |   |
| 90 | // This constructor allows you to specify an external buffer of pixels to use. If copyPixels is true, it simply  |
| 91 | // copies the values from the buffer you supply. If copyPixels is false, it means you are giving the buffer to the  |
| 92 | // tPicture. In this case the tPicture will delete[] the buffer for you when appropriate.  |
| 93 | tPicture(int width, int height, tPixel4b* pixelBuffer, bool copyPixels = true) { Set(width, height, pixelBuffer, copyPixels); }  |
| 94 |   |
| 95 | // Construct from a tFrame. If steal is true the tPicture will take ownership of the tFrame. If steal is false it  |
| 96 | // will copy the pixels out. The frame duration is also taken from the frame.  |
| 97 | tPicture(tFrame* frame, bool steal) { Set(frame, steal); }  |
| 98 |   |
| 99 | // Constructs from any type derived from tImageBase (eg. tImageASTC). If steal is true the tImageBase MAY be  |
| 100 | // modified. In particular it may be invalid afterwards because the pixels may have been stolen from it. For  |
| 101 | // multiframe images it meay still be valid after but down a frame. On the other hand with steal false you are  |
| 102 | // guaranteed that image remains unmodified, but at the cost of duplicating memory for the pixels.  |
| 103 | tPicture(tBaseImage& image, bool steal = true) { Set(image, steal); }  |
| 104 |   |
| 105 | // Copy constructor.  |
| 106 | tPicture(const tPicture& src) : tPicture() { Set(src); }  |
| 107 |   |
| 108 | virtual ~tPicture() { Clear(); }  |
| 109 | bool IsValid() const { return Pixels ? true : false; }  |
| 110 |   |
| 111 | // Invalidated the picture and frees memory associated with it. The tPicture will be invalid after this.  |
| 112 | void Clear();  |
| 113 |   |
| 114 | // Sets the image to the dimensions provided. Image will be opaque black after this call. Internally, if the  |
| 115 | // existing buffer is the right size, it is reused. In all cases, the entire image is cleared to black.  |
| 116 | void Set(int width, int height, const tPixel4b& colour = tPixel4b::black);  |
| 117 |   |
| 118 | // Sets the image to the dimensions provided. Allows you to specify an external buffer of pixels to use. If  |
| 119 | // copyPixels is true, it simply copies the values from the buffer you supply. In this case it will attempt to  |
| 120 | // reuse it's existing buffer if it can. If copyPixels is false, it means you are giving the buffer to the  |
| 121 | // tPicture. In this case the tPicture will delete[] the buffer for you when appropriate. In all cases, existing  |
| 122 | // pixel data is lost. Other members of the tPicture are unmodified.  |
| 123 | void Set(int width, int height, tPixel4b* pixelBuffer, bool copyPixels = true);  |
| 124 |   |
| 125 | // Sets from a tFrame. If steal is true the tPicture will take ownership of the tFrame. If steal is false it will  |
| 126 | // copy the pixels out. The frame duration is also taken from the frame.  |
| 127 | void Set(tFrame* frame, bool steal);  |
| 128 |   |
| 129 | // Sets from any type derived from tImageBase (eg. tImageASTC). If steal is true the tImageBase MAY be  |
| 130 | // modified. In particular it may be invalid afterwards because the pixels may have been stolen from it. For  |
| 131 | // multiframe images it meay still be valid after but down a frame. On the other hand with steal false you are  |
| 132 | // guaranteed that image remains unmodified, but at the cost of duplicating memory for the pixels.  |
| 133 | void Set(tBaseImage& image, bool steal = true);  |
| 134 |   |
| 135 | void Set(const tPicture& src);  |
| 136 |   |
| 137 | // Save and Load to tChunk format.  |
| 138 | void Save(tChunkWriter&) const;  |
| 139 | void Load(const tChunk&);  |
| 140 |   |
| 141 | // Returns true if all pixels are completely opaque (an alphas of 255). This function checks the entire pixel  |
| 142 | // buffer every time it is called.  |
| 143 | bool IsOpaque() const;  |
| 144 |   |
| 145 | // These functions allow reading and writing pixels.  |
| 146 | tPixel4b& Pixel(int x, int y) { return Pixels[ GetIndex(x, y) ]; }  |
| 147 | tPixel4b* operator[](int i) /* Syntax: image[y][x] = colour; No bounds checking performed. */ { return Pixels + GetIndex(x: 0, y: i); }  |
| 148 | tPixel4b GetPixel(int x, int y) const { return Pixels[ GetIndex(x, y) ]; }  |
| 149 | tPixel4b* GetPixelPointer(int x = 0, int y = 0) const { return &Pixels[ GetIndex(x, y) ]; }  |
| 150 | tPixel4b* GetPixels() const { return Pixels; }  |
| 151 | tPixel4b* StealPixels() { tPixel4b* p = Pixels; Pixels = nullptr; return p; }  |
| 152 |   |
| 153 | void SetPixel(int x, int y, const tColour4b& c) { Pixels[ GetIndex(x, y) ] = c; }  |
| 154 | void SetPixel(int x, int y, uint8 r, uint8 g, uint8 b, uint8 a = 0xFF) { Pixels[ GetIndex(x, y) ] = tColour4b(r, g, b, a); }  |
| 155 | void SetPixel(int x, int y, const tColour4b& c, comp_t channels);  |
| 156 | void SetAll(const tColour4b& = tColour4b(0, 0, 0), comp_t channels = tCompBit_RGBA);  |
| 157 |   |
| 158 | // Spreads the specified single channel to all RGB channels. If channel is R, G, or B, it spreads to the remainder  |
| 159 | // of RGB (e.g. R will spread to GB). If channel is alpha, spreads to RGB.  |
| 160 | void Spread(tComp channel = tComp::R);  |
| 161 |   |
| 162 | // Swizzle colour channels. You specify the RGBA destination channels in that order. For example, to swap R and B  |
| 163 | // channels you would call Swizzle(B,G,R,A). You can also use tComp::Zero and tComp::Full to set the channel to zero  |
| 164 | // or full values. The default swizzle is RGBA which does nothing. If tComp::Auto is set for any channel, it just  |
| 165 | // uses the default for that channel.  |
| 166 | void Swizzle(tComp = tComp::R, tComp = tComp::G, tComp = tComp::B, tComp = tComp::A);  |
| 167 |   |
| 168 | // Computes RGB intensity and sets specified channels to that value. Any combination of RGBA allowed.  |
| 169 | void Intensity(comp_t channels = tCompBit_RGB);  |
| 170 |   |
| 171 | // Blends blendColour (background) into the RGB channels specified (usually RGB, but any combination of the 3 is  |
| 172 | // allowed) using the pixel alpha to modulate. The new pixel colour is alpha*component + (1-alpha)*blend_component.  |
| 173 | //  |
| 174 | // Eg. If pixel alpha is 255, then none of the blend colour is used for that pixel. If alpha is 0, all of it is  |
| 175 | // used. If alpha is 64, then 1/4 of the current pixel colour and 3/4 of the supplied,  |
| 176 | //  |
| 177 | // FinalAlpha should be in [-1, 255]. If finalAlpha is >= 0 then the alpha will be set to finalAlpha after the blend  |
| 178 | // is complete. If finalAlpha is -1, the alpha is left unmodified. By default the finalAlpha is 255 (opaque) which  |
| 179 | // means the operation essentially creates a premultiplied-alpha opaque image.  |
| 180 | // Note that the alpha of the supplied colour is ignored (since we use finalAlpha).  |
| 181 | // Note that unspecified RGB channels are keft unmodified.  |
| 182 | void AlphaBlendColour(const tColour4b& blendColour, comp_t = tCompBit_RGB, int finalAlpha = 255);  |
| 183 |   |
| 184 | int GetWidth() const { return Width; }  |
| 185 | int GetHeight() const { return Height; }  |
| 186 | int GetArea() const { return Width*Height; }  |
| 187 | int GetNumPixels() const { return GetArea(); }  |
| 188 |   |
| 189 | void Rotate90(bool antiClockWise);  |
| 190 |   |
| 191 | // Rotates image about center point. The resultant image size is always big enough to hold every source pixel. Call  |
| 192 | // one or more of the crop functions after if you need to change the canvas size or remove transparent sides. The  |
| 193 | // rotate algorithm first upscales the image x4, rotates, then downscales. That is what upFilter and downFilter are  |
| 194 | // for. If you want to rotate pixel-art (nearest neighbour, no up/dowuse upFilter = none.  |
| 195 | //  |
| 196 | // UpFilter DownFilter Description  |
| 197 | // None NA No up/down scaling. Preserves colours. Nearest Neighbour. Fast. Good for pixel art.  |
| 198 | // Valid Valid Up/down scaling. Smooth. Good results with up=bilinear, down=box.  |
| 199 | // Valid None Up/down scaling. Use alternate (sharper) downscaling scheme (pad + 2 X ScaleHalf).  |
| 200 | void RotateCenter  |
| 201 | (  |
| 202 | float angle,  |
| 203 | const tPixel4b& fill = tPixel4b::transparent,  |
| 204 | tResampleFilter upFilter = tResampleFilter::None,  |
| 205 | tResampleFilter downFilter = tResampleFilter::None  |
| 206 | );  |
| 207 |   |
| 208 | void Flip(bool horizontal);  |
| 209 |   |
| 210 | enum class Anchor  |
| 211 | {  |
| 212 | LeftTop, MiddleTop, RightTop,  |
| 213 | LeftMiddle, MiddleMiddle, RightMiddle,  |
| 214 | LeftBottom, MiddleBottom, RightBottom  |
| 215 | };  |
| 216 |   |
| 217 | // Cropping. Can also perform a canvas enlargement. If width or height are smaller than the current size the image  |
| 218 | // is cropped. If larger, the fill colour is used. Fill defaults to transparent-zero-alpha black pixels.  |
| 219 | bool Crop(int newWidth, int newHeight, Anchor = Anchor::MiddleMiddle, const tColour4b& fill = tColour4b::transparent);  |
| 220 | bool Crop(int newWidth, int newHeight, int originX, int originY, const tColour4b& fill = tColour4b::transparent);  |
| 221 |   |
| 222 | // Copies a supplied rectangle into the image at the specified position.  |
| 223 | bool CopyRegion(int regionW, int regionH, const tColour4b* regionPixels, int originX, int originY, comp_t channels = tCompBit_RGBA);  |
| 224 | bool CopyRegion(int regionW, int regionH, const tColour4b* regionPixels, Anchor = Anchor::LeftBottom, comp_t channels = tCompBit_RGBA);  |
| 225 |   |
| 226 | // Crops sides that match the specified colour. Optionally select only some channels to be considered.  |
| 227 | // If this function wants to remove everything it returns false and leaves the image untouched.  |
| 228 | // If this function wants to remove nothing it returns false and leaves the image untouched.  |
| 229 | bool Deborder(const tColour4b& = tColour4b::transparent, comp_t channels = tCompBit_A);  |
| 230 |   |
| 231 | // Same as above but only check if borders exist. Does not modify picture.  |
| 232 | bool HasBorders(const tColour4b& = tColour4b::transparent, comp_t channels = tCompBit_A) const;  |
| 233 |   |
| 234 | // Quantize image colours based on a fixed palette. numColours must be 256 or less. checkExact means no change to  |
| 235 | // the image will be made if it already contains fewer colours than numColours already. This may or may not be  |
| 236 | // desireable as the computed or fixed palette would not be used.  |
| 237 | bool QuantizeFixed(int numColours, bool checkExact = true);  |
| 238 |   |
| 239 | // Similar to above but uses spatial quantization to generate the palette. If ditherLevel is 0.0 it will compute a  |
| 240 | // good dither amount for you based on the image dimensions and number of colours. Filter size must be 1, 3, or 5.  |
| 241 | bool QuantizeSpatial(int numColours, bool checkExact = true, double ditherLevel = 0.0, int filterSize = 3);  |
| 242 |   |
| 243 | // Similar to above but uses neuquant algorighm to generate the palette. With a sampling factor of 1 the entire  |
| 244 | // image is used in the learning phase. With a factor of 10, a pseudo-random subset of 1/10 of the pixels are used  |
| 245 | // in the learning phase. sampleFactor must be in [1, 30]. Bigger values are faster but lower quality.  |
| 246 | bool QuantizeNeu(int numColours, bool checkExact = true, int sampleFactor = 1);  |
| 247 |   |
| 248 | // Similar to above but uses Wu algorighm to generate the palette.  |
| 249 | bool QuantizeWu(int numColours, bool checkExact = true);  |
| 250 |   |
| 251 | // Ideally adjustments (brightness, contrast etc) would be done in a fragment shader and then 'committed' to the  |
| 252 | // tPicture with a simple adjust call. However currently the clients of tPicture don't have that ability so we're  |
| 253 | // going with a begin/adjust/end setup where a new 'original' pixel buffer is allocated on begin, and an adjustment  |
| 254 | // writes to the current buffer. End deletes the temporary original buffer. Adjustments are always based on the  |
| 255 | // original source pixels. It stops the issue, for example, of setting the brightness to full and losing all the  |
| 256 | // colour data when you move back down. This function also precomputes the internal min/max colour values  |
| 257 | // and histograms. Essentially this starts an adjustment session. Returns false if the image is invalid.  |
| 258 | bool AdjustmentBegin();  |
| 259 |   |
| 260 | // Adjust brightness based on the tPicture pixels and write them into the adjustment pixel buffer. Brightness is in  |
| 261 | // [0.0,1.0]. When brightness at 0.0 adjustment buffer will be completely black. When brightness at 1.0, pure white,  |
| 262 | // Note that the range of the brigtness is computed so that all values between [0,1] have an effect on the image.  |
| 263 | // This is possible because the min and max colour values were computed by inspecting every pixel when begin was  |
| 264 | // called. In other words the values the colours move up or down for a particular brightness are image dependent,  |
| 265 | // Returns success.  |
| 266 | //  |
| 267 | // The AdjustGetDefaultNNN functions get the parameters needed to have zero affect on the image. For brightness in  |
| 268 | // particular it is dependent on the image contents and may not be exactly 0.5. If the min/max colour values did not  |
| 269 | // reach 0 and full, the default brightness may be offset from 0.5.  |
| 270 | bool AdjustBrightness(float brightness, comp_t = tCompBit_RGB);  |
| 271 | bool AdjustGetDefaultBrightness(float& brightness);  |
| 272 |   |
| 273 | // Adjust contrast based on the tPicture pixels and write them into the adjustment pixel buffer. Contrast is in  |
| 274 | // [0.0, 1.0]. When contrast is at 0.0, adjustment buffer will be lowest contrast. When contrast at 1.0, highest,  |
| 275 | // Returns success.  |
| 276 | bool AdjustContrast(float contrast, comp_t = tCompBit_RGB);  |
| 277 | bool AdjustGetDefaultContrast(float& contrast);  |
| 278 |   |
| 279 | // Adjust levels. All values are E [0, 1]. Ensure blackPoint <= midPoint <= whitePoint and blackOut <= whiteOut.  |
| 280 | // If these conditions are not met they are silently enforced starting at black (unmodified). The powerMidGamma  |
| 281 | // option lets you decide between 2 algorithms to determine the curve on the gamma. If false it uses some code that  |
| 282 | // tries to mimic Photoshop. See https://stackoverflow.com/questions/39510072/algorithm-for-adjustment-of-image-levels  |
| 283 | // The curve for the above is C1 discontinuous at gamma 1, so I didn't like it. powerMidGamma, the default, uses  |
| 284 | // a continuous base-10 power curve that smoothly goes from gamma 0.1 to gamma 10.  |
| 285 | // For the power curve the gamma range is [0.1, 10.0] where 1.0 is linear. This approximates GIMP.  |
| 286 | // For the photo curve the gamma range is [0.01, 9.99] where 1.0 is linear. This approximates PS.  |
| 287 | // The defaults to result in no change are the same for both algorithms.  |
| 288 | bool AdjustLevels(float blackPoint, float midPoint, float whitePoint, float blackOut, float whiteOut, bool powerMidGamma = true, comp_t = tCompBit_RGB);  |
| 289 | bool AdjustGetDefaultLevels(float& blackPoint, float& midPoint, float& whitePoint, float& blackOut, float& whiteOut);  |
| 290 |   |
| 291 | // Keeps the adjustment session open and restores the pixels to their original values.  |
| 292 | bool AdjustRestoreOriginal();  |
| 293 |   |
| 294 | // Ends adjustment session and deletes the temporary original pixel buffer. Returns success.  |
| 295 | bool AdjustmentEnd();  |
| 296 |   |
| 297 | // This function scales the image by half using a box filter. Useful for generating mipmaps. This function returns  |
| 298 | // false if the rescale could not be performed. For this function to succeed:  |
| 299 | // - The image needs to be valid AND  |
| 300 | // - The width must be divisible by two if it is not equal to 1 AND  |
| 301 | // - The height must be divisible by two if it is not equal to 1.  |
| 302 | // Dimensions of 1 are handled since it's handy for mipmap generation. If width=10 and height=1, we'd end up with a  |
| 303 | // 5x1 image. An 11x1 image would yield an error and return false. A 1x1 successfully yields the same 1x1 image.  |
| 304 | bool ScaleHalf();  |
| 305 |   |
| 306 | // Resizes the image using the specified filter. Returns success. If the resample fails the tPicture is unmodified.  |
| 307 | bool Resample  |
| 308 | (  |
| 309 | int width, int height,  |
| 310 | tResampleFilter = tResampleFilter::Bilinear, tResampleEdgeMode = tResampleEdgeMode::Clamp  |
| 311 | );  |
| 312 | bool Resize  |
| 313 | (  |
| 314 | int width, int height,  |
| 315 | tResampleFilter filter = tResampleFilter::Bilinear, tResampleEdgeMode edgeMode = tResampleEdgeMode::Clamp  |
| 316 | ) { return Resample(width, height, filter, edgeMode); }  |
| 317 |   |
| 318 | // A convenience. This is sort of light tTexture functionality -- generate layers that may be passed off to HW.  |
| 319 | // Unlike tTexture that compresses to a BC format, this function always uses R8G8B8A8 pixel format and does not  |
| 320 | // require power-of-2 dimensions. If generating mipmap layers, each layer is half (truncated) in width and height  |
| 321 | // until a 1x1 is reached. There is no restriction on starting dimensions (they may be odd for example). Populates  |
| 322 | // (appends) to the supplied tLayer list. If resampleFilter is None no mipmap layers are generated, only a single  |
| 323 | // layer will be appened. In this case edgeMode is ignored. If chainGeneration is true, the previous mip texture  |
| 324 | // is gused to generate the next -- this is faster but may not be as good quality. Returns number of appended  |
| 325 | // layers.  |
| 326 | int GenerateLayers  |
| 327 | (  |
| 328 | tList<tLayer>&, tResampleFilter filter = tResampleFilter::Lanczos_Narrow,  |
| 329 | tResampleEdgeMode edgeMode = tResampleEdgeMode::Clamp,  |
| 330 | bool chainGeneration = true  |
| 331 | );  |
| 332 | bool operator==(const tPicture&) const;  |
| 333 | bool operator!=(const tPicture&) const;  |
| 334 |   |
| 335 | tString Filename;  |
| 336 | tPixelFormat PixelFormatSrc = tPixelFormat::Invalid;  |
| 337 | uint TextureID = 0;  |
| 338 | float Duration = 0.5f;  |
| 339 |   |
| 340 | // Transient parameters. Only access between AdjustmentBegin/End.  |
| 341 | int BrightnessRGBMin = 0; // Used for brightness adjustments.  |
| 342 | int BrightnessRGBMax = 0; // Used for brightness adjustments.  |
| 343 | static const int NumGroups = 256; // Sure makes it easy choosing 256 groups.  |
| 344 |   |
| 345 | // We use float counts since pixels with alpha are computed by multiplying by the alphs. This means we get  |
| 346 | // fractional counts -- but it makes the histogram more representive of the actual colours/intensity present.  |
| 347 | float HistogramR[NumGroups]; float MaxRCount; // Frequency of Red. Max R count in all groups.  |
| 348 | float HistogramG[NumGroups]; float MaxGCount; // Frequency of Green. Max G count in all groups.  |
| 349 | float HistogramB[NumGroups]; float MaxBCount; // Frequency of Blue. Max B count in all groups.  |
| 350 | float HistogramA[NumGroups]; float MaxACount; // Frequency of Alpha. Max A count in all groups.  |
| 351 | float HistogramI[NumGroups]; float MaxICount; // Frequency of Inten. Max I count in all groups (intensity).  |
| 352 |   |
| 353 | private:  |
| 354 | int GetIndex(int x, int y) const { tAssert((x >= 0) && (y >= 0) && (x < Width) && (y < Height)); return y * Width + x; }  |
| 355 | static int GetIndex(int x, int y, int w, int h) { tAssert((x >= 0) && (y >= 0) && (x < w) && (y < h)); return y * w + x; }  |
| 356 |   |
| 357 | void RotateCenterNearest(const tMath::tMatrix2& rotMat, const tMath::tMatrix2& invRot, const tPixel4b& fill);  |
| 358 | void RotateCenterResampled  |
| 359 | (  |
| 360 | const tMath::tMatrix2& rotMat, const tMath::tMatrix2& invRot, const tPixel4b& fill,  |
| 361 | tResampleFilter upFilter, tResampleFilter downFilter  |
| 362 | );  |
| 363 |   |
| 364 | // Returns false if either no borders or the borders overlap because the image in homogenous in the channels.  |
| 365 | bool   |
| 366 | (  |
| 367 | const tColour4b&, comp_t channels,  |
| 368 | int& numBottomRows, int& numTopRows, int& numLeftCols, int& numRightCols  |
| 369 | ) const;  |
| 370 |   |
| 371 | int Width = 0;  |
| 372 | int Height = 0;  |
| 373 | tPixel4b* Pixels = nullptr;  |
| 374 | tPixel4b* OriginalPixels = nullptr;  |
| 375 | };  |
| 376 |   |
| 377 |   |
| 378 | // Implementation below this line.  |
| 379 |   |
| 380 |   |
| 381 | inline void tPicture::Clear()  |
| 382 | {  |
| 383 | Filename.Clear();  |
| 384 | delete[] Pixels;  |
| 385 | Pixels = nullptr;  |
| 386 | delete[] OriginalPixels;  |
| 387 | OriginalPixels = nullptr;  |
| 388 | Width = 0;  |
| 389 | Height = 0;  |
| 390 | PixelFormatSrc = tPixelFormat::Invalid;  |
| 391 | }  |
| 392 |   |
| 393 |   |
| 394 | inline void tPicture::Set(int width, int height, const tPixel4b& colour)  |
| 395 | {  |
| 396 | tAssert((width > 0) && (height > 0));  |
| 397 |   |
| 398 | // Reuse the existing buffer if possible.  |
| 399 | if (width*height != Width*Height)  |
| 400 | {  |
| 401 | delete[] Pixels;  |
| 402 | Pixels = new tPixel4b[width*height];  |
| 403 | }  |
| 404 | Width = width;  |
| 405 | Height = height;  |
| 406 | for (int pixel = 0; pixel < (Width*Height); pixel++)  |
| 407 | Pixels[pixel] = colour;  |
| 408 |   |
| 409 | PixelFormatSrc = tPixelFormat::R8G8B8A8;  |
| 410 | }  |
| 411 |   |
| 412 |   |
| 413 | inline void tPicture::Set(int width, int height, tPixel4b* pixelBuffer, bool copyPixels)  |
| 414 | {  |
| 415 | tAssert((width > 0) && (height > 0) && pixelBuffer);  |
| 416 |   |
| 417 | // If we're copying the pixels we may be able to reuse the existing buffer if it's the right size. If we're not  |
| 418 | // copying and the buffer is being handed to us, we just need to free our current buffer.  |
| 419 | if (copyPixels)  |
| 420 | {  |
| 421 | if ((width*height != Width*Height) || !Pixels)  |
| 422 | {  |
| 423 | delete[] Pixels;  |
| 424 | Pixels = new tPixel4b[width*height];  |
| 425 | }  |
| 426 | }  |
| 427 | else  |
| 428 | {  |
| 429 | delete[] Pixels;  |
| 430 | Pixels = pixelBuffer;  |
| 431 | }  |
| 432 | Width = width;  |
| 433 | Height = height;  |
| 434 |   |
| 435 | if (copyPixels)  |
| 436 | tStd::tMemcpy(dest: Pixels, src: pixelBuffer, numBytes: Width*Height*sizeof(tPixel4b));  |
| 437 |   |
| 438 | PixelFormatSrc = tPixelFormat::R8G8B8A8;  |
| 439 | }  |
| 440 |   |
| 441 |   |
| 442 | inline void tPicture::Set(tFrame* frame, bool steal)  |
| 443 | {  |
| 444 | if (!frame || !frame->IsValid())  |
| 445 | return;  |
| 446 |   |
| 447 | Set(width: frame->Width, height: frame->Height, pixelBuffer: frame->GetPixels(steal), copyPixels: !steal);  |
| 448 | Duration = frame->Duration;  |
| 449 | if (steal)  |
| 450 | delete frame;  |
| 451 | }  |
| 452 |   |
| 453 |   |
| 454 | inline void tPicture::Set(tBaseImage& image, bool steal)  |
| 455 | {  |
| 456 | if (!image.IsValid())  |
| 457 | return;  |
| 458 |   |
| 459 | tFrame* frame = image.GetFrame(steal);  |
| 460 |   |
| 461 | // The true here is correct. Whether steal was true or not, we now have a frame that is under our  |
| 462 | // management and must be eventually deleted.  |
| 463 | Set(frame, steal: true);  |
| 464 | }  |
| 465 |   |
| 466 |   |
| 467 | inline void tPicture::Set(const tPicture& src)  |
| 468 | {  |
| 469 | Clear();  |
| 470 | if (!src.IsValid())  |
| 471 | return;  |
| 472 |   |
| 473 | Set(width: src.Width, height: src.Height, pixelBuffer: src.Pixels);  |
| 474 | Filename = src.Filename;  |
| 475 | PixelFormatSrc = src.PixelFormatSrc;  |
| 476 | Duration = src.Duration;  |
| 477 | }  |
| 478 |   |
| 479 |   |
| 480 | inline bool tPicture::IsOpaque() const  |
| 481 | {  |
| 482 | for (int pixel = 0; pixel < (Width*Height); pixel++)  |
| 483 | {  |
| 484 | if (Pixels[pixel].A < 255)  |
| 485 | return false;  |
| 486 | }  |
| 487 |   |
| 488 | return true;  |
| 489 | }  |
| 490 |   |
| 491 |   |
| 492 | inline void tPicture::SetPixel(int x, int y, const tColour4b& c, comp_t channels)  |
| 493 | {  |
| 494 | tPixel4b& pixel = Pixels[ GetIndex(x, y) ];  |
| 495 |   |
| 496 | if (channels & tCompBit_R) pixel.R = c.R;  |
| 497 | if (channels & tCompBit_G) pixel.G = c.G;  |
| 498 | if (channels & tCompBit_B) pixel.B = c.B;  |
| 499 | if (channels & tCompBit_A) pixel.A = c.A;  |
| 500 | }  |
| 501 |   |
| 502 |   |
| 503 | inline void tPicture::SetAll(const tColour4b& clearColour, comp_t channels)  |
| 504 | {  |
| 505 | if (!Pixels)  |
| 506 | return;  |
| 507 |   |
| 508 | int numPixels = Width*Height;  |
| 509 | if (channels == tCompBit_RGBA)  |
| 510 | {  |
| 511 | for (int p = 0; p < numPixels; p++)  |
| 512 | Pixels[p] = clearColour;  |
| 513 | }  |
| 514 | else  |
| 515 | {  |
| 516 | for (int p = 0; p < numPixels; p++)  |
| 517 | {  |
| 518 | if (channels & tCompBit_R) Pixels[p].R = clearColour.R;  |
| 519 | if (channels & tCompBit_G) Pixels[p].G = clearColour.G;  |
| 520 | if (channels & tCompBit_B) Pixels[p].B = clearColour.B;  |
| 521 | if (channels & tCompBit_A) Pixels[p].A = clearColour.A;  |
| 522 | }  |
| 523 | }  |
| 524 | }  |
| 525 |   |
| 526 |   |
| 527 | inline void tPicture::Spread(tComp channel)  |
| 528 | {  |
| 529 | int numPixels = Width*Height;  |
| 530 | for (int p = 0; p < numPixels; p++)  |
| 531 | {  |
| 532 | tPixel4b& pixel = Pixels[p];  |
| 533 | switch (channel)  |
| 534 | {  |
| 535 | case tComp::R: pixel.G = pixel.B = pixel.R; break;  |
| 536 | case tComp::G: pixel.R = pixel.B = pixel.G; break;  |
| 537 | case tComp::B: pixel.R = pixel.G = pixel.B; break;  |
| 538 | case tComp::A: pixel.R = pixel.G = pixel.B = pixel.A; break;  |
| 539 | }  |
| 540 | }  |
| 541 | }  |
| 542 |   |
| 543 |   |
| 544 | inline void tPicture::Swizzle(tComp R, tComp G, tComp B, tComp A)  |
| 545 | {  |
| 546 | if (!Pixels)  |
| 547 | return;  |
| 548 |   |
| 549 | if (R == tComp::Auto) R = tComp::R;  |
| 550 | if (G == tComp::Auto) G = tComp::G;  |
| 551 | if (B == tComp::Auto) B = tComp::B;  |
| 552 | if (A == tComp::Auto) A = tComp::A;  |
| 553 |   |
| 554 | if ((R == tComp::R) && (G == tComp::G) && (B == tComp::B) && (A == tComp::A))  |
| 555 | return;  |
| 556 |   |
| 557 | tPixel4b* newPixels = new tPixel4b[Width*Height];  |
| 558 | for (int p = 0; p < Width*Height; p++)  |
| 559 | {  |
| 560 | tColour4b& src = Pixels[p];  |
| 561 | tColour4b& dst = newPixels[p];  |
| 562 | uint8 r =  |
| 563 | (R==tComp::Zero ? 0 :  |
| 564 | (R==tComp::Full ? 255 :  |
| 565 | (R==tComp::R ? src.R :  |
| 566 | (R==tComp::G ? src.G :  |
| 567 | (R==tComp::B ? src.B :  |
| 568 | (R==tComp::A ? src.A : 0  |
| 569 | ))))));  |
| 570 | uint8 g =  |
| 571 | (G==tComp::Zero ? 0 :  |
| 572 | (G==tComp::Full ? 255 :  |
| 573 | (G==tComp::R ? src.R :  |
| 574 | (G==tComp::G ? src.G :  |
| 575 | (G==tComp::B ? src.B :  |
| 576 | (G==tComp::A ? src.A : 0  |
| 577 | ))))));  |
| 578 | uint8 b =  |
| 579 | (B==tComp::Zero ? 0 :  |
| 580 | (B==tComp::Full ? 255 :  |
| 581 | (B==tComp::R ? src.R :  |
| 582 | (B==tComp::G ? src.G :  |
| 583 | (B==tComp::B ? src.B :  |
| 584 | (B==tComp::A ? src.A : 0  |
| 585 | ))))));  |
| 586 | uint8 a =  |
| 587 | (A==tComp::Zero ? 0 :  |
| 588 | (A==tComp::Full ? 255 :  |
| 589 | (A==tComp::R ? src.R :  |
| 590 | (A==tComp::G ? src.G :  |
| 591 | (A==tComp::B ? src.B :  |
| 592 | (A==tComp::A ? src.A : 255  |
| 593 | ))))));  |
| 594 | dst.Set(r, g, b, a);  |
| 595 | }  |
| 596 | delete[] Pixels;  |
| 597 | Pixels = newPixels;  |
| 598 | }  |
| 599 |   |
| 600 |   |
| 601 | inline void tPicture::Intensity(comp_t channels)  |
| 602 | {  |
| 603 | if (!channels)  |
| 604 | return;  |
| 605 |   |
| 606 | int numPixels = Width*Height;  |
| 607 | for (int p = 0; p < numPixels; p++)  |
| 608 | {  |
| 609 | tPixel4b& pixel = Pixels[p];  |
| 610 | int intensity = pixel.Intensity();  |
| 611 | if (channels & tCompBit_R) pixel.R = intensity;  |
| 612 | if (channels & tCompBit_G) pixel.G = intensity;  |
| 613 | if (channels & tCompBit_B) pixel.B = intensity;  |
| 614 | if (channels & tCompBit_A) pixel.A = intensity;  |
| 615 | }  |
| 616 | }  |
| 617 |   |
| 618 |   |
| 619 | inline void tPicture::AlphaBlendColour(const tColour4b& blend, comp_t channels, int finalAlpha)  |
| 620 | {  |
| 621 | if (!Pixels)  |
| 622 | return;  |
| 623 |   |
| 624 | tMath::tiClamp(val&: finalAlpha, min: -1, max: 255);  |
| 625 | int numPixels = Width*Height;  |
| 626 | for (int p = 0; p < numPixels; p++)  |
| 627 | {  |
| 628 | tColour4f pixelCol(Pixels[p]);  |
| 629 | tColour4f blendCol(blend);  |
| 630 | tColour4f pixel = pixelCol;  |
| 631 | float alpha = pixelCol.A;  |
| 632 | float oneMinusAlpha = 1.0f - alpha;  |
| 633 |   |
| 634 | if (channels & tCompBit_R) pixel.R = pixelCol.R*alpha + blendCol.R*oneMinusAlpha;  |
| 635 | if (channels & tCompBit_G) pixel.G = pixelCol.G*alpha + blendCol.G*oneMinusAlpha;  |
| 636 | if (channels & tCompBit_B) pixel.B = pixelCol.B*alpha + blendCol.B*oneMinusAlpha;  |
| 637 | if (finalAlpha >= 0)  |
| 638 | pixel.SetA(finalAlpha);  |
| 639 |   |
| 640 | Pixels[p].Set(pixel);  |
| 641 | }  |
| 642 | }  |
| 643 |   |
| 644 |   |
| 645 | inline bool tPicture::operator==(const tPicture& src) const  |
| 646 | {  |
| 647 | if (!Pixels || !src.Pixels)  |
| 648 | return false;  |
| 649 |   |
| 650 | if ((Width != src.Width) || (Height != src.Height))  |
| 651 | return false;  |
| 652 |   |
| 653 | for (int pixel = 0; pixel < (Width * Height); pixel++)  |
| 654 | if (Pixels[pixel] != src.Pixels[pixel])  |
| 655 | return false;  |
| 656 |   |
| 657 | return true;  |
| 658 | }  |
| 659 |   |
| 660 |   |
| 661 | inline bool tPicture::operator!=(const tPicture& src) const  |
| 662 | {  |
| 663 | return !(*this == src);  |
| 664 | }  |
| 665 |   |
| 666 |   |
| 667 | }  |
| 668 | |