/******************************************************************************
 *
 * Project:  GeoTIFF Driver
 * Purpose:  GDAL GeoTIFF support.
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 1998, 2002, Frank Warmerdam <warmerdam@pobox.com>
 * Copyright (c) 2007-2015, Even Rouault <even dot rouault at spatialys dot com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/

#include "cpl_port.h"  // Must be first.

#include "gtiff.h"

#include <cassert>
#include <cerrno>
#include <climits>
#include <cmath>
#include <cstdarg>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#if HAVE_FCNTL_H
#include <fcntl.h>
#endif
#if HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif

#include <algorithm>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <string>
#include <queue>
#include <utility>
#include <vector>

#include "cpl_conv.h"
#include "cpl_error.h"
#include "cpl_error_internal.h"
#include "cpl_mem_cache.h"
#include "cpl_md5.h"
#include "cpl_minixml.h"
#include "cpl_multiproc.h"
#include "cpl_port.h"
#include "cpl_progress.h"
#include "cpl_string.h"
#include "cpl_virtualmem.h"
#include "cpl_vsi.h"
#include "cpl_vsi_virtual.h"
#include "cpl_worker_thread_pool.h"
#include "cplkeywordparser.h"
#include "gdal.h"
#include "gdal_frmts.h"
#include "gdal_mdreader.h"
#include "gdal_pam.h"
#include "gdal_priv.h"
#include "gdal_priv_templates.hpp"
#include "gdal_thread_pool.h"
#include "geo_normalize.h"
#include "geotiff.h"
#include "geovalues.h"
#include "gt_jpeg_copy.h"
#include "gt_overview.h"
#include "gt_wkt_srs.h"
#include "gt_wkt_srs_priv.h"
#include "ogr_spatialref.h"
#include "ogr_proj_p.h"
#include "tiff.h"
#include "cpl_float.h"
#include "tiffio.h"
#include "tif_jxl.h"
#include "tiffvers.h"
#include "tifvsi.h"
#include "xtiffio.h"
#include "quant_table_md5sum.h"
#include "quant_table_md5sum_jpeg9e.h"

// Needed to expose WEBP_LOSSLESS option
#ifdef WEBP_SUPPORT
#include "webp/encode.h"
#endif

#define STRINGIFY(x) #x
#define XSTRINGIFY(x) STRINGIFY(x)

#define DEFAULT_WEBP_LEVEL 75

static bool bGlobalInExternalOvr = false;

static thread_local int gnThreadLocalLibtiffError = 0;

constexpr double DEFAULT_NODATA_VALUE = -9999.0;

// Only libtiff 4.0.4 can handle between 32768 and 65535 directories.
#if TIFFLIB_VERSION >= 20120922
#define SUPPORTS_MORE_THAN_32768_DIRECTORIES
#endif

#if TIFFLIB_VERSION > 20181110  // > 4.0.10
#define SUPPORTS_GET_OFFSET_BYTECOUNT
#endif

const char *const szJPEGGTiffDatasetTmpPrefix = "/vsimem/gtiffdataset_jpg_tmp_";

typedef enum
{
    GTIFFTAGTYPE_STRING,
    GTIFFTAGTYPE_SHORT,
    GTIFFTAGTYPE_FLOAT,
    GTIFFTAGTYPE_BYTE_STRING
} GTIFFTagTypes;

static const struct
{
    const char *pszTagName;
    int nTagVal;
    GTIFFTagTypes eType;
} asTIFFTags[] = {
    {"TIFFTAG_DOCUMENTNAME", TIFFTAG_DOCUMENTNAME, GTIFFTAGTYPE_STRING},
    {"TIFFTAG_IMAGEDESCRIPTION", TIFFTAG_IMAGEDESCRIPTION, GTIFFTAGTYPE_STRING},
    {"TIFFTAG_SOFTWARE", TIFFTAG_SOFTWARE, GTIFFTAGTYPE_STRING},
    {"TIFFTAG_DATETIME", TIFFTAG_DATETIME, GTIFFTAGTYPE_STRING},
    {"TIFFTAG_ARTIST", TIFFTAG_ARTIST, GTIFFTAGTYPE_STRING},
    {"TIFFTAG_HOSTCOMPUTER", TIFFTAG_HOSTCOMPUTER, GTIFFTAGTYPE_STRING},
    {"TIFFTAG_COPYRIGHT", TIFFTAG_COPYRIGHT, GTIFFTAGTYPE_STRING},
    {"TIFFTAG_XRESOLUTION", TIFFTAG_XRESOLUTION, GTIFFTAGTYPE_FLOAT},
    {"TIFFTAG_YRESOLUTION", TIFFTAG_YRESOLUTION, GTIFFTAGTYPE_FLOAT},
    // Dealt as special case.
    {"TIFFTAG_RESOLUTIONUNIT", TIFFTAG_RESOLUTIONUNIT, GTIFFTAGTYPE_SHORT},
    {"TIFFTAG_MINSAMPLEVALUE", TIFFTAG_MINSAMPLEVALUE, GTIFFTAGTYPE_SHORT},
    {"TIFFTAG_MAXSAMPLEVALUE", TIFFTAG_MAXSAMPLEVALUE, GTIFFTAGTYPE_SHORT},

    // GeoTIFF DGIWG tags
    {"GEO_METADATA", TIFFTAG_GEO_METADATA, GTIFFTAGTYPE_BYTE_STRING},
    {"TIFF_RSID", TIFFTAG_TIFF_RSID, GTIFFTAGTYPE_STRING},
};

const char szPROFILE_BASELINE[] = "BASELINE";
const char szPROFILE_GeoTIFF[] = "GeoTIFF";
const char szPROFILE_GDALGeoTIFF[] = "GDALGeoTIFF";

#define COMPRESSION_ENTRY(x, bWriteSupported)                                  \
    {                                                                          \
        COMPRESSION_##x, STRINGIFY(x), bWriteSupported                         \
    }

static const struct
{
    int nCode;
    const char *pszText;
    bool bWriteSupported;
} asCompressionNames[] = {
    // Compression methods in read/write mode
    COMPRESSION_ENTRY(NONE, true),
    COMPRESSION_ENTRY(CCITTRLE, true),
    COMPRESSION_ENTRY(CCITTFAX3, true),
    {COMPRESSION_CCITTFAX3, "FAX3", true},  // alternate name for write side
    COMPRESSION_ENTRY(CCITTFAX4, true),
    {COMPRESSION_CCITTFAX4, "FAX4", true},  // alternate name for write side
    COMPRESSION_ENTRY(LZW, true),
    COMPRESSION_ENTRY(JPEG, true),
    COMPRESSION_ENTRY(PACKBITS, true),
    {COMPRESSION_ADOBE_DEFLATE, "DEFLATE",
     true},  // manual entry since we want the user friendly name to be DEFLATE
    {COMPRESSION_ADOBE_DEFLATE, "ZIP", true},  // alternate name for write side
    COMPRESSION_ENTRY(LZMA, true),
    COMPRESSION_ENTRY(ZSTD, true),
    COMPRESSION_ENTRY(LERC, true),
    {COMPRESSION_LERC, "LERC_DEFLATE", true},
    {COMPRESSION_LERC, "LERC_ZSTD", true},
    COMPRESSION_ENTRY(WEBP, true),
    COMPRESSION_ENTRY(JXL, true),

    // Compression methods in read-only
    COMPRESSION_ENTRY(OJPEG, false),
    COMPRESSION_ENTRY(NEXT, false),
    COMPRESSION_ENTRY(CCITTRLEW, false),
    COMPRESSION_ENTRY(THUNDERSCAN, false),
    COMPRESSION_ENTRY(PIXARFILM, false),
    COMPRESSION_ENTRY(PIXARLOG, false),
    COMPRESSION_ENTRY(DEFLATE, false),  // COMPRESSION_DEFLATE is deprecated
    COMPRESSION_ENTRY(DCS, false),
    COMPRESSION_ENTRY(JBIG, false),
    COMPRESSION_ENTRY(SGILOG, false),
    COMPRESSION_ENTRY(SGILOG24, false),
    COMPRESSION_ENTRY(JP2000, false),
};

/************************************************************************/
/*                      GetCompressionMethodName()                      */
/************************************************************************/

static const char *GetCompressionMethodName(int nCompressionCode)
{
    for (const auto &entry : asCompressionNames)
    {
        if (entry.nCode == nCompressionCode)
        {
            return entry.pszText;
        }
    }
    return nullptr;
}

/************************************************************************/
/*                         GTIFFSupportsPredictor()                     */
/************************************************************************/

bool GTIFFSupportsPredictor(int nCompression)
{
    return nCompression == COMPRESSION_LZW ||
           nCompression == COMPRESSION_ADOBE_DEFLATE ||
           nCompression == COMPRESSION_ZSTD;
}

/************************************************************************/
/*                          GTIFFSetInExternalOvr()                     */
/************************************************************************/

void GTIFFSetInExternalOvr(bool b)
{
    bGlobalInExternalOvr = b;
}

/************************************************************************/
/*                     GTIFFGetOverviewBlockSize()                      */
/************************************************************************/

void GTIFFGetOverviewBlockSize(GDALRasterBandH hBand, int *pnBlockXSize,
                               int *pnBlockYSize)
{
    const char *pszVal = CPLGetConfigOption("GDAL_TIFF_OVR_BLOCKSIZE", nullptr);
    if (!pszVal)
    {
        GDALRasterBand *const poBand = GDALRasterBand::FromHandle(hBand);
        poBand->GetBlockSize(pnBlockXSize, pnBlockYSize);
        if (*pnBlockXSize != *pnBlockYSize || *pnBlockXSize < 64 ||
            *pnBlockXSize > 4096 || !CPLIsPowerOfTwo(*pnBlockXSize))
        {
            *pnBlockXSize = *pnBlockYSize = 128;
        }
    }
    else
    {
        int nOvrBlockSize = atoi(pszVal);
        if (nOvrBlockSize < 64 || nOvrBlockSize > 4096 ||
            !CPLIsPowerOfTwo(nOvrBlockSize))
        {
            static bool bHasWarned = false;
            if (!bHasWarned)
            {
                CPLError(CE_Warning, CPLE_NotSupported,
                         "Wrong value for GDAL_TIFF_OVR_BLOCKSIZE : %s. "
                         "Should be a power of 2 between 64 and 4096. "
                         "Defaulting to 128",
                         pszVal);
                bHasWarned = true;
            }
            nOvrBlockSize = 128;
        }

        *pnBlockXSize = nOvrBlockSize;
        *pnBlockYSize = nOvrBlockSize;
    }
}

enum
{
    ENDIANNESS_NATIVE,
    ENDIANNESS_LITTLE,
    ENDIANNESS_BIG
};

/************************************************************************/
/* ==================================================================== */
/*                          GTiffDataset                                */
/* ==================================================================== */
/************************************************************************/

class GTiffBitmapBand;
class GTiffDataset;
class GTiffJPEGOverviewBand;
class GTiffJPEGOverviewDS;
class GTiffRasterBand;
class GTiffRGBABand;

#if !defined(__MINGW32__)
namespace
{
#endif
typedef struct
{
    GTiffDataset *poDS;
    char *pszTmpFilename;
    GByte *pabyBuffer;
    GByte *pabyCompressedBuffer;  // Owned by pszTmpFilename.
    GPtrDiff_t nBufferSize;
    GPtrDiff_t nCompressedBufferSize;
    int nHeight;
    int nStripOrTile;
    uint16_t nPredictor;
    bool bTIFFIsBigEndian;
    bool bReady;
} GTiffCompressionJob;
#if !defined(__MINGW32__)
}
#endif

enum class GTiffProfile : GByte
{
    BASELINE,
    GEOTIFF,
    GDALGEOTIFF
};

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
static void ThreadDecompressionFunc(void *);
#endif

class GTiffDataset final : public GDALPamDataset
{
  public:
    struct MaskOffset
    {
        uint64_t nMask;
        uint64_t nRoundUpBitTest;
    };

  private:
    CPL_DISALLOW_COPY_ASSIGN(GTiffDataset)

    friend class GTiffBitmapBand;
    friend class GTiffJPEGOverviewDS;
    friend class GTiffJPEGOverviewBand;
    friend class GTiffOddBitsBand;
    friend class GTiffRasterBand;
    friend class GTiffRGBABand;
    friend class GTiffSplitBand;
    friend class GTiffSplitBitmapBand;

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    friend void ThreadDecompressionFunc(void *);
#endif

    friend void GTIFFSetJpegQuality(GDALDatasetH hGTIFFDS, int nJpegQuality);
    friend void GTIFFSetJpegTablesMode(GDALDatasetH hGTIFFDS,
                                       int nJpegTablesMode);
    friend void GTIFFSetWebPLevel(GDALDatasetH hGTIFFDS, int nWebPLevel);
    friend void GTIFFSetWebPLossless(GDALDatasetH hGTIFFDS, bool bWebPLossless);
    friend void GTIFFSetZLevel(GDALDatasetH hGTIFFDS, int nZLevel);
    friend void GTIFFSetZSTDLevel(GDALDatasetH hGTIFFDS, int nZSTDLevel);
    friend void GTIFFSetMaxZError(GDALDatasetH hGTIFFDS, double dfMaxZError);

    TIFF *m_hTIFF = nullptr;
    VSILFILE *m_fpL = nullptr;
    VSILFILE *m_fpToWrite = nullptr;
    GTiffDataset **m_papoOverviewDS = nullptr;
    GTiffDataset *m_poMaskDS = nullptr;  // For a non-mask dataset, points to
                                         // the corresponding (internal) mask
    GDALDataset *m_poExternalMaskDS =
        nullptr;  // Points to a dataset within m_poMaskExtOvrDS
    GTiffDataset *m_poImageryDS = nullptr;  // For a mask dataset, points to the
                                            // corresponding imagery dataset
    GTiffDataset *m_poBaseDS =
        nullptr;  // For an overview or mask dataset, points to the root dataset
    std::unique_ptr<GDALDataset>
        m_poMaskExtOvrDS{};  // Used with MASK_OVERVIEW_DATASET open option
    GTiffJPEGOverviewDS **m_papoJPEGOverviewDS = nullptr;
    GDAL_GCP *m_pasGCPList = nullptr;
    GDALColorTable *m_poColorTable = nullptr;
    char **m_papszMetadataFiles = nullptr;
    GByte *m_pabyBlockBuf = nullptr;
    char **m_papszCreationOptions = nullptr;
    void *m_pabyTempWriteBuffer = nullptr;
    CPLVirtualMem *m_pBaseMapping = nullptr;
    GByte *m_pTempBufferForCommonDirectIO = nullptr;
    CPLVirtualMem *m_psVirtualMemIOMapping = nullptr;
    CPLWorkerThreadPool *m_poThreadPool = nullptr;
    std::unique_ptr<CPLJobQueue> m_poCompressQueue{};
    CPLMutex *m_hCompressThreadPoolMutex = nullptr;

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    lru11::Cache<int, std::pair<vsi_l_offset, vsi_l_offset>>
        m_oCacheStrileToOffsetByteCount{1024};
#endif

    MaskOffset *m_panMaskOffsetLsb = nullptr;
    char *m_pszVertUnit = nullptr;
    char *m_pszFilename = nullptr;
    char *m_pszTmpFilename = nullptr;
    char *m_pszGeorefFilename = nullptr;

    double m_adfGeoTransform[6]{0, 1, 0, 0, 0, 1};
    double m_dfMaxZError = 0.0;
    uint32_t m_anLercAddCompressionAndVersion[2]{0, 0};
#if HAVE_JXL
    bool m_bJXLLossless = true;
    float m_fJXLDistance = 1.0f;
    uint32_t m_nJXLEffort = 5;
#endif
    double m_dfNoDataValue = DEFAULT_NODATA_VALUE;
    int64_t m_nNoDataValueInt64 = GDAL_PAM_DEFAULT_NODATA_VALUE_INT64;
    uint64_t m_nNoDataValueUInt64 = GDAL_PAM_DEFAULT_NODATA_VALUE_UINT64;

    toff_t m_nDirOffset = 0;

    int m_nBlocksPerBand = 0;
    int m_nBlockXSize = 0;
    int m_nBlockYSize = 0;
    int m_nLoadedBlock = -1;  // Or tile, or scanline
    uint32_t m_nRowsPerStrip = 0;
    int m_nLastBandRead = -1;        // Used for the all-in-on-strip case.
    int m_nLastWrittenBlockId = -1;  // used for m_bStreamingOut
    int m_nRefBaseMapping = 0;
    int m_nGCPCount = 0;

    GTIFFKeysFlavorEnum m_eGeoTIFFKeysFlavor = GEOTIFF_KEYS_STANDARD;
    GeoTIFFVersionEnum m_eGeoTIFFVersion = GEOTIFF_VERSION_AUTO;

    uint16_t m_nPlanarConfig = 0;
    uint16_t m_nSamplesPerPixel = 0;
    uint16_t m_nBitsPerSample = 0;
    uint16_t m_nPhotometric = 0;
    uint16_t m_nSampleFormat = 0;
    uint16_t m_nCompression = 0;

    signed char m_nOverviewCount = 0;

    // If > 0, the implicit JPEG overviews are visible through
    // GetOverviewCount().
    signed char m_nJPEGOverviewVisibilityCounter = 0;
    // Currently visible overviews. Generally == nJPEGOverviewCountOri.
    signed char m_nJPEGOverviewCount = -1;
    signed char m_nJPEGOverviewCountOri = 0;  // Size of papoJPEGOverviewDS.
    signed char m_nPAMGeorefSrcIndex = -1;
    signed char m_nINTERNALGeorefSrcIndex = -1;
    signed char m_nTABFILEGeorefSrcIndex = -1;
    signed char m_nWORLDFILEGeorefSrcIndex = -1;
    signed char m_nGeoTransformGeorefSrcIndex = -1;

    signed char m_nHasOptimizedReadMultiRange = -1;

    signed char m_nZLevel = -1;
    signed char m_nLZMAPreset = -1;
    signed char m_nZSTDLevel = -1;
    signed char m_nWebPLevel = DEFAULT_WEBP_LEVEL;
    signed char m_nJpegQuality = -1;
    signed char m_nJpegTablesMode = -1;

    enum class VirtualMemIOEnum : GByte
    {
        NO,
        YES,
        IF_ENOUGH_RAM
    };

    VirtualMemIOEnum m_eVirtualMemIOUsage = VirtualMemIOEnum::NO;

    GTiffProfile m_eProfile = GTiffProfile::GDALGEOTIFF;

    OGRSpatialReference m_oSRS{};

    GDALMultiDomainMetadata m_oGTiffMDMD{};

    std::vector<GTiffCompressionJob> m_asCompressionJobs{};
    std::queue<int> m_asQueueJobIdx{};  // queue of index of m_asCompressionJobs
                                        // being compressed in worker threads

    bool m_bStreamingIn : 1;
    bool m_bStreamingOut : 1;
    bool m_bScanDeferred : 1;
    bool m_bSingleIFDOpened = false;
    bool m_bLoadedBlockDirty : 1;
    bool m_bWriteError : 1;
    bool m_bLookedForProjection : 1;
    bool m_bLookedForMDAreaOrPoint : 1;
    bool m_bGeoTransformValid : 1;
    bool m_bCrystalized : 1;
    bool m_bGeoTIFFInfoChanged : 1;
    bool m_bForceUnsetGTOrGCPs : 1;
    bool m_bForceUnsetProjection : 1;
    bool m_bNoDataChanged : 1;
    bool m_bNoDataSet : 1;
    bool m_bNoDataSetAsInt64 : 1;
    bool m_bNoDataSetAsUInt64 : 1;
    bool m_bMetadataChanged : 1;
    bool m_bColorProfileMetadataChanged : 1;
    bool m_bForceUnsetRPC : 1;
    bool m_bNeedsRewrite : 1;
    bool m_bLoadingOtherBands : 1;
    bool m_bIsOverview : 1;
    bool m_bWriteEmptyTiles : 1;  // Whether a write of a tile entirely at
                                  // nodata/0 should go to the disk. Default is
                                  // true, unless SPARSE_OK is set
    bool m_bFillEmptyTilesAtClosing : 1;  // Might only be set to true on newly
                                          // created files, when SPARSE_OK is
                                          // not set
    bool m_bTreatAsSplit : 1;
    bool m_bTreatAsSplitBitmap : 1;
    bool m_bClipWarn : 1;
    bool m_bIMDRPCMetadataLoaded : 1;
    bool m_bEXIFMetadataLoaded : 1;
    bool m_bICCMetadataLoaded : 1;
    bool m_bHasWarnedDisableAggressiveBandCaching : 1;
    bool m_bDontReloadFirstBlock : 1;  // Hack for libtiff 3.X and #3633.
    bool m_bWebPLossless : 1;
    bool m_bPromoteTo8Bits : 1;
    bool m_bDebugDontWriteBlocks : 1;
    bool m_bIsFinalized : 1;
    bool m_bIgnoreReadErrors : 1;
    bool m_bDirectIO : 1;
    bool m_bReadGeoTransform : 1;
    bool m_bLoadPam : 1;
    bool m_bHasGotSiblingFiles : 1;
    bool m_bHasIdentifiedAuthorizedGeoreferencingSources : 1;
    bool m_bLayoutIFDSBeforeData : 1;
    bool m_bBlockOrderRowMajor : 1;
    bool m_bLeaderSizeAsUInt4 : 1;
    bool m_bTrailerRepeatedLast4BytesRepeated : 1;
    bool m_bMaskInterleavedWithImagery : 1;
    bool m_bKnownIncompatibleEdition : 1;
    bool m_bWriteKnownIncompatibleEdition : 1;
    bool m_bHasUsedReadEncodedAPI : 1;  // for debugging
    bool m_bWriteCOGLayout : 1;

    void ScanDirectories();
    bool ReadStrile(int nBlockId, void *pOutputBuffer,
                    GPtrDiff_t nBlockReqSize);
    CPLErr LoadBlockBuf(int nBlockId, bool bReadFromDisk = true);
    CPLErr FlushBlockBuf();

    void LoadMDAreaOrPoint();
    void LookForProjection();

    void Crystalize();  // TODO: Spelling.
    void RestoreVolatileParameters(TIFF *hTIFF);

    void WriteGeoTIFFInfo();
    bool SetDirectory();
    void ReloadDirectory(bool bReopenHandle = false);

    int GetJPEGOverviewCount();

    bool IsBlockAvailable(int nBlockId, vsi_l_offset *pnOffset = nullptr,
                          vsi_l_offset *pnSize = nullptr,
                          bool *pbErrOccurred = nullptr);

    void ApplyPamInfo();
    void PushMetadataToPam();

    bool WriteEncodedTile(uint32_t tile, GByte *pabyData,
                          int bPreserveDataBuffer);
    bool WriteEncodedStrip(uint32_t strip, GByte *pabyData,
                           int bPreserveDataBuffer);

    bool HasOnlyNoData(const void *pBuffer, int nWidth, int nHeight,
                       int nLineStride, int nComponents);
    inline bool IsFirstPixelEqualToNoData(const void *pBuffer);

    void FillEmptyTiles();

    void FlushDirectory();
    CPLErr CleanOverviews();

    void LoadMetadata();
    void LoadEXIFMetadata();
    void LoadICCProfile();

    CPLErr RegisterNewOverviewDataset(toff_t nOverviewOffset,
                                      int l_nJpegQuality,
                                      CSLConstList papszOptions);
    CPLErr CreateOverviewsFromSrcOverviews(GDALDataset *poSrcDS,
                                           GDALDataset *poOvrDS,
                                           int nOverviews);
    CPLErr CreateInternalMaskOverviews(int nOvrBlockXSize, int nOvrBlockYSize);
    int Finalize();

    void DiscardLsb(GByte *pabyBuffer, GPtrDiff_t nBytes, int iBand) const;
    void GetDiscardLsbOption(char **papszOptions);
    void InitCompressionThreads(bool bUpdateMode, CSLConstList papszOptions);
    void InitCreationOrOpenOptions(bool bUpdateMode, CSLConstList papszOptions);
    static void ThreadCompressionFunc(void *pData);
    void WaitCompletionForJobIdx(int i);
    void WaitCompletionForBlock(int nBlockId);
    void WriteRawStripOrTile(int nStripOrTile, GByte *pabyCompressedBuffer,
                             GPtrDiff_t nCompressedBufferSize);
    bool SubmitCompressionJob(int nStripOrTile, GByte *pabyData, GPtrDiff_t cc,
                              int nHeight);

    int GuessJPEGQuality(bool &bOutHasQuantizationTable,
                         bool &bOutHasHuffmanTable);

    void SetJPEGQualityAndTablesModeFromFile(int nQuality,
                                             bool bHasQuantizationTable,
                                             bool bHasHuffmanTable);

    int DirectIO(GDALRWFlag eRWFlag, int nXOff, int nYOff, int nXSize,
                 int nYSize, void *pData, int nBufXSize, int nBufYSize,
                 GDALDataType eBufType, int nBandCount, int *panBandMap,
                 GSpacing nPixelSpace, GSpacing nLineSpace, GSpacing nBandSpace,
                 GDALRasterIOExtraArg *psExtraArg);

    int VirtualMemIO(GDALRWFlag eRWFlag, int nXOff, int nYOff, int nXSize,
                     int nYSize, void *pData, int nBufXSize, int nBufYSize,
                     GDALDataType eBufType, int nBandCount, int *panBandMap,
                     GSpacing nPixelSpace, GSpacing nLineSpace,
                     GSpacing nBandSpace, GDALRasterIOExtraArg *psExtraArg);

    void SetStructuralMDFromParent(GTiffDataset *poParentDS);

    template <class FetchBuffer>
    CPLErr CommonDirectIO(FetchBuffer &oFetcher, int nXOff, int nYOff,
                          int nXSize, int nYSize, void *pData, int nBufXSize,
                          int nBufYSize, GDALDataType eBufType, int nBandCount,
                          int *panBandMap, GSpacing nPixelSpace,
                          GSpacing nLineSpace, GSpacing nBandSpace);

    void LoadGeoreferencingAndPamIfNeeded();

    char **GetSiblingFiles();

    void IdentifyAuthorizedGeoreferencingSources();

    void FlushCacheInternal(bool bAtClosing, bool bFlushDirectory);
    bool HasOptimizedReadMultiRange();

    bool AssociateExternalMask();

    static bool MustCreateInternalMask();

    static CPLErr CopyImageryAndMask(GTiffDataset *poDstDS,
                                     GDALDataset *poSrcDS,
                                     GDALRasterBand *poSrcMaskBand,
                                     GDALProgressFunc pfnProgress,
                                     void *pProgressData);

    bool GetOverviewParameters(int &nCompression, uint16_t &nPlanarConfig,
                               uint16_t &nPredictor, uint16_t &nPhotometric,
                               int &nOvrJpegQuality, std::string &osNoData,
                               uint16_t *&panExtraSampleValues,
                               uint16_t &nExtraSamples,
                               CSLConstList papszOptions) const;

  protected:
    virtual int CloseDependentDatasets() override;

  public:
    GTiffDataset();
    virtual ~GTiffDataset();

    const OGRSpatialReference *GetSpatialRef() const override;
    CPLErr SetSpatialRef(const OGRSpatialReference *poSRS) override;

    virtual CPLErr GetGeoTransform(double *) override;
    virtual CPLErr SetGeoTransform(double *) override;

    virtual int GetGCPCount() override;
    const OGRSpatialReference *GetGCPSpatialRef() const override;
    virtual const GDAL_GCP *GetGCPs() override;
    CPLErr SetGCPs(int nGCPCountIn, const GDAL_GCP *pasGCPListIn,
                   const OGRSpatialReference *poSRS) override;
#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    bool IsMultiThreadedReadCompatible() const;
    CPLErr MultiThreadedRead(int nXOff, int nYOff, int nXSize, int nYSize,
                             void *pData, GDALDataType eBufType, int nBandCount,
                             const int *panBandMap, GSpacing nPixelSpace,
                             GSpacing nLineSpace, GSpacing nBandSpace);
#endif
    virtual CPLErr IRasterIO(GDALRWFlag eRWFlag, int nXOff, int nYOff,
                             int nXSize, int nYSize, void *pData, int nBufXSize,
                             int nBufYSize, GDALDataType eBufType,
                             int nBandCount, int *panBandMap,
                             GSpacing nPixelSpace, GSpacing nLineSpace,
                             GSpacing nBandSpace,
                             GDALRasterIOExtraArg *psExtraArg) override;
    virtual char **GetFileList() override;

    virtual CPLErr IBuildOverviews(const char *, int, const int *, int,
                                   const int *, GDALProgressFunc, void *,
                                   CSLConstList papszOptions) override;

    CPLErr OpenOffset(TIFF *, toff_t nDirOffset, GDALAccess,
                      bool bAllowRGBAInterface = true,
                      bool bReadGeoTransform = false);

    static GDALDataset *OpenDir(GDALOpenInfo *);
    static GDALDataset *Open(GDALOpenInfo *);
    static int Identify(GDALOpenInfo *);
    static GDALDataset *Create(const char *pszFilename, int nXSize, int nYSize,
                               int nBands, GDALDataType eType,
                               char **papszParamList);
    static GDALDataset *CreateCopy(const char *pszFilename,
                                   GDALDataset *poSrcDS, int bStrict,
                                   char **papszOptions,
                                   GDALProgressFunc pfnProgress,
                                   void *pProgressData);
    virtual void FlushCache(bool bAtClosing) override;

    virtual char **GetMetadataDomainList() override;
    virtual CPLErr SetMetadata(char **, const char * = "") override;
    virtual char **GetMetadata(const char *pszDomain = "") override;
    virtual CPLErr SetMetadataItem(const char *, const char *,
                                   const char * = "") override;
    virtual const char *GetMetadataItem(const char *pszName,
                                        const char *pszDomain = "") override;
    virtual void *GetInternalHandle(const char *) override;

    virtual CPLErr CreateMaskBand(int nFlags) override;

    bool GetRawBinaryLayout(GDALDataset::RawBinaryLayout &) override;

    // Only needed by createcopy and close code.
    static void WriteRPC(GDALDataset *, TIFF *, int, GTiffProfile, const char *,
                         char **, bool bWriteOnlyInPAMIfNeeded = false);
    static bool WriteMetadata(GDALDataset *, TIFF *, bool, GTiffProfile,
                              const char *, char **,
                              bool bExcludeRPBandIMGFileWriting = false);
    static void WriteNoDataValue(TIFF *, double);
    static void WriteNoDataValue(TIFF *, int64_t);
    static void WriteNoDataValue(TIFF *, uint64_t);
    static void UnsetNoDataValue(TIFF *);

    static TIFF *CreateLL(const char *pszFilename, int nXSize, int nYSize,
                          int nBands, GDALDataType eType,
                          double dfExtraSpaceForOverviews,
                          char **papszParamList, VSILFILE **pfpL,
                          CPLString &osTmpFilename);

    CPLErr WriteEncodedTileOrStrip(uint32_t tile_or_strip, void *data,
                                   int bPreserveDataBuffer);

    static void SaveICCProfile(GTiffDataset *pDS, TIFF *hTIFF,
                               char **papszParamList, uint32_t nBitsPerSample);
};

/************************************************************************/
/* ==================================================================== */
/*                        GTiffJPEGOverviewDS                           */
/* ==================================================================== */
/************************************************************************/

class GTiffJPEGOverviewDS final : public GDALDataset
{
    CPL_DISALLOW_COPY_ASSIGN(GTiffJPEGOverviewDS)

    friend class GTiffJPEGOverviewBand;
    GTiffDataset *m_poParentDS = nullptr;
    int m_nOverviewLevel = 0;

    int m_nJPEGTableSize = 0;
    GByte *m_pabyJPEGTable = nullptr;
    CPLString m_osTmpFilenameJPEGTable{};

    CPLString m_osTmpFilename{};
    std::unique_ptr<GDALDataset> m_poJPEGDS{};
    // Valid block id of the parent DS that match poJPEGDS.
    int m_nBlockId = -1;

  public:
    GTiffJPEGOverviewDS(GTiffDataset *poParentDS, int nOverviewLevel,
                        const void *pJPEGTable, int nJPEGTableSize);
    virtual ~GTiffJPEGOverviewDS();

    virtual CPLErr IRasterIO(GDALRWFlag eRWFlag, int nXOff, int nYOff,
                             int nXSize, int nYSize, void *pData, int nBufXSize,
                             int nBufYSize, GDALDataType eBufType,
                             int nBandCount, int *panBandMap,
                             GSpacing nPixelSpace, GSpacing nLineSpace,
                             GSpacing nBandSpace,
                             GDALRasterIOExtraArg *psExtraArg) override;
};

class GTiffJPEGOverviewBand final : public GDALRasterBand
{
  public:
    GTiffJPEGOverviewBand(GTiffJPEGOverviewDS *poDS, int nBand);
    virtual ~GTiffJPEGOverviewBand()
    {
    }

    virtual CPLErr IReadBlock(int, int, void *) override;
};

/************************************************************************/
/*                        GTiffJPEGOverviewDS()                         */
/************************************************************************/

GTiffJPEGOverviewDS::GTiffJPEGOverviewDS(GTiffDataset *poParentDSIn,
                                         int nOverviewLevelIn,
                                         const void *pJPEGTable,
                                         int nJPEGTableSizeIn)
    : m_poParentDS(poParentDSIn), m_nOverviewLevel(nOverviewLevelIn),
      m_nJPEGTableSize(nJPEGTableSizeIn)
{
    ShareLockWithParentDataset(poParentDSIn);

    m_osTmpFilenameJPEGTable.Printf("/vsimem/jpegtable_%p", this);

    const GByte abyAdobeAPP14RGB[] = {0xFF, 0xEE, 0x00, 0x0E, 0x41, 0x64,
                                      0x6F, 0x62, 0x65, 0x00, 0x64, 0x00,
                                      0x00, 0x00, 0x00, 0x00};
    const bool bAddAdobe =
        m_poParentDS->m_nPlanarConfig == PLANARCONFIG_CONTIG &&
        m_poParentDS->m_nPhotometric != PHOTOMETRIC_YCBCR &&
        m_poParentDS->nBands == 3;
    m_pabyJPEGTable = static_cast<GByte *>(CPLMalloc(
        m_nJPEGTableSize + (bAddAdobe ? sizeof(abyAdobeAPP14RGB) : 0)));
    memcpy(m_pabyJPEGTable, pJPEGTable, m_nJPEGTableSize);
    if (bAddAdobe)
    {
        memcpy(m_pabyJPEGTable + m_nJPEGTableSize, abyAdobeAPP14RGB,
               sizeof(abyAdobeAPP14RGB));
        m_nJPEGTableSize += sizeof(abyAdobeAPP14RGB);
    }
    CPL_IGNORE_RET_VAL(VSIFCloseL(VSIFileFromMemBuffer(
        m_osTmpFilenameJPEGTable, m_pabyJPEGTable, m_nJPEGTableSize, TRUE)));

    const int nScaleFactor = 1 << m_nOverviewLevel;
    nRasterXSize =
        (m_poParentDS->nRasterXSize + nScaleFactor - 1) / nScaleFactor;
    nRasterYSize =
        (m_poParentDS->nRasterYSize + nScaleFactor - 1) / nScaleFactor;

    for (int i = 1; i <= m_poParentDS->nBands; ++i)
        SetBand(i, new GTiffJPEGOverviewBand(this, i));

    SetMetadataItem("INTERLEAVE", "PIXEL", "IMAGE_STRUCTURE");
    if (m_poParentDS->m_nPhotometric == PHOTOMETRIC_YCBCR)
        SetMetadataItem("COMPRESSION", "YCbCr JPEG", "IMAGE_STRUCTURE");
    else
        SetMetadataItem("COMPRESSION", "JPEG", "IMAGE_STRUCTURE");
}

/************************************************************************/
/*                       ~GTiffJPEGOverviewDS()                         */
/************************************************************************/

GTiffJPEGOverviewDS::~GTiffJPEGOverviewDS()
{
    m_poJPEGDS.reset();
    VSIUnlink(m_osTmpFilenameJPEGTable);
    if (!m_osTmpFilename.empty())
        VSIUnlink(m_osTmpFilename);
}

/************************************************************************/
/*                            IRasterIO()                               */
/************************************************************************/

CPLErr GTiffJPEGOverviewDS::IRasterIO(GDALRWFlag eRWFlag, int nXOff, int nYOff,
                                      int nXSize, int nYSize, void *pData,
                                      int nBufXSize, int nBufYSize,
                                      GDALDataType eBufType, int nBandCount,
                                      int *panBandMap, GSpacing nPixelSpace,
                                      GSpacing nLineSpace, GSpacing nBandSpace,
                                      GDALRasterIOExtraArg *psExtraArg)

{
    // For non-single strip JPEG-IN-TIFF, the block based strategy will
    // be the most efficient one, to avoid decompressing the JPEG content
    // for each requested band.
    if (nBandCount > 1 &&
        m_poParentDS->m_nPlanarConfig == PLANARCONFIG_CONTIG &&
        (m_poParentDS->m_nBlockXSize < m_poParentDS->nRasterXSize ||
         m_poParentDS->m_nBlockYSize > 1))
    {
        return BlockBasedRasterIO(eRWFlag, nXOff, nYOff, nXSize, nYSize, pData,
                                  nBufXSize, nBufYSize, eBufType, nBandCount,
                                  panBandMap, nPixelSpace, nLineSpace,
                                  nBandSpace, psExtraArg);
    }

    return GDALDataset::IRasterIO(eRWFlag, nXOff, nYOff, nXSize, nYSize, pData,
                                  nBufXSize, nBufYSize, eBufType, nBandCount,
                                  panBandMap, nPixelSpace, nLineSpace,
                                  nBandSpace, psExtraArg);
}

/************************************************************************/
/*                        GTiffJPEGOverviewBand()                       */
/************************************************************************/

GTiffJPEGOverviewBand::GTiffJPEGOverviewBand(GTiffJPEGOverviewDS *poDSIn,
                                             int nBandIn)
{
    poDS = poDSIn;
    nBand = nBandIn;
    eDataType =
        poDSIn->m_poParentDS->GetRasterBand(nBandIn)->GetRasterDataType();
    poDSIn->m_poParentDS->GetRasterBand(nBandIn)->GetBlockSize(&nBlockXSize,
                                                               &nBlockYSize);
    const int nScaleFactor = 1 << poDSIn->m_nOverviewLevel;
    nBlockXSize = (nBlockXSize + nScaleFactor - 1) / nScaleFactor;
    nBlockYSize = (nBlockYSize + nScaleFactor - 1) / nScaleFactor;
}

/************************************************************************/
/*                          IReadBlock()                                */
/************************************************************************/

CPLErr GTiffJPEGOverviewBand::IReadBlock(int nBlockXOff, int nBlockYOff,
                                         void *pImage)
{
    GTiffJPEGOverviewDS *m_poGDS = cpl::down_cast<GTiffJPEGOverviewDS *>(poDS);

    // Compute the source block ID.
    int nBlockId = 0;
    int nParentBlockXSize, nParentBlockYSize;
    m_poGDS->m_poParentDS->GetRasterBand(1)->GetBlockSize(&nParentBlockXSize,
                                                          &nParentBlockYSize);
    const bool bIsSingleStripAsSplit =
        (nParentBlockYSize == 1 &&
         m_poGDS->m_poParentDS->m_nBlockYSize != nParentBlockYSize);
    if (!bIsSingleStripAsSplit)
    {
        int l_nBlocksPerRow =
            DIV_ROUND_UP(m_poGDS->m_poParentDS->nRasterXSize,
                         m_poGDS->m_poParentDS->m_nBlockXSize);
        nBlockId = nBlockYOff * l_nBlocksPerRow + nBlockXOff;
    }
    if (m_poGDS->m_poParentDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
    {
        nBlockId += (nBand - 1) * m_poGDS->m_poParentDS->m_nBlocksPerBand;
    }

    // Make sure it is available.
    const int nDataTypeSize = GDALGetDataTypeSizeBytes(eDataType);
    vsi_l_offset nOffset = 0;
    vsi_l_offset nByteCount = 0;
    bool bErrOccurred = false;
    if (!m_poGDS->m_poParentDS->IsBlockAvailable(nBlockId, &nOffset,
                                                 &nByteCount, &bErrOccurred))
    {
        memset(pImage, 0,
               static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize *
                   nDataTypeSize);
        if (bErrOccurred)
            return CE_Failure;
        return CE_None;
    }

    const int nScaleFactor = 1 << m_poGDS->m_nOverviewLevel;
    if (m_poGDS->m_poJPEGDS == nullptr || nBlockId != m_poGDS->m_nBlockId)
    {
        if (nByteCount < 2)
            return CE_Failure;
        nOffset += 2;  // Skip leading 0xFF 0xF8.
        nByteCount -= 2;

        // Special case for last strip that might be smaller than other strips
        // In which case we must invalidate the dataset.
        TIFF *hTIFF = m_poGDS->m_poParentDS->m_hTIFF;
        if (!TIFFIsTiled(hTIFF) && !bIsSingleStripAsSplit &&
            (nBlockYOff + 1 ==
                 DIV_ROUND_UP(m_poGDS->m_poParentDS->nRasterYSize,
                              m_poGDS->m_poParentDS->m_nBlockYSize) ||
             (m_poGDS->m_poJPEGDS != nullptr &&
              m_poGDS->m_poJPEGDS->GetRasterYSize() !=
                  nBlockYSize * nScaleFactor)))
        {
            m_poGDS->m_poJPEGDS.reset();
        }

        CPLString osFileToOpen;
        m_poGDS->m_osTmpFilename.Printf("/vsimem/sparse_%p", m_poGDS);
        VSILFILE *fp = VSIFOpenL(m_poGDS->m_osTmpFilename, "wb+");

        // If the size of the JPEG strip/tile is small enough, we will
        // read it from the TIFF file and forge a in-memory JPEG file with
        // the JPEG table followed by the JPEG data.
        const bool bInMemoryJPEGFile = nByteCount < 256 * 256;
        if (bInMemoryJPEGFile)
        {
            // If the previous file was opened as a /vsisparse/, must re-open.
            if (m_poGDS->m_poJPEGDS != nullptr &&
                STARTS_WITH(m_poGDS->m_poJPEGDS->GetDescription(),
                            "/vsisparse/"))
            {
                m_poGDS->m_poJPEGDS.reset();
            }
            osFileToOpen = m_poGDS->m_osTmpFilename;

            bool bError = false;
            if (VSIFSeekL(fp, m_poGDS->m_nJPEGTableSize + nByteCount - 1,
                          SEEK_SET) != 0)
                bError = true;
            char ch = 0;
            if (!bError && VSIFWriteL(&ch, 1, 1, fp) != 1)
                bError = true;
            GByte *pabyBuffer =
                VSIGetMemFileBuffer(m_poGDS->m_osTmpFilename, nullptr, FALSE);
            memcpy(pabyBuffer, m_poGDS->m_pabyJPEGTable,
                   m_poGDS->m_nJPEGTableSize);
            VSILFILE *fpTIF = VSI_TIFFGetVSILFile(TIFFClientdata(hTIFF));
            if (!bError && VSIFSeekL(fpTIF, nOffset, SEEK_SET) != 0)
                bError = true;
            if (VSIFReadL(pabyBuffer + m_poGDS->m_nJPEGTableSize,
                          static_cast<size_t>(nByteCount), 1, fpTIF) != 1)
                bError = true;
            if (bError)
            {
                CPL_IGNORE_RET_VAL(VSIFCloseL(fp));
                return CE_Failure;
            }
        }
        else
        {
            // If the JPEG strip/tile is too big (e.g. a single-strip
            // JPEG-in-TIFF), we will use /vsisparse mechanism to make a
            // fake JPEG file.

            // Always re-open.
            m_poGDS->m_poJPEGDS.reset();

            osFileToOpen =
                CPLSPrintf("/vsisparse/%s", m_poGDS->m_osTmpFilename.c_str());

            if (VSIFPrintfL(fp,
                            "<VSISparseFile><SubfileRegion>"
                            "<Filename relative='0'>%s</Filename>"
                            "<DestinationOffset>0</DestinationOffset>"
                            "<SourceOffset>0</SourceOffset>"
                            "<RegionLength>%d</RegionLength>"
                            "</SubfileRegion>"
                            "<SubfileRegion>"
                            "<Filename relative='0'>%s</Filename>"
                            "<DestinationOffset>%d</DestinationOffset>"
                            "<SourceOffset>" CPL_FRMT_GUIB "</SourceOffset>"
                            "<RegionLength>" CPL_FRMT_GUIB "</RegionLength>"
                            "</SubfileRegion></VSISparseFile>",
                            m_poGDS->m_osTmpFilenameJPEGTable.c_str(),
                            static_cast<int>(m_poGDS->m_nJPEGTableSize),
                            m_poGDS->m_poParentDS->GetDescription(),
                            static_cast<int>(m_poGDS->m_nJPEGTableSize),
                            nOffset, nByteCount) < 0)
            {
                CPL_IGNORE_RET_VAL(VSIFCloseL(fp));
                return CE_Failure;
            }
        }
        CPL_IGNORE_RET_VAL(VSIFCloseL(fp));

        if (m_poGDS->m_poJPEGDS == nullptr)
        {
            const char *const apszDrivers[] = {"JPEG", nullptr};

            CPLConfigOptionSetter oJPEGtoRGBSetter(
                "GDAL_JPEG_TO_RGB",
                m_poGDS->m_poParentDS->m_nPlanarConfig == PLANARCONFIG_CONTIG &&
                        m_poGDS->nBands == 4
                    ? "NO"
                    : "YES",
                false);

            m_poGDS->m_poJPEGDS.reset(GDALDataset::Open(
                osFileToOpen, GDAL_OF_RASTER | GDAL_OF_INTERNAL, apszDrivers,
                nullptr, nullptr));

            if (m_poGDS->m_poJPEGDS != nullptr)
            {
                // Force all implicit overviews to be available, even for
                // small tiles.
                CPLConfigOptionSetter oInternalOverviewsSetter(
                    "JPEG_FORCE_INTERNAL_OVERVIEWS", "YES", false);
                GDALGetOverviewCount(
                    GDALGetRasterBand(m_poGDS->m_poJPEGDS.get(), 1));

                m_poGDS->m_nBlockId = nBlockId;
            }
        }
        else
        {
            // Trick: we invalidate the JPEG dataset to force a reload
            // of the new content.
            CPLErrorReset();
            m_poGDS->m_poJPEGDS->FlushCache(false);
            if (CPLGetLastErrorNo() != 0)
            {
                m_poGDS->m_poJPEGDS.reset();
                return CE_Failure;
            }
            m_poGDS->m_nBlockId = nBlockId;
        }
    }

    CPLErr eErr = CE_Failure;
    if (m_poGDS->m_poJPEGDS)
    {
        GDALDataset *l_poDS = m_poGDS->m_poJPEGDS.get();

        int nReqXOff = 0;
        int nReqYOff = 0;
        int nReqXSize = 0;
        int nReqYSize = 0;
        if (bIsSingleStripAsSplit)
        {
            nReqYOff = nBlockYOff * nScaleFactor;
            nReqXSize = l_poDS->GetRasterXSize();
            nReqYSize = nScaleFactor;
        }
        else
        {
            if (nBlockXSize == m_poGDS->GetRasterXSize())
            {
                nReqXSize = l_poDS->GetRasterXSize();
            }
            else
            {
                nReqXSize = nBlockXSize * nScaleFactor;
            }
            nReqYSize = nBlockYSize * nScaleFactor;
        }
        int nBufXSize = nBlockXSize;
        int nBufYSize = nBlockYSize;
        if (nBlockXOff == DIV_ROUND_UP(m_poGDS->m_poParentDS->nRasterXSize,
                                       m_poGDS->m_poParentDS->m_nBlockXSize) -
                              1)
        {
            nReqXSize = m_poGDS->m_poParentDS->nRasterXSize -
                        nBlockXOff * m_poGDS->m_poParentDS->m_nBlockXSize;
        }
        if (nReqXOff + nReqXSize > l_poDS->GetRasterXSize())
        {
            nReqXSize = l_poDS->GetRasterXSize() - nReqXOff;
        }
        if (!bIsSingleStripAsSplit &&
            nBlockYOff == DIV_ROUND_UP(m_poGDS->m_poParentDS->nRasterYSize,
                                       m_poGDS->m_poParentDS->m_nBlockYSize) -
                              1)
        {
            nReqYSize = m_poGDS->m_poParentDS->nRasterYSize -
                        nBlockYOff * m_poGDS->m_poParentDS->m_nBlockYSize;
        }
        if (nReqYOff + nReqYSize > l_poDS->GetRasterYSize())
        {
            nReqYSize = l_poDS->GetRasterYSize() - nReqYOff;
        }
        if (nBlockXOff * nBlockXSize > m_poGDS->GetRasterXSize() - nBufXSize)
        {
            memset(pImage, 0,
                   static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize *
                       nDataTypeSize);
            nBufXSize = m_poGDS->GetRasterXSize() - nBlockXOff * nBlockXSize;
        }
        if (nBlockYOff * nBlockYSize > m_poGDS->GetRasterYSize() - nBufYSize)
        {
            memset(pImage, 0,
                   static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize *
                       nDataTypeSize);
            nBufYSize = m_poGDS->GetRasterYSize() - nBlockYOff * nBlockYSize;
        }

        const int nSrcBand =
            m_poGDS->m_poParentDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE
                ? 1
                : nBand;
        if (nSrcBand <= l_poDS->GetRasterCount())
        {
            eErr = l_poDS->GetRasterBand(nSrcBand)->RasterIO(
                GF_Read, nReqXOff, nReqYOff, nReqXSize, nReqYSize, pImage,
                nBufXSize, nBufYSize, eDataType, 0,
                static_cast<GPtrDiff_t>(nBlockXSize) * nDataTypeSize, nullptr);
        }
    }

    return eErr;
}

/************************************************************************/
/*                        GTIFFSetJpegQuality()                         */
/* Called by GTIFFBuildOverviews() to set the jpeg quality on the IFD   */
/* of the .ovr file.                                                    */
/************************************************************************/

void GTIFFSetJpegQuality(GDALDatasetH hGTIFFDS, int nJpegQuality)
{
    CPLAssert(
        EQUAL(GDALGetDriverShortName(GDALGetDatasetDriver(hGTIFFDS)), "GTIFF"));

    GTiffDataset *const poDS = static_cast<GTiffDataset *>(hGTIFFDS);
    poDS->m_nJpegQuality = static_cast<signed char>(nJpegQuality);

    poDS->ScanDirectories();

    for (int i = 0; i < poDS->m_nOverviewCount; ++i)
        poDS->m_papoOverviewDS[i]->m_nJpegQuality = poDS->m_nJpegQuality;
}

/************************************************************************/
/*                        GTIFFSetWebPLevel()                         */
/* Called by GTIFFBuildOverviews() to set the jpeg quality on the IFD   */
/* of the .ovr file.                                                    */
/************************************************************************/

void GTIFFSetWebPLevel(GDALDatasetH hGTIFFDS, int nWebpLevel)
{
    CPLAssert(
        EQUAL(GDALGetDriverShortName(GDALGetDatasetDriver(hGTIFFDS)), "GTIFF"));

    GTiffDataset *const poDS = static_cast<GTiffDataset *>(hGTIFFDS);
    poDS->m_nWebPLevel = static_cast<signed char>(nWebpLevel);

    poDS->ScanDirectories();

    for (int i = 0; i < poDS->m_nOverviewCount; ++i)
        poDS->m_papoOverviewDS[i]->m_nWebPLevel = poDS->m_nWebPLevel;
}

/************************************************************************/
/*                       GTIFFSetWebPLossless()                         */
/* Called by GTIFFBuildOverviews() to set webp lossless on the IFD      */
/* of the .ovr file.                                                    */
/************************************************************************/

void GTIFFSetWebPLossless(GDALDatasetH hGTIFFDS, bool bWebpLossless)
{
    CPLAssert(
        EQUAL(GDALGetDriverShortName(GDALGetDatasetDriver(hGTIFFDS)), "GTIFF"));

    GTiffDataset *const poDS = static_cast<GTiffDataset *>(hGTIFFDS);
    poDS->m_bWebPLossless = bWebpLossless;

    poDS->ScanDirectories();

    for (int i = 0; i < poDS->m_nOverviewCount; ++i)
        poDS->m_papoOverviewDS[i]->m_bWebPLossless = poDS->m_bWebPLossless;
}

/************************************************************************/
/*                     GTIFFSetJpegTablesMode()                         */
/* Called by GTIFFBuildOverviews() to set the jpeg tables mode on the   */
/* of the .ovr file.                                                    */
/************************************************************************/

void GTIFFSetJpegTablesMode(GDALDatasetH hGTIFFDS, int nJpegTablesMode)
{
    CPLAssert(
        EQUAL(GDALGetDriverShortName(GDALGetDatasetDriver(hGTIFFDS)), "GTIFF"));

    GTiffDataset *const poDS = static_cast<GTiffDataset *>(hGTIFFDS);
    poDS->m_nJpegTablesMode = static_cast<signed char>(nJpegTablesMode);

    poDS->ScanDirectories();

    for (int i = 0; i < poDS->m_nOverviewCount; ++i)
        poDS->m_papoOverviewDS[i]->m_nJpegTablesMode = poDS->m_nJpegTablesMode;
}

/************************************************************************/
/*                        GTIFFSetZLevel()                              */
/* Called by GTIFFBuildOverviews() to set the deflate level on the IFD  */
/* of the .ovr file.                                                    */
/************************************************************************/

void GTIFFSetZLevel(GDALDatasetH hGTIFFDS, int nZLevel)
{
    CPLAssert(
        EQUAL(GDALGetDriverShortName(GDALGetDatasetDriver(hGTIFFDS)), "GTIFF"));

    GTiffDataset *const poDS = static_cast<GTiffDataset *>(hGTIFFDS);
    poDS->m_nZLevel = static_cast<signed char>(nZLevel);

    poDS->ScanDirectories();

    for (int i = 0; i < poDS->m_nOverviewCount; ++i)
        poDS->m_papoOverviewDS[i]->m_nZLevel = poDS->m_nZLevel;
}

/************************************************************************/
/*                        GTIFFSetZSTDLevel()                           */
/* Called by GTIFFBuildOverviews() to set the ZSTD level on the IFD     */
/* of the .ovr file.                                                    */
/************************************************************************/

void GTIFFSetZSTDLevel(GDALDatasetH hGTIFFDS, int nZSTDLevel)
{
    CPLAssert(
        EQUAL(GDALGetDriverShortName(GDALGetDatasetDriver(hGTIFFDS)), "GTIFF"));

    GTiffDataset *const poDS = static_cast<GTiffDataset *>(hGTIFFDS);
    poDS->m_nZSTDLevel = static_cast<signed char>(nZSTDLevel);

    poDS->ScanDirectories();

    for (int i = 0; i < poDS->m_nOverviewCount; ++i)
        poDS->m_papoOverviewDS[i]->m_nZSTDLevel = poDS->m_nZSTDLevel;
}

/************************************************************************/
/*                        GTIFFSetMaxZError()                           */
/* Called by GTIFFBuildOverviews() to set the Lerc max error on the IFD */
/* of the .ovr file.                                                    */
/************************************************************************/

void GTIFFSetMaxZError(GDALDatasetH hGTIFFDS, double dfMaxZError)
{
    CPLAssert(
        EQUAL(GDALGetDriverShortName(GDALGetDatasetDriver(hGTIFFDS)), "GTIFF"));

    GTiffDataset *const poDS = static_cast<GTiffDataset *>(hGTIFFDS);
    poDS->m_dfMaxZError = dfMaxZError;

    poDS->ScanDirectories();

    for (int i = 0; i < poDS->m_nOverviewCount; ++i)
        poDS->m_papoOverviewDS[i]->m_dfMaxZError = poDS->m_dfMaxZError;
}

/************************************************************************/
/* ==================================================================== */
/*                            GTiffRasterBand                           */
/* ==================================================================== */
/************************************************************************/

class GTiffRasterBand CPL_NON_FINAL : public GDALPamRasterBand
{
    CPL_DISALLOW_COPY_ASSIGN(GTiffRasterBand)

    friend class GTiffDataset;

    double m_dfOffset = 0;
    double m_dfScale = 1;
    CPLString m_osUnitType{};
    CPLString m_osDescription{};
    GDALColorInterp m_eBandInterp = GCI_Undefined;
    std::set<GTiffRasterBand **> m_aSetPSelf{};
    bool m_bHaveOffsetScale = false;

    int DirectIO(GDALRWFlag eRWFlag, int nXOff, int nYOff, int nXSize,
                 int nYSize, void *pData, int nBufXSize, int nBufYSize,
                 GDALDataType eBufType, GSpacing nPixelSpace,
                 GSpacing nLineSpace, GDALRasterIOExtraArg *psExtraArg);

    static void DropReferenceVirtualMem(void *pUserData);
    CPLVirtualMem *GetVirtualMemAutoInternal(GDALRWFlag eRWFlag,
                                             int *pnPixelSpace,
                                             GIntBig *pnLineSpace,
                                             char **papszOptions);

    void *CacheMultiRange(int nXOff, int nYOff, int nXSize, int nYSize,
                          int nBufXSize, int nBufYSize,
                          GDALRasterIOExtraArg *psExtraArg);

  protected:
    GTiffDataset *m_poGDS = nullptr;
    GDALMultiDomainMetadata m_oGTiffMDMD{};

    double m_dfNoDataValue = DEFAULT_NODATA_VALUE;
    bool m_bNoDataSet = false;

    int64_t m_nNoDataValueInt64 = GDAL_PAM_DEFAULT_NODATA_VALUE_INT64;
    bool m_bNoDataSetAsInt64 = false;

    uint64_t m_nNoDataValueUInt64 = GDAL_PAM_DEFAULT_NODATA_VALUE_UINT64;
    bool m_bNoDataSetAsUInt64 = false;

    void NullBlock(void *pData);
    CPLErr FillCacheForOtherBands(int nBlockXOff, int nBlockYOff);
#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    void CacheMaskForBlock(int nBlockXOff, int nBlockYOff);
#endif

    void ResetNoDataValues(bool bResetDatasetToo);

  public:
    GTiffRasterBand(GTiffDataset *, int);
    virtual ~GTiffRasterBand();

    virtual bool IsBaseGTiffClass() const
    {
        return true;
    }

    virtual CPLErr IReadBlock(int, int, void *) override;
    virtual CPLErr IWriteBlock(int, int, void *) override;

    virtual GDALSuggestedBlockAccessPattern
    GetSuggestedBlockAccessPattern() const override
    {
        return GSBAP_RANDOM;
    }

    virtual int IGetDataCoverageStatus(int nXOff, int nYOff, int nXSize,
                                       int nYSize, int nMaskFlagStop,
                                       double *pdfDataPct) override;

    virtual CPLErr IRasterIO(GDALRWFlag eRWFlag, int nXOff, int nYOff,
                             int nXSize, int nYSize, void *pData, int nBufXSize,
                             int nBufYSize, GDALDataType eBufType,
                             GSpacing nPixelSpace, GSpacing nLineSpace,
                             GDALRasterIOExtraArg *psExtraArg) override final;

    virtual const char *GetDescription() const override final;
    virtual void SetDescription(const char *) override final;

    virtual GDALColorInterp GetColorInterpretation() override /*final*/;
    virtual GDALColorTable *GetColorTable() override /*final*/;
    virtual CPLErr SetColorTable(GDALColorTable *) override final;

    CPLErr SetNoDataValue(double) override final;
    CPLErr SetNoDataValueAsInt64(int64_t nNoData) override final;
    CPLErr SetNoDataValueAsUInt64(uint64_t nNoData) override final;
    double GetNoDataValue(int *pbSuccess = nullptr) override final;
    int64_t GetNoDataValueAsInt64(int *pbSuccess = nullptr) override final;
    uint64_t GetNoDataValueAsUInt64(int *pbSuccess = nullptr) override final;
    CPLErr DeleteNoDataValue() override final;

    virtual double GetOffset(int *pbSuccess = nullptr) override final;
    virtual CPLErr SetOffset(double dfNewValue) override final;
    virtual double GetScale(int *pbSuccess = nullptr) override final;
    virtual CPLErr SetScale(double dfNewValue) override final;
    virtual const char *GetUnitType() override final;
    virtual CPLErr SetUnitType(const char *pszNewValue) override final;
    virtual CPLErr SetColorInterpretation(GDALColorInterp) override final;

    virtual char **GetMetadataDomainList() override final;
    virtual CPLErr SetMetadata(char **, const char * = "") override final;
    virtual char **GetMetadata(const char *pszDomain = "") override final;
    virtual CPLErr SetMetadataItem(const char *, const char *,
                                   const char * = "") override final;
    virtual const char *
    GetMetadataItem(const char *pszName,
                    const char *pszDomain = "") override final;
    virtual int GetOverviewCount() override final;
    virtual GDALRasterBand *GetOverview(int) override final;

    virtual GDALRasterBand *GetMaskBand() override final;
    virtual int GetMaskFlags() override final;
    virtual CPLErr CreateMaskBand(int nFlags) override final;
    virtual bool IsMaskBand() const override final;
    virtual GDALMaskValueRange GetMaskValueRange() const override final;

    virtual CPLVirtualMem *
    GetVirtualMemAuto(GDALRWFlag eRWFlag, int *pnPixelSpace,
                      GIntBig *pnLineSpace, char **papszOptions) override final;

    GDALRasterAttributeTable *GetDefaultRAT() override final;
    virtual CPLErr
    SetDefaultRAT(const GDALRasterAttributeTable *) override final;
    virtual CPLErr GetHistogram(double dfMin, double dfMax, int nBuckets,
                                GUIntBig *panHistogram, int bIncludeOutOfRange,
                                int bApproxOK, GDALProgressFunc,
                                void *pProgressData) override final;

    virtual CPLErr GetDefaultHistogram(double *pdfMin, double *pdfMax,
                                       int *pnBuckets, GUIntBig **ppanHistogram,
                                       int bForce, GDALProgressFunc,
                                       void *pProgressData) override final;
};

/************************************************************************/
/*                           GTiffRasterBand()                          */
/************************************************************************/

GTiffRasterBand::GTiffRasterBand(GTiffDataset *poDSIn, int nBandIn)
    : m_poGDS(poDSIn)
{
    poDS = poDSIn;
    nBand = nBandIn;

    /* -------------------------------------------------------------------- */
    /*      Get the GDAL data type.                                         */
    /* -------------------------------------------------------------------- */
    const uint16_t nBitsPerSample = m_poGDS->m_nBitsPerSample;
    const uint16_t nSampleFormat = m_poGDS->m_nSampleFormat;

    eDataType = GDT_Unknown;

    if (nBitsPerSample <= 8)
    {
        eDataType = GDT_Byte;
        if (nSampleFormat == SAMPLEFORMAT_INT)
            m_oGTiffMDMD.SetMetadataItem("PIXELTYPE", "SIGNEDBYTE",
                                         "IMAGE_STRUCTURE");
    }
    else if (nBitsPerSample <= 16)
    {
        if (nSampleFormat == SAMPLEFORMAT_INT)
            eDataType = GDT_Int16;
        else
            eDataType = GDT_UInt16;
    }
    else if (nBitsPerSample == 32)
    {
        if (nSampleFormat == SAMPLEFORMAT_COMPLEXINT)
            eDataType = GDT_CInt16;
        else if (nSampleFormat == SAMPLEFORMAT_IEEEFP)
            eDataType = GDT_Float32;
        else if (nSampleFormat == SAMPLEFORMAT_INT)
            eDataType = GDT_Int32;
        else
            eDataType = GDT_UInt32;
    }
    else if (nBitsPerSample == 64)
    {
        if (nSampleFormat == SAMPLEFORMAT_IEEEFP)
            eDataType = GDT_Float64;
        else if (nSampleFormat == SAMPLEFORMAT_COMPLEXIEEEFP)
            eDataType = GDT_CFloat32;
        else if (nSampleFormat == SAMPLEFORMAT_COMPLEXINT)
            eDataType = GDT_CInt32;
        else if (nSampleFormat == SAMPLEFORMAT_INT)
            eDataType = GDT_Int64;
        else
            eDataType = GDT_UInt64;
    }
    else if (nBitsPerSample == 128)
    {
        if (nSampleFormat == SAMPLEFORMAT_COMPLEXIEEEFP)
            eDataType = GDT_CFloat64;
    }

    /* -------------------------------------------------------------------- */
    /*      Try to work out band color interpretation.                      */
    /* -------------------------------------------------------------------- */
    bool bLookForExtraSamples = false;

    if (m_poGDS->m_poColorTable != nullptr && nBand == 1)
    {
        m_eBandInterp = GCI_PaletteIndex;
    }
    else if (m_poGDS->m_nPhotometric == PHOTOMETRIC_RGB ||
             (m_poGDS->m_nPhotometric == PHOTOMETRIC_YCBCR &&
              m_poGDS->m_nCompression == COMPRESSION_JPEG &&
              CPLTestBool(CPLGetConfigOption("CONVERT_YCBCR_TO_RGB", "YES"))))
    {
        if (nBand == 1)
            m_eBandInterp = GCI_RedBand;
        else if (nBand == 2)
            m_eBandInterp = GCI_GreenBand;
        else if (nBand == 3)
            m_eBandInterp = GCI_BlueBand;
        else
            bLookForExtraSamples = true;
    }
    else if (m_poGDS->m_nPhotometric == PHOTOMETRIC_YCBCR)
    {
        if (nBand == 1)
            m_eBandInterp = GCI_YCbCr_YBand;
        else if (nBand == 2)
            m_eBandInterp = GCI_YCbCr_CbBand;
        else if (nBand == 3)
            m_eBandInterp = GCI_YCbCr_CrBand;
        else
            bLookForExtraSamples = true;
    }
    else if (m_poGDS->m_nPhotometric == PHOTOMETRIC_SEPARATED)
    {
        if (nBand == 1)
            m_eBandInterp = GCI_CyanBand;
        else if (nBand == 2)
            m_eBandInterp = GCI_MagentaBand;
        else if (nBand == 3)
            m_eBandInterp = GCI_YellowBand;
        else if (nBand == 4)
            m_eBandInterp = GCI_BlackBand;
        else
            bLookForExtraSamples = true;
    }
    else if (m_poGDS->m_nPhotometric == PHOTOMETRIC_MINISBLACK && nBand == 1)
    {
        m_eBandInterp = GCI_GrayIndex;
    }
    else
    {
        bLookForExtraSamples = true;
    }

    if (bLookForExtraSamples)
    {
        uint16_t *v = nullptr;
        uint16_t count = 0;

        if (TIFFGetField(m_poGDS->m_hTIFF, TIFFTAG_EXTRASAMPLES, &count, &v))
        {
            const int nBaseSamples = m_poGDS->m_nSamplesPerPixel - count;
            const int nExpectedBaseSamples =
                (m_poGDS->m_nPhotometric == PHOTOMETRIC_MINISBLACK)   ? 1
                : (m_poGDS->m_nPhotometric == PHOTOMETRIC_MINISWHITE) ? 1
                : (m_poGDS->m_nPhotometric == PHOTOMETRIC_RGB)        ? 3
                : (m_poGDS->m_nPhotometric == PHOTOMETRIC_YCBCR)      ? 3
                : (m_poGDS->m_nPhotometric == PHOTOMETRIC_SEPARATED)  ? 4
                                                                      : 0;

            if (nExpectedBaseSamples > 0 && nBand == nExpectedBaseSamples + 1 &&
                nBaseSamples != nExpectedBaseSamples)
            {
                ReportError(
                    CE_Warning, CPLE_AppDefined,
                    "Wrong number of ExtraSamples : %d. %d were expected",
                    count, m_poGDS->m_nSamplesPerPixel - nExpectedBaseSamples);
            }

            if (nBand > nBaseSamples && nBand - nBaseSamples - 1 < count &&
                (v[nBand - nBaseSamples - 1] == EXTRASAMPLE_ASSOCALPHA ||
                 v[nBand - nBaseSamples - 1] == EXTRASAMPLE_UNASSALPHA))
                m_eBandInterp = GCI_AlphaBand;
            else
                m_eBandInterp = GCI_Undefined;
        }
        else
        {
            m_eBandInterp = GCI_Undefined;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Establish block size for strip or tiles.                        */
    /* -------------------------------------------------------------------- */
    nBlockXSize = m_poGDS->m_nBlockXSize;
    nBlockYSize = m_poGDS->m_nBlockYSize;
}

/************************************************************************/
/*                          ~GTiffRasterBand()                          */
/************************************************************************/

GTiffRasterBand::~GTiffRasterBand()
{
    // So that any future DropReferenceVirtualMem() will not try to access the
    // raster band object, but this would not conform to the advertised
    // contract.
    if (!m_aSetPSelf.empty())
    {
        ReportError(CE_Warning, CPLE_AppDefined,
                    "Virtual memory objects still exist at GTiffRasterBand "
                    "destruction");
        std::set<GTiffRasterBand **>::iterator oIter = m_aSetPSelf.begin();
        for (; oIter != m_aSetPSelf.end(); ++oIter)
            *(*oIter) = nullptr;
    }
}

/************************************************************************/
/*                        FetchBufferDirectIO                           */
/************************************************************************/

class FetchBufferDirectIO final
{
    VSILFILE *fp;
    GByte *pTempBuffer;
    size_t nTempBufferSize;

  public:
    FetchBufferDirectIO(VSILFILE *fpIn, GByte *pTempBufferIn,
                        size_t nTempBufferSizeIn)
        : fp(fpIn), pTempBuffer(pTempBufferIn),
          nTempBufferSize(nTempBufferSizeIn)
    {
    }

    const GByte *FetchBytes(vsi_l_offset nOffset, int nPixels, int nDTSize,
                            bool bIsByteSwapped, bool bIsComplex, int nBlockId)
    {
        if (!FetchBytes(pTempBuffer, nOffset, nPixels, nDTSize, bIsByteSwapped,
                        bIsComplex, nBlockId))
        {
            return nullptr;
        }
        return pTempBuffer;
    }

    bool FetchBytes(GByte *pabyDstBuffer, vsi_l_offset nOffset, int nPixels,
                    int nDTSize, bool bIsByteSwapped, bool bIsComplex,
                    int nBlockId)
    {
        vsi_l_offset nSeekForward = 0;
        if (nOffset <= VSIFTellL(fp) ||
            (nSeekForward = nOffset - VSIFTellL(fp)) > nTempBufferSize)
        {
            if (VSIFSeekL(fp, nOffset, SEEK_SET) != 0)
            {
                CPLError(CE_Failure, CPLE_FileIO, "Cannot seek to block %d",
                         nBlockId);
                return false;
            }
        }
        else
        {
            while (nSeekForward > 0)
            {
                vsi_l_offset nToRead = nSeekForward;
                if (nToRead > nTempBufferSize)
                    nToRead = nTempBufferSize;
                if (VSIFReadL(pTempBuffer, static_cast<size_t>(nToRead), 1,
                              fp) != 1)
                {
                    CPLError(CE_Failure, CPLE_FileIO, "Cannot seek to block %d",
                             nBlockId);
                    return false;
                }
                nSeekForward -= nToRead;
            }
        }
        if (VSIFReadL(pabyDstBuffer, nPixels * nDTSize, 1, fp) != 1)
        {
            CPLError(CE_Failure, CPLE_FileIO, "Missing data for block %d",
                     nBlockId);
            return false;
        }

        if (bIsByteSwapped)
        {
            if (bIsComplex)
                GDALSwapWords(pabyDstBuffer, nDTSize / 2, 2 * nPixels,
                              nDTSize / 2);
            else
                GDALSwapWords(pabyDstBuffer, nDTSize, nPixels, nDTSize);
        }
        return true;
    }

    static const bool bMinimizeIO = true;
};

/************************************************************************/
/*                           DirectIO()                                 */
/************************************************************************/

// Reads directly bytes from the file using ReadMultiRange(), and by-pass
// block reading. Restricted to simple TIFF configurations
// (uncompressed data, standard data types). Particularly useful to extract
// sub-windows of data on a large /vsicurl dataset).
// Returns -1 if DirectIO() can't be supported on that file.

int GTiffRasterBand::DirectIO(GDALRWFlag eRWFlag, int nXOff, int nYOff,
                              int nXSize, int nYSize, void *pData,
                              int nBufXSize, int nBufYSize,
                              GDALDataType eBufType, GSpacing nPixelSpace,
                              GSpacing nLineSpace,
                              GDALRasterIOExtraArg *psExtraArg)
{
    const int nDTSizeBits = GDALGetDataTypeSizeBits(eDataType);
    if (!(eRWFlag == GF_Read && m_poGDS->m_nCompression == COMPRESSION_NONE &&
          (m_poGDS->m_nPhotometric == PHOTOMETRIC_MINISBLACK ||
           m_poGDS->m_nPhotometric == PHOTOMETRIC_RGB ||
           m_poGDS->m_nPhotometric == PHOTOMETRIC_PALETTE) &&
          IsBaseGTiffClass()))
    {
        return -1;
    }
    m_poGDS->Crystalize();

    // Only know how to deal with nearest neighbour in this optimized routine.
    if ((nXSize != nBufXSize || nYSize != nBufYSize) && psExtraArg != nullptr &&
        psExtraArg->eResampleAlg != GRIORA_NearestNeighbour)
    {
        return -1;
    }

#if DEBUG_VERBOSE
    CPLDebug("GTiff", "DirectIO(%d,%d,%d,%d -> %dx%d)", nXOff, nYOff, nXSize,
             nYSize, nBufXSize, nBufYSize);
#endif

    // Make sure that TIFFTAG_STRIPOFFSETS is up-to-date.
    if (m_poGDS->GetAccess() == GA_Update)
    {
        m_poGDS->FlushCache(false);
        VSI_TIFFFlushBufferedWrite(TIFFClientdata(m_poGDS->m_hTIFF));
    }

    if (TIFFIsTiled(m_poGDS->m_hTIFF))
    {
        const int nDTSize = nDTSizeBits / 8;
        const size_t nTempBufferForCommonDirectIOSize = static_cast<size_t>(
            static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize * nDTSize *
            (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG ? m_poGDS->nBands
                                                             : 1));
        if (m_poGDS->m_pTempBufferForCommonDirectIO == nullptr)
        {
            m_poGDS->m_pTempBufferForCommonDirectIO = static_cast<GByte *>(
                VSI_MALLOC_VERBOSE(nTempBufferForCommonDirectIOSize));
            if (m_poGDS->m_pTempBufferForCommonDirectIO == nullptr)
                return CE_Failure;
        }

        VSILFILE *fp = VSI_TIFFGetVSILFile(TIFFClientdata(m_poGDS->m_hTIFF));
        FetchBufferDirectIO oFetcher(fp,
                                     m_poGDS->m_pTempBufferForCommonDirectIO,
                                     nTempBufferForCommonDirectIOSize);

        return m_poGDS->CommonDirectIO(oFetcher, nXOff, nYOff, nXSize, nYSize,
                                       pData, nBufXSize, nBufYSize, eBufType, 1,
                                       &nBand, nPixelSpace, nLineSpace, 0);
    }

    // Get strip offsets.
    toff_t *panTIFFOffsets = nullptr;
    if (!TIFFGetField(m_poGDS->m_hTIFF, TIFFTAG_STRIPOFFSETS,
                      &panTIFFOffsets) ||
        panTIFFOffsets == nullptr)
    {
        return CE_Failure;
    }

    // Sub-sampling or over-sampling can only be done at last stage.
    int nReqXSize = nXSize;
    // Can do sub-sampling at the extraction stage.
    const int nReqYSize = std::min(nBufYSize, nYSize);
    // TODO(schwehr): Make ppData be GByte**.
    void **ppData =
        static_cast<void **>(VSI_MALLOC_VERBOSE(nReqYSize * sizeof(void *)));
    vsi_l_offset *panOffsets = static_cast<vsi_l_offset *>(
        VSI_MALLOC_VERBOSE(nReqYSize * sizeof(vsi_l_offset)));
    size_t *panSizes =
        static_cast<size_t *>(VSI_MALLOC_VERBOSE(nReqYSize * sizeof(size_t)));
    const int nDTSize = GDALGetDataTypeSizeBytes(eDataType);
    void *pTmpBuffer = nullptr;
    int eErr = CE_None;
    int nContigBands =
        m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG ? m_poGDS->nBands : 1;
    int nSrcPixelSize = nDTSize * nContigBands;

    if (ppData == nullptr || panOffsets == nullptr || panSizes == nullptr)
        eErr = CE_Failure;
    else if (nXSize != nBufXSize || nYSize != nBufYSize ||
             eBufType != eDataType ||
             nPixelSpace != GDALGetDataTypeSizeBytes(eBufType) ||
             nContigBands > 1)
    {
        // We need a temporary buffer for over-sampling/sub-sampling
        // and/or data type conversion.
        pTmpBuffer = VSI_MALLOC_VERBOSE(nReqXSize * nReqYSize * nSrcPixelSize);
        if (pTmpBuffer == nullptr)
            eErr = CE_Failure;
    }

    // Prepare data extraction.
    const double dfSrcYInc = nYSize / static_cast<double>(nBufYSize);

    for (int iLine = 0; eErr == CE_None && iLine < nReqYSize; ++iLine)
    {
        if (pTmpBuffer == nullptr)
            ppData[iLine] = static_cast<GByte *>(pData) + iLine * nLineSpace;
        else
            ppData[iLine] = static_cast<GByte *>(pTmpBuffer) +
                            iLine * nReqXSize * nSrcPixelSize;
        int nSrcLine = 0;
        if (nBufYSize < nYSize)  // Sub-sampling in y.
            nSrcLine = nYOff + static_cast<int>((iLine + 0.5) * dfSrcYInc);
        else
            nSrcLine = nYOff + iLine;

        const int nBlockXOff = 0;
        const int nBlockYOff = nSrcLine / nBlockYSize;
        const int nYOffsetInBlock = nSrcLine % nBlockYSize;
        nBlocksPerRow = DIV_ROUND_UP(nRasterXSize, nBlockXSize);
        int nBlockId = nBlockXOff + nBlockYOff * nBlocksPerRow;
        if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
        {
            nBlockId += (nBand - 1) * m_poGDS->m_nBlocksPerBand;
        }

        panOffsets[iLine] = panTIFFOffsets[nBlockId];
        if (panOffsets[iLine] == 0)  // We don't support sparse files.
            eErr = -1;

        panOffsets[iLine] +=
            (nXOff + static_cast<vsi_l_offset>(nYOffsetInBlock) * nBlockXSize) *
            nSrcPixelSize;
        panSizes[iLine] = nReqXSize * nSrcPixelSize;
    }

    // Extract data from the file.
    if (eErr == CE_None)
    {
        VSILFILE *fp = VSI_TIFFGetVSILFile(TIFFClientdata(m_poGDS->m_hTIFF));
        const int nRet =
            VSIFReadMultiRangeL(nReqYSize, ppData, panOffsets, panSizes, fp);
        if (nRet != 0)
            eErr = CE_Failure;
    }

    // Byte-swap if necessary.
    if (eErr == CE_None && TIFFIsByteSwapped(m_poGDS->m_hTIFF))
    {
        for (int iLine = 0; iLine < nReqYSize; ++iLine)
        {
            if (GDALDataTypeIsComplex(eDataType))
                GDALSwapWords(ppData[iLine], nDTSize / 2,
                              2 * nReqXSize * nContigBands, nDTSize / 2);
            else
                GDALSwapWords(ppData[iLine], nDTSize, nReqXSize * nContigBands,
                              nDTSize);
        }
    }

    // Over-sampling/sub-sampling and/or data type conversion.
    const double dfSrcXInc = nXSize / static_cast<double>(nBufXSize);
    if (eErr == CE_None && pTmpBuffer != nullptr)
    {
        for (int iY = 0; iY < nBufYSize; ++iY)
        {
            const int iSrcY = nBufYSize <= nYSize
                                  ? iY
                                  : static_cast<int>((iY + 0.5) * dfSrcYInc);

            GByte *pabySrcData = static_cast<GByte *>(ppData[iSrcY]) +
                                 (nContigBands > 1 ? (nBand - 1) : 0) * nDTSize;
            GByte *pabyDstData = static_cast<GByte *>(pData) + iY * nLineSpace;
            if (nBufXSize == nXSize)
            {
                GDALCopyWords(pabySrcData, eDataType, nSrcPixelSize,
                              pabyDstData, eBufType,
                              static_cast<int>(nPixelSpace), nBufXSize);
            }
            else
            {
                if (eDataType == GDT_Byte && eBufType == GDT_Byte)
                {
                    double dfSrcX = 0.5 * dfSrcXInc;
                    for (int iX = 0; iX < nBufXSize; ++iX, dfSrcX += dfSrcXInc)
                    {
                        const int iSrcX = static_cast<int>(dfSrcX);
                        pabyDstData[iX * nPixelSpace] =
                            pabySrcData[iSrcX * nSrcPixelSize];
                    }
                }
                else
                {
                    double dfSrcX = 0.5 * dfSrcXInc;
                    for (int iX = 0; iX < nBufXSize; ++iX, dfSrcX += dfSrcXInc)
                    {
                        const int iSrcX = static_cast<int>(dfSrcX);
                        GDALCopyWords(
                            pabySrcData + iSrcX * nSrcPixelSize, eDataType, 0,
                            pabyDstData + iX * nPixelSpace, eBufType, 0, 1);
                    }
                }
            }
        }
    }

    // Cleanup.
    CPLFree(pTmpBuffer);
    CPLFree(ppData);
    CPLFree(panOffsets);
    CPLFree(panSizes);

    return eErr;
}

/************************************************************************/
/*                           GetVirtualMemAuto()                        */
/************************************************************************/

CPLVirtualMem *GTiffRasterBand::GetVirtualMemAuto(GDALRWFlag eRWFlag,
                                                  int *pnPixelSpace,
                                                  GIntBig *pnLineSpace,
                                                  char **papszOptions)
{
    const char *pszImpl = CSLFetchNameValueDef(
        papszOptions, "USE_DEFAULT_IMPLEMENTATION", "AUTO");
    if (EQUAL(pszImpl, "YES") || EQUAL(pszImpl, "ON") || EQUAL(pszImpl, "1") ||
        EQUAL(pszImpl, "TRUE"))
    {
        return GDALRasterBand::GetVirtualMemAuto(eRWFlag, pnPixelSpace,
                                                 pnLineSpace, papszOptions);
    }

    CPLVirtualMem *psRet = GetVirtualMemAutoInternal(eRWFlag, pnPixelSpace,
                                                     pnLineSpace, papszOptions);
    if (psRet != nullptr)
    {
        CPLDebug("GTiff", "GetVirtualMemAuto(): Using memory file mapping");
        return psRet;
    }

    if (EQUAL(pszImpl, "NO") || EQUAL(pszImpl, "OFF") || EQUAL(pszImpl, "0") ||
        EQUAL(pszImpl, "FALSE"))
    {
        return nullptr;
    }

    CPLDebug("GTiff", "GetVirtualMemAuto(): Defaulting to base implementation");
    return GDALRasterBand::GetVirtualMemAuto(eRWFlag, pnPixelSpace, pnLineSpace,
                                             papszOptions);
}

/************************************************************************/
/*                           GetDefaultRAT()                            */
/************************************************************************/

GDALRasterAttributeTable *GTiffRasterBand::GetDefaultRAT()

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();
    return GDALPamRasterBand::GetDefaultRAT();
}

CPLErr GTiffRasterBand::SetDefaultRAT(const GDALRasterAttributeTable *poRAT)
{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();
    return GDALPamRasterBand::SetDefaultRAT(poRAT);
}

/************************************************************************/
/*                           GetHistogram()                             */
/************************************************************************/

CPLErr GTiffRasterBand::GetHistogram(double dfMin, double dfMax, int nBuckets,
                                     GUIntBig *panHistogram,
                                     int bIncludeOutOfRange, int bApproxOK,
                                     GDALProgressFunc pfnProgress,
                                     void *pProgressData)
{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();
    return GDALPamRasterBand::GetHistogram(dfMin, dfMax, nBuckets, panHistogram,
                                           bIncludeOutOfRange, bApproxOK,
                                           pfnProgress, pProgressData);
}

/************************************************************************/
/*                       GetDefaultHistogram()                          */
/************************************************************************/

CPLErr GTiffRasterBand::GetDefaultHistogram(
    double *pdfMin, double *pdfMax, int *pnBuckets, GUIntBig **ppanHistogram,
    int bForce, GDALProgressFunc pfnProgress, void *pProgressData)
{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();
    return GDALPamRasterBand::GetDefaultHistogram(pdfMin, pdfMax, pnBuckets,
                                                  ppanHistogram, bForce,
                                                  pfnProgress, pProgressData);
}

/************************************************************************/
/*                     DropReferenceVirtualMem()                        */
/************************************************************************/

void GTiffRasterBand::DropReferenceVirtualMem(void *pUserData)
{
    // This function may also be called when the dataset and rasterband
    // objects have been destroyed.
    // If they are still alive, it updates the reference counter of the
    // base mapping to invalidate the pointer to it if needed.

    GTiffRasterBand **ppoSelf = static_cast<GTiffRasterBand **>(pUserData);
    GTiffRasterBand *poSelf = *ppoSelf;

    if (poSelf != nullptr)
    {
        if (--(poSelf->m_poGDS->m_nRefBaseMapping) == 0)
        {
            poSelf->m_poGDS->m_pBaseMapping = nullptr;
        }
        poSelf->m_aSetPSelf.erase(ppoSelf);
    }
    CPLFree(pUserData);
}

/************************************************************************/
/*                     GetVirtualMemAutoInternal()                      */
/************************************************************************/

CPLVirtualMem *GTiffRasterBand::GetVirtualMemAutoInternal(GDALRWFlag eRWFlag,
                                                          int *pnPixelSpace,
                                                          GIntBig *pnLineSpace,
                                                          char **papszOptions)
{
    int nLineSize = nBlockXSize * GDALGetDataTypeSizeBytes(eDataType);
    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG)
        nLineSize *= m_poGDS->nBands;

    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG)
    {
        // In case of a pixel interleaved file, we save virtual memory space
        // by reusing a base mapping that embraces the whole imagery.
        if (m_poGDS->m_pBaseMapping != nullptr)
        {
            // Offset between the base mapping and the requested mapping.
            vsi_l_offset nOffset = static_cast<vsi_l_offset>(nBand - 1) *
                                   GDALGetDataTypeSizeBytes(eDataType);

            GTiffRasterBand **ppoSelf = static_cast<GTiffRasterBand **>(
                CPLCalloc(1, sizeof(GTiffRasterBand *)));
            *ppoSelf = this;

            CPLVirtualMem *pVMem = CPLVirtualMemDerivedNew(
                m_poGDS->m_pBaseMapping, nOffset,
                CPLVirtualMemGetSize(m_poGDS->m_pBaseMapping) - nOffset,
                GTiffRasterBand::DropReferenceVirtualMem, ppoSelf);
            if (pVMem == nullptr)
            {
                CPLFree(ppoSelf);
                return nullptr;
            }

            // Mechanism used so that the memory mapping object can be
            // destroyed after the raster band.
            m_aSetPSelf.insert(ppoSelf);
            ++m_poGDS->m_nRefBaseMapping;
            *pnPixelSpace = GDALGetDataTypeSizeBytes(eDataType);
            if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG)
                *pnPixelSpace *= m_poGDS->nBands;
            *pnLineSpace = nLineSize;
            return pVMem;
        }
    }

    VSILFILE *fp = VSI_TIFFGetVSILFile(TIFFClientdata(m_poGDS->m_hTIFF));

    vsi_l_offset nLength = static_cast<vsi_l_offset>(nRasterYSize) * nLineSize;

    if (!(CPLIsVirtualMemFileMapAvailable() &&
          VSIFGetNativeFileDescriptorL(fp) != nullptr &&
#if SIZEOF_VOIDP == 4
          nLength == static_cast<size_t>(nLength) &&
#endif
          m_poGDS->m_nCompression == COMPRESSION_NONE &&
          (m_poGDS->m_nPhotometric == PHOTOMETRIC_MINISBLACK ||
           m_poGDS->m_nPhotometric == PHOTOMETRIC_RGB ||
           m_poGDS->m_nPhotometric == PHOTOMETRIC_PALETTE) &&
          m_poGDS->m_nBitsPerSample == GDALGetDataTypeSizeBits(eDataType) &&
          !TIFFIsTiled(m_poGDS->m_hTIFF) &&
          !TIFFIsByteSwapped(m_poGDS->m_hTIFF)))
    {
        return nullptr;
    }

    // Make sure that TIFFTAG_STRIPOFFSETS is up-to-date.
    if (m_poGDS->GetAccess() == GA_Update)
    {
        m_poGDS->FlushCache(false);
        VSI_TIFFFlushBufferedWrite(TIFFClientdata(m_poGDS->m_hTIFF));
    }

    // Get strip offsets.
    toff_t *panTIFFOffsets = nullptr;
    if (!TIFFGetField(m_poGDS->m_hTIFF, TIFFTAG_STRIPOFFSETS,
                      &panTIFFOffsets) ||
        panTIFFOffsets == nullptr)
    {
        return nullptr;
    }

    GPtrDiff_t nBlockSize = static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize *
                            GDALGetDataTypeSizeBytes(eDataType);
    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG)
        nBlockSize *= m_poGDS->nBands;

    int nBlocks = m_poGDS->m_nBlocksPerBand;
    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
        nBlocks *= m_poGDS->nBands;
    int i = 0;  // Used after for.
    for (; i < nBlocks; ++i)
    {
        if (panTIFFOffsets[i] != 0)
            break;
    }
    if (i == nBlocks)
    {
        // All zeroes.
        if (m_poGDS->eAccess == GA_Update)
        {
            // Initialize the file with empty blocks so that the file has
            // the appropriate size.

            toff_t *panByteCounts = nullptr;
            if (!TIFFGetField(m_poGDS->m_hTIFF, TIFFTAG_STRIPBYTECOUNTS,
                              &panByteCounts) ||
                panByteCounts == nullptr)
            {
                return nullptr;
            }
            if (VSIFSeekL(fp, 0, SEEK_END) != 0)
                return nullptr;
            vsi_l_offset nBaseOffset = VSIFTellL(fp);

            // Just write one tile with libtiff to put it in appropriate state.
            GByte *pabyData =
                static_cast<GByte *>(VSI_CALLOC_VERBOSE(1, nBlockSize));
            if (pabyData == nullptr)
            {
                return nullptr;
            }
            const auto ret = TIFFWriteEncodedStrip(m_poGDS->m_hTIFF, 0,
                                                   pabyData, nBlockSize);
            VSI_TIFFFlushBufferedWrite(TIFFClientdata(m_poGDS->m_hTIFF));
            VSIFree(pabyData);
            if (ret != nBlockSize)
            {
                return nullptr;
            }
            CPLAssert(panTIFFOffsets[0] == nBaseOffset);
            CPLAssert(panByteCounts[0] == static_cast<toff_t>(nBlockSize));

            // Now simulate the writing of other blocks.
            const vsi_l_offset nDataSize =
                static_cast<vsi_l_offset>(nBlockSize) * nBlocks;
            if (VSIFTruncateL(fp, nBaseOffset + nDataSize) != 0)
                return nullptr;

            for (i = 1; i < nBlocks; ++i)
            {
                panTIFFOffsets[i] =
                    nBaseOffset + i * static_cast<toff_t>(nBlockSize);
                panByteCounts[i] = nBlockSize;
            }
        }
        else
        {
            CPLDebug("GTiff", "Sparse files not supported in file mapping");
            return nullptr;
        }
    }

    GIntBig nBlockSpacing = 0;
    bool bCompatibleSpacing = true;
    toff_t nPrevOffset = 0;
    for (i = 0; i < m_poGDS->m_nBlocksPerBand; ++i)
    {
        toff_t nCurOffset = 0;
        if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
            nCurOffset =
                panTIFFOffsets[m_poGDS->m_nBlocksPerBand * (nBand - 1) + i];
        else
            nCurOffset = panTIFFOffsets[i];
        if (nCurOffset == 0)
        {
            bCompatibleSpacing = false;
            break;
        }
        if (i > 0)
        {
            const GIntBig nCurSpacing = nCurOffset - nPrevOffset;
            if (i == 1)
            {
                if (nCurSpacing !=
                    static_cast<GIntBig>(nBlockYSize) * nLineSize)
                {
                    bCompatibleSpacing = false;
                    break;
                }
                nBlockSpacing = nCurSpacing;
            }
            else if (nBlockSpacing != nCurSpacing)
            {
                bCompatibleSpacing = false;
                break;
            }
        }
        nPrevOffset = nCurOffset;
    }

    if (!bCompatibleSpacing)
    {
        return nullptr;
    }

    vsi_l_offset nOffset = 0;
    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG)
    {
        CPLAssert(m_poGDS->m_pBaseMapping == nullptr);
        nOffset = panTIFFOffsets[0];
    }
    else
    {
        nOffset = panTIFFOffsets[m_poGDS->m_nBlocksPerBand * (nBand - 1)];
    }
    CPLVirtualMem *pVMem = CPLVirtualMemFileMapNew(
        fp, nOffset, nLength,
        eRWFlag == GF_Write ? VIRTUALMEM_READWRITE : VIRTUALMEM_READONLY,
        nullptr, nullptr);
    if (pVMem == nullptr)
    {
        return nullptr;
    }

    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG)
    {
        // TODO(schwehr): Revisit this block.
        m_poGDS->m_pBaseMapping = pVMem;
        pVMem = GetVirtualMemAutoInternal(eRWFlag, pnPixelSpace, pnLineSpace,
                                          papszOptions);
        // Drop ref on base mapping.
        CPLVirtualMemFree(m_poGDS->m_pBaseMapping);
        if (pVMem == nullptr)
            m_poGDS->m_pBaseMapping = nullptr;
    }
    else
    {
        *pnPixelSpace = GDALGetDataTypeSizeBytes(eDataType);
        if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG)
            *pnPixelSpace *= m_poGDS->nBands;
        *pnLineSpace = nLineSize;
    }
    return pVMem;
}

/************************************************************************/
/*                     HasOptimizedReadMultiRange()                     */
/************************************************************************/

bool GTiffDataset::HasOptimizedReadMultiRange()
{
    if (m_nHasOptimizedReadMultiRange >= 0)
        return m_nHasOptimizedReadMultiRange != 0;
    m_nHasOptimizedReadMultiRange = static_cast<signed char>(
        VSIHasOptimizedReadMultiRange(m_pszFilename)
        // Config option for debug and testing purposes only
        || CPLTestBool(CPLGetConfigOption(
               "GTIFF_HAS_OPTIMIZED_READ_MULTI_RANGE", "NO")));
    return m_nHasOptimizedReadMultiRange != 0;
}

/************************************************************************/
/*                            IRasterIO()                               */
/************************************************************************/

CPLErr GTiffDataset::IRasterIO(GDALRWFlag eRWFlag, int nXOff, int nYOff,
                               int nXSize, int nYSize, void *pData,
                               int nBufXSize, int nBufYSize,
                               GDALDataType eBufType, int nBandCount,
                               int *panBandMap, GSpacing nPixelSpace,
                               GSpacing nLineSpace, GSpacing nBandSpace,
                               GDALRasterIOExtraArg *psExtraArg)

{
    // Try to pass the request to the most appropriate overview dataset.
    if (nBufXSize < nXSize && nBufYSize < nYSize)
    {
        int bTried = FALSE;
        ++m_nJPEGOverviewVisibilityCounter;
        const CPLErr eErr = TryOverviewRasterIO(
            eRWFlag, nXOff, nYOff, nXSize, nYSize, pData, nBufXSize, nBufYSize,
            eBufType, nBandCount, panBandMap, nPixelSpace, nLineSpace,
            nBandSpace, psExtraArg, &bTried);
        --m_nJPEGOverviewVisibilityCounter;
        if (bTried)
            return eErr;
    }

    if (m_eVirtualMemIOUsage != VirtualMemIOEnum::NO)
    {
        const int nErr =
            VirtualMemIO(eRWFlag, nXOff, nYOff, nXSize, nYSize, pData,
                         nBufXSize, nBufYSize, eBufType, nBandCount, panBandMap,
                         nPixelSpace, nLineSpace, nBandSpace, psExtraArg);
        if (nErr >= 0)
            return static_cast<CPLErr>(nErr);
    }
    if (m_bDirectIO)
    {
        const int nErr =
            DirectIO(eRWFlag, nXOff, nYOff, nXSize, nYSize, pData, nBufXSize,
                     nBufYSize, eBufType, nBandCount, panBandMap, nPixelSpace,
                     nLineSpace, nBandSpace, psExtraArg);
        if (nErr >= 0)
            return static_cast<CPLErr>(nErr);
    }

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    bool bCanUseMultiThreadedRead = false;
    if (m_poThreadPool && eRWFlag == GF_Read && nBufXSize == nXSize &&
        nBufYSize == nYSize && IsMultiThreadedReadCompatible())
    {
        const int nBlockX1 = nXOff / m_nBlockXSize;
        const int nBlockY1 = nYOff / m_nBlockYSize;
        const int nBlockX2 = (nXOff + nXSize - 1) / m_nBlockXSize;
        const int nBlockY2 = (nYOff + nYSize - 1) / m_nBlockYSize;
        const int nXBlocks = nBlockX2 - nBlockX1 + 1;
        const int nYBlocks = nBlockY2 - nBlockY1 + 1;
        const int nBlocks =
            nXBlocks * nYBlocks *
            (m_nPlanarConfig == PLANARCONFIG_CONTIG ? 1 : nBandCount);
        if (nBlocks > 1)
        {
            bCanUseMultiThreadedRead = true;
        }
    }
#endif

    void *pBufferedData = nullptr;
    if (eAccess == GA_ReadOnly && eRWFlag == GF_Read &&
        (nBands == 1 || m_nPlanarConfig == PLANARCONFIG_CONTIG) &&
        HasOptimizedReadMultiRange()
#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
        && !(bCanUseMultiThreadedRead &&
             reinterpret_cast<VSIVirtualHandle *>(
                 VSI_TIFFGetVSILFile(TIFFClientdata(m_hTIFF)))
                 ->HasPRead())
#endif
    )
    {
        pBufferedData = cpl::down_cast<GTiffRasterBand *>(GetRasterBand(1))
                            ->CacheMultiRange(nXOff, nYOff, nXSize, nYSize,
                                              nBufXSize, nBufYSize, psExtraArg);
    }
#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    else if (bCanUseMultiThreadedRead)
    {
        return MultiThreadedRead(nXOff, nYOff, nXSize, nYSize, pData, eBufType,
                                 nBandCount, panBandMap, nPixelSpace,
                                 nLineSpace, nBandSpace);
    }
#endif

    ++m_nJPEGOverviewVisibilityCounter;
    const CPLErr eErr = GDALPamDataset::IRasterIO(
        eRWFlag, nXOff, nYOff, nXSize, nYSize, pData, nBufXSize, nBufYSize,
        eBufType, nBandCount, panBandMap, nPixelSpace, nLineSpace, nBandSpace,
        psExtraArg);
    m_nJPEGOverviewVisibilityCounter--;

    if (pBufferedData)
    {
        VSIFree(pBufferedData);
        VSI_TIFFSetCachedRanges(TIFFClientdata(m_hTIFF), 0, nullptr, nullptr,
                                nullptr);
    }

    return eErr;
}

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT

struct GTiffDecompressContext
{
    std::mutex oMutex{};
    bool bSuccess = true;

    VSIVirtualHandle *poHandle = nullptr;
    GTiffDataset *poDS = nullptr;
    GDALDataType eDT = GDT_Unknown;
    int nXOff = 0;
    int nYOff = 0;
    int nXSize = 0;
    int nYSize = 0;
    int nBlockXStart = 0;
    int nBlockYStart = 0;
    int nBlockXEnd = 0;
    int nBlockYEnd = 0;
    GByte *pabyData = nullptr;
    GDALDataType eBufType = GDT_Unknown;
    int nBufDTSize = 0;
    int nBandCount = 0;
    const int *panBandMap = nullptr;
    GSpacing nPixelSpace = 0;
    GSpacing nLineSpace = 0;
    GSpacing nBandSpace = 0;
    bool bHasPRead = false;
    bool bCacheAllBands = false;
    bool bSkipBlockCache = false;
    bool bUseBIPOptim = false;
    bool bUseDeinterleaveOptimNoBlockCache = false;
    bool bUseDeinterleaveOptimBlockCache = false;
    bool bIsTiled = false;
    bool bTIFFIsBigEndian = false;
    int nBlocksPerRow = 0;

    uint16_t nPredictor = 0;

    uint32_t nJPEGTableSize = 0;
    void *pJPEGTable = nullptr;
    uint16_t nYCrbCrSubSampling0 = 2;
    uint16_t nYCrbCrSubSampling1 = 2;

    uint16_t *pExtraSamples = nullptr;
    uint16_t nExtraSampleCount = 0;
};

struct GTiffDecompressJob
{
    GTiffDecompressContext *psContext = nullptr;
    int iBand = 0;  // or -1 to indicate all bands of panBandMap
    int nXBlock = 0;
    int nYBlock = 0;
    vsi_l_offset nOffset = 0;
    vsi_l_offset nSize = 0;
};

/************************************************************************/
/*                     ThreadDecompressionFunc()                        */
/************************************************************************/

static void ThreadDecompressionFunc(void *pData)
{
    const auto psJob = static_cast<const GTiffDecompressJob *>(pData);
    auto psContext = psJob->psContext;
    auto poDS = psContext->poDS;

    const int nBandsPerStrile =
        poDS->m_nPlanarConfig == PLANARCONFIG_CONTIG ? poDS->nBands : 1;
    const int nBandsToWrite = poDS->m_nPlanarConfig == PLANARCONFIG_CONTIG
                                  ? psContext->nBandCount
                                  : 1;

    const int nXOffsetInBlock = psJob->nXBlock == psContext->nBlockXStart
                                    ? psContext->nXOff % poDS->m_nBlockXSize
                                    : 0;
    const int nXOffsetInData =
        psJob->nXBlock == psContext->nBlockXStart
            ? 0
            : (psJob->nXBlock - psContext->nBlockXStart) * poDS->m_nBlockXSize -
                  (psContext->nXOff % poDS->m_nBlockXSize);
    const int nXSize =
        psJob->nXBlock == psContext->nBlockXStart
            ? (psJob->nXBlock == psContext->nBlockXEnd
                   ? psContext->nXSize
                   : poDS->m_nBlockXSize -
                         (psContext->nXOff % poDS->m_nBlockXSize))
        : psJob->nXBlock == psContext->nBlockXEnd
            ? (((psContext->nXOff + psContext->nXSize) % poDS->m_nBlockXSize) ==
                       0
                   ? poDS->m_nBlockXSize
                   : ((psContext->nXOff + psContext->nXSize) %
                      poDS->m_nBlockXSize))
            : poDS->m_nBlockXSize;

    const int nYOffsetInBlock = psJob->nYBlock == psContext->nBlockYStart
                                    ? psContext->nYOff % poDS->m_nBlockYSize
                                    : 0;
    const int nYOffsetInData =
        psJob->nYBlock == psContext->nBlockYStart
            ? 0
            : (psJob->nYBlock - psContext->nBlockYStart) * poDS->m_nBlockYSize -
                  (psContext->nYOff % poDS->m_nBlockYSize);
    const int nYSize =
        psJob->nYBlock == psContext->nBlockYStart
            ? (psJob->nYBlock == psContext->nBlockYEnd
                   ? psContext->nYSize
                   : poDS->m_nBlockYSize -
                         (psContext->nYOff % poDS->m_nBlockYSize))
        : psJob->nYBlock == psContext->nBlockYEnd
            ? (((psContext->nYOff + psContext->nYSize) % poDS->m_nBlockYSize) ==
                       0
                   ? poDS->m_nBlockYSize
                   : ((psContext->nYOff + psContext->nYSize) %
                      poDS->m_nBlockYSize))
            : poDS->m_nBlockYSize;
#if 0
    CPLDebug("GTiff",
             "nXBlock = %d, nYBlock = %d, "
             "nXOffsetInBlock = %d, nXOffsetInData = %d, nXSize = %d, "
             "nYOffsetInBlock = %d, nYOffsetInData = %d, nYSize = %d\n",
             psJob->nXBlock, psJob->nYBlock,
             nXOffsetInBlock, nXOffsetInData, nXSize,
             nYOffsetInBlock, nYOffsetInData, nYSize);
#endif

    if (psJob->nSize == 0)
    {
        {
            std::lock_guard<std::mutex> oLock(psContext->oMutex);
            if (!psContext->bSuccess)
                return;
        }
        const double dfNoDataValue =
            poDS->m_bNoDataSet ? poDS->m_dfNoDataValue : 0;
        for (int y = 0; y < nYSize; ++y)
        {
            for (int i = 0; i < nBandsToWrite; ++i)
            {
                const int iDstBandIdx =
                    poDS->m_nPlanarConfig == PLANARCONFIG_CONTIG ? i
                                                                 : psJob->iBand;
                GDALCopyWords64(
                    &dfNoDataValue, GDT_Float64, 0,
                    psContext->pabyData + iDstBandIdx * psContext->nBandSpace +
                        (y + nYOffsetInData) * psContext->nLineSpace +
                        nXOffsetInData * psContext->nPixelSpace,
                    psContext->eBufType,
                    static_cast<int>(psContext->nPixelSpace), nXSize);
            }
        }
        return;
    }

    const int nBandsToCache =
        psContext->bCacheAllBands ? poDS->nBands : nBandsToWrite;
    std::vector<GDALRasterBlock *> apoBlocks(nBandsToCache);
    std::vector<bool> abAlreadyLoadedBlocks(nBandsToCache);
    int nAlreadyLoadedBlocks = 0;
    std::vector<GByte> abyInput;

    struct FreeBlocks
    {
        std::vector<GDALRasterBlock *> &m_apoBlocks;

        explicit FreeBlocks(std::vector<GDALRasterBlock *> &apoBlocksIn)
            : m_apoBlocks(apoBlocksIn)
        {
        }
        ~FreeBlocks()
        {
            for (auto *poBlock : m_apoBlocks)
            {
                if (poBlock)
                    poBlock->DropLock();
            }
        }
    };
    FreeBlocks oFreeBlocks(apoBlocks);

    const auto LoadBlocks = [&]()
    {
        for (int i = 0; i < nBandsToCache; ++i)
        {
            const int iBand = psContext->bCacheAllBands ? i + 1
                              : poDS->m_nPlanarConfig == PLANARCONFIG_CONTIG
                                  ? psContext->panBandMap[i]
                                  : psJob->iBand + 1;
            apoBlocks[i] = poDS->GetRasterBand(iBand)->TryGetLockedBlockRef(
                psJob->nXBlock, psJob->nYBlock);
            if (apoBlocks[i] == nullptr)
            {
                apoBlocks[i] = poDS->GetRasterBand(iBand)->GetLockedBlockRef(
                    psJob->nXBlock, psJob->nYBlock, TRUE);
                if (apoBlocks[i] == nullptr)
                    return false;
            }
            else
            {
                abAlreadyLoadedBlocks[i] = true;
                nAlreadyLoadedBlocks++;
            }
        }
        return true;
    };

    const auto AllocInputBuffer = [&]()
    {
        bool bError = false;
#if SIZEOF_VOIDP == 4
        if (psJob->nSize != static_cast<size_t>(psJob->nSize))
        {
            bError = true;
        }
        else
#endif
        {
            try
            {
                abyInput.resize(static_cast<size_t>(psJob->nSize));
            }
            catch (const std::exception &)
            {
                bError = true;
            }
        }
        if (bError)
        {
            CPLError(CE_Failure, CPLE_OutOfMemory,
                     "Cannot allocate working buffer of size " CPL_FRMT_GUIB,
                     static_cast<GUIntBig>(psJob->nSize));
            return false;
        }
        return true;
    };

    if (psContext->bHasPRead)
    {
        {
            std::lock_guard<std::mutex> oLock(psContext->oMutex);
            if (!psContext->bSuccess)
                return;

            // Coverity Scan notices that GDALRasterBlock::Internalize() calls
            // CPLSleep() in a debug code path, and warns about that while
            // holding the above mutex.
            // coverity[sleep]
            if (!psContext->bSkipBlockCache && !LoadBlocks())
            {
                psContext->bSuccess = false;
                return;
            }
        }
        if (nAlreadyLoadedBlocks != nBandsToCache)
        {
            if (!AllocInputBuffer())
            {
                std::lock_guard<std::mutex> oLock(psContext->oMutex);
                psContext->bSuccess = false;
                return;
            }
            if (psContext->poHandle->PRead(abyInput.data(), abyInput.size(),
                                           psJob->nOffset) != abyInput.size())
            {
                CPLError(CE_Failure, CPLE_AppDefined,
                         "Cannot read " CPL_FRMT_GUIB
                         " bytes at offset " CPL_FRMT_GUIB,
                         static_cast<GUIntBig>(psJob->nSize),
                         static_cast<GUIntBig>(psJob->nOffset));

                std::lock_guard<std::mutex> oLock(psContext->oMutex);
                psContext->bSuccess = false;
                return;
            }
        }
    }
    else
    {
        std::lock_guard<std::mutex> oLock(psContext->oMutex);
        if (!psContext->bSuccess)
            return;

        // Coverity Scan notices that GDALRasterBlock::Internalize() calls
        // CPLSleep() in a debug code path, and warns about that while
        // holding the above mutex.
        // coverity[sleep]
        if (!psContext->bSkipBlockCache && !LoadBlocks())
        {
            psContext->bSuccess = false;
            return;
        }

        if (nAlreadyLoadedBlocks != nBandsToCache)
        {
            if (!AllocInputBuffer())
            {
                psContext->bSuccess = false;
                return;
            }
            if (psContext->poHandle->Seek(psJob->nOffset, SEEK_SET) != 0 ||
                psContext->poHandle->Read(abyInput.data(), abyInput.size(),
                                          1) != 1)
            {
                CPLError(CE_Failure, CPLE_AppDefined,
                         "Cannot read " CPL_FRMT_GUIB
                         " bytes at offset " CPL_FRMT_GUIB,
                         static_cast<GUIntBig>(psJob->nSize),
                         static_cast<GUIntBig>(psJob->nOffset));
                psContext->bSuccess = false;
                return;
            }
        }
    }

    const int nDTSize = GDALGetDataTypeSizeBytes(psContext->eDT);
    GByte *pDstPtr = psContext->pabyData +
                     nYOffsetInData * psContext->nLineSpace +
                     nXOffsetInData * psContext->nPixelSpace;

    if (nAlreadyLoadedBlocks != nBandsToCache)
    {
        // Generate a dummy in-memory TIFF file that has all the needed tags
        // from the original file
        CPLString osTmpFilename;
        osTmpFilename.Printf("/vsimem/decompress_%p.tif", psJob);
        VSILFILE *fpTmp = VSIFOpenL(osTmpFilename.c_str(), "wb+");
        TIFF *hTIFFTmp =
            VSI_TIFFOpen(osTmpFilename.c_str(),
                         psContext->bTIFFIsBigEndian ? "wb+" : "wl+", fpTmp);
        CPLAssert(hTIFFTmp != nullptr);
        const int nBlocksPerColumn =
            DIV_ROUND_UP(poDS->nRasterYSize, poDS->m_nBlockYSize);
        const int nBlockYSize =
            (psContext->bIsTiled || psJob->nYBlock < nBlocksPerColumn - 1)
                ? poDS->m_nBlockYSize
            : (poDS->nRasterYSize % poDS->m_nBlockYSize) == 0
                ? poDS->m_nBlockYSize
                : poDS->nRasterYSize % poDS->m_nBlockYSize;
        TIFFSetField(hTIFFTmp, TIFFTAG_IMAGEWIDTH, poDS->m_nBlockXSize);
        TIFFSetField(hTIFFTmp, TIFFTAG_IMAGELENGTH, nBlockYSize);
        TIFFSetField(hTIFFTmp, TIFFTAG_BITSPERSAMPLE, poDS->m_nBitsPerSample);
        TIFFSetField(hTIFFTmp, TIFFTAG_COMPRESSION, poDS->m_nCompression);
        TIFFSetField(hTIFFTmp, TIFFTAG_PHOTOMETRIC, poDS->m_nPhotometric);
        TIFFSetField(hTIFFTmp, TIFFTAG_SAMPLEFORMAT, poDS->m_nSampleFormat);
        TIFFSetField(hTIFFTmp, TIFFTAG_SAMPLESPERPIXEL,
                     poDS->m_nPlanarConfig == PLANARCONFIG_CONTIG
                         ? poDS->m_nSamplesPerPixel
                         : 1);
        TIFFSetField(hTIFFTmp, TIFFTAG_ROWSPERSTRIP, nBlockYSize);
        TIFFSetField(hTIFFTmp, TIFFTAG_PLANARCONFIG, poDS->m_nPlanarConfig);
        if (psContext->nPredictor != PREDICTOR_NONE)
            TIFFSetField(hTIFFTmp, TIFFTAG_PREDICTOR, psContext->nPredictor);
        if (poDS->m_nCompression == COMPRESSION_LERC)
        {
            TIFFSetField(hTIFFTmp, TIFFTAG_LERC_PARAMETERS, 2,
                         poDS->m_anLercAddCompressionAndVersion);
        }
        else if (poDS->m_nCompression == COMPRESSION_JPEG)
        {
            if (psContext->pJPEGTable)
            {
                TIFFSetField(hTIFFTmp, TIFFTAG_JPEGTABLES,
                             psContext->nJPEGTableSize, psContext->pJPEGTable);
            }
            if (poDS->m_nPhotometric == PHOTOMETRIC_YCBCR)
            {
                TIFFSetField(hTIFFTmp, TIFFTAG_YCBCRSUBSAMPLING,
                             psContext->nYCrbCrSubSampling0,
                             psContext->nYCrbCrSubSampling1);
            }
        }
        if (psContext->pExtraSamples)
        {
            TIFFSetField(hTIFFTmp, TIFFTAG_EXTRASAMPLES,
                         psContext->nExtraSampleCount,
                         psContext->pExtraSamples);
        }
        TIFFWriteCheck(hTIFFTmp, FALSE, "ThreadDecompressionFunc");
        TIFFWriteDirectory(hTIFFTmp);
        XTIFFClose(hTIFFTmp);

        // Re-open file
        hTIFFTmp = VSI_TIFFOpen(osTmpFilename.c_str(), "r", fpTmp);
        CPLAssert(hTIFFTmp != nullptr);
        poDS->RestoreVolatileParameters(hTIFFTmp);

        bool bRet = true;
        // Request m_nBlockYSize line in the block, except on the bottom-most
        // tile/strip.
        const int nBlockReqYSize =
            (psJob->nYBlock < nBlocksPerColumn - 1) ? poDS->m_nBlockYSize
            : (poDS->nRasterYSize % poDS->m_nBlockYSize) == 0
                ? poDS->m_nBlockYSize
                : poDS->nRasterYSize % poDS->m_nBlockYSize;

        const size_t nReqSize = static_cast<size_t>(poDS->m_nBlockXSize) *
                                nBlockReqYSize * nBandsPerStrile * nDTSize;

        GByte *pabyOutput;
        std::vector<GByte> abyOutput;
        if (poDS->m_nCompression == COMPRESSION_NONE &&
            !TIFFIsByteSwapped(poDS->m_hTIFF) && abyInput.size() >= nReqSize &&
            (psContext->bSkipBlockCache || nBandsPerStrile > 1))
        {
            pabyOutput = abyInput.data();
        }
        else
        {
            if (psContext->bSkipBlockCache || nBandsPerStrile > 1)
            {
                abyOutput.resize(nReqSize);
                pabyOutput = abyOutput.data();
            }
            else
            {
                pabyOutput = static_cast<GByte *>(apoBlocks[0]->GetDataRef());
            }
            if (!TIFFReadFromUserBuffer(hTIFFTmp, 0, abyInput.data(),
                                        abyInput.size(), pabyOutput,
                                        nReqSize) &&
                !poDS->m_bIgnoreReadErrors)
            {
                bRet = false;
            }
        }
        XTIFFClose(hTIFFTmp);
        CPL_IGNORE_RET_VAL(VSIFCloseL(fpTmp));
        VSIUnlink(osTmpFilename.c_str());

        if (!bRet)
        {
            std::lock_guard<std::mutex> oLock(psContext->oMutex);
            psContext->bSuccess = false;
            return;
        }

        if (!psContext->bSkipBlockCache && nBandsPerStrile > 1)
        {
            // Copy pixel-interleaved all-band buffer to cached blocks

            if (psContext->bUseDeinterleaveOptimBlockCache)
            {
                // Optimization
                std::vector<void *> ppDestBuffers(poDS->nBands);
                for (int i = 0; i < poDS->nBands; ++i)
                {
                    ppDestBuffers[i] = apoBlocks[i]->GetDataRef();
                }
                GDALDeinterleave(pabyOutput, psContext->eDT, poDS->nBands,
                                 ppDestBuffers.data(), psContext->eDT,
                                 static_cast<size_t>(nBlockReqYSize) *
                                     poDS->m_nBlockXSize);
            }
            else
            {
                // General case
                for (int i = 0; i < nBandsToCache; ++i)
                {
                    if (!abAlreadyLoadedBlocks[i])
                    {
                        const int iBand = psContext->bCacheAllBands
                                              ? i
                                              : psContext->panBandMap[i] - 1;
                        GDALCopyWords64(pabyOutput + iBand * nDTSize,
                                        psContext->eDT, nDTSize * poDS->nBands,
                                        apoBlocks[i]->GetDataRef(),
                                        psContext->eDT, nDTSize,
                                        static_cast<size_t>(nBlockReqYSize) *
                                            poDS->m_nBlockXSize);
                    }
                }
            }
        }

        const GByte *pSrcPtr =
            pabyOutput +
            (static_cast<size_t>(nYOffsetInBlock) * poDS->m_nBlockXSize +
             nXOffsetInBlock) *
                nDTSize * nBandsPerStrile;
        const size_t nSrcLineInc =
            poDS->m_nBlockXSize * nDTSize * nBandsPerStrile;

        // Optimization when writing to BIP buffer.
        if (psContext->bUseBIPOptim)
        {
            for (int y = 0; y < nYSize; ++y)
            {
                GDALCopyWords64(pSrcPtr, psContext->eDT, nDTSize, pDstPtr,
                                psContext->eBufType, psContext->nBufDTSize,
                                static_cast<size_t>(nXSize) * poDS->nBands);
                pSrcPtr += nSrcLineInc;
                pDstPtr += psContext->nLineSpace;
            }
            return;
        }

        if (psContext->bSkipBlockCache)
        {
            // Copy from pixel-interleaved all-band buffer (or temporary buffer
            // for single-band/separate case) into final buffer
            if (psContext->bUseDeinterleaveOptimNoBlockCache)
            {
                // Optimization
                std::vector<void *> ppDestBuffers(psContext->nBandCount);
                for (int i = 0; i < psContext->nBandCount; ++i)
                {
                    ppDestBuffers[i] =
                        pDstPtr +
                        (psContext->panBandMap[i] - 1) * psContext->nBandSpace;
                }
                for (int y = 0; y < nYSize; ++y)
                {
                    GDALDeinterleave(
                        pSrcPtr, psContext->eDT, psContext->nBandCount,
                        ppDestBuffers.data(), psContext->eDT, nXSize);
                    pSrcPtr += nSrcLineInc;
                    for (int i = 0; i < psContext->nBandCount; ++i)
                    {
                        ppDestBuffers[i] =
                            static_cast<GByte *>(ppDestBuffers[i]) +
                            psContext->nLineSpace;
                    }
                }
                return;
            }

            // General case
            for (int y = 0; y < nYSize; ++y)
            {
                for (int i = 0; i < nBandsToWrite; ++i)
                {
                    const int iSrcBandIdx =
                        poDS->m_nPlanarConfig == PLANARCONFIG_CONTIG
                            ? psContext->panBandMap[i] - 1
                            : 0;
                    const int iDstBandIdx =
                        poDS->m_nPlanarConfig == PLANARCONFIG_CONTIG
                            ? i
                            : psJob->iBand;
                    GDALCopyWords64(
                        pSrcPtr + iSrcBandIdx * nDTSize + y * nSrcLineInc,
                        psContext->eDT, nDTSize * nBandsPerStrile,
                        pDstPtr + iDstBandIdx * psContext->nBandSpace +
                            y * psContext->nLineSpace,
                        psContext->eBufType,
                        static_cast<int>(psContext->nPixelSpace), nXSize);
                }
            }
            return;
        }
    }

    CPLAssert(!psContext->bSkipBlockCache);

    // Compose cached blocks into final buffer
    for (int i = 0; i < nBandsToWrite; ++i)
    {
        const int iSrcBandIdx =
            psContext->bCacheAllBands ? psContext->panBandMap[i] - 1
            : poDS->m_nPlanarConfig == PLANARCONFIG_CONTIG ? i
                                                           : 0;
        const int iDstBandIdx =
            poDS->m_nPlanarConfig == PLANARCONFIG_CONTIG ? i : psJob->iBand;
        const GByte *pSrcPtr =
            static_cast<GByte *>(apoBlocks[iSrcBandIdx]->GetDataRef()) +
            (static_cast<size_t>(nYOffsetInBlock) * poDS->m_nBlockXSize +
             nXOffsetInBlock) *
                nDTSize;
        for (int y = 0; y < nYSize; ++y)
        {
            GDALCopyWords64(pSrcPtr + static_cast<size_t>(y) *
                                          poDS->m_nBlockXSize * nDTSize,
                            psContext->eDT, nDTSize,
                            pDstPtr + iDstBandIdx * psContext->nBandSpace +
                                y * psContext->nLineSpace,
                            psContext->eBufType,
                            static_cast<int>(psContext->nPixelSpace), nXSize);
        }
    }
}

/************************************************************************/
/*                    IsMultiThreadedReadCompatible()                   */
/************************************************************************/

bool GTiffDataset::IsMultiThreadedReadCompatible() const
{
    return cpl::down_cast<GTiffRasterBand *>(papoBands[0])
               ->IsBaseGTiffClass() &&
           !m_bStreamingIn && !m_bStreamingOut &&
           (m_nCompression == COMPRESSION_NONE ||
            m_nCompression == COMPRESSION_ADOBE_DEFLATE ||
            m_nCompression == COMPRESSION_LZW ||
            m_nCompression == COMPRESSION_PACKBITS ||
            m_nCompression == COMPRESSION_LZMA ||
            m_nCompression == COMPRESSION_ZSTD ||
            m_nCompression == COMPRESSION_LERC ||
            m_nCompression == COMPRESSION_JXL ||
            m_nCompression == COMPRESSION_WEBP ||
            m_nCompression == COMPRESSION_JPEG);
}

/************************************************************************/
/*                        MultiThreadedRead()                           */
/************************************************************************/

CPLErr GTiffDataset::MultiThreadedRead(int nXOff, int nYOff, int nXSize,
                                       int nYSize, void *pData,
                                       GDALDataType eBufType, int nBandCount,
                                       const int *panBandMap,
                                       GSpacing nPixelSpace,
                                       GSpacing nLineSpace, GSpacing nBandSpace)
{
    auto poQueue = m_poThreadPool->CreateJobQueue();
    if (poQueue == nullptr)
    {
        return CE_Failure;
    }

    const int nBlockXStart = nXOff / m_nBlockXSize;
    const int nBlockYStart = nYOff / m_nBlockYSize;
    const int nBlockXEnd = (nXOff + nXSize - 1) / m_nBlockXSize;
    const int nBlockYEnd = (nYOff + nYSize - 1) / m_nBlockYSize;
    const int nXBlocks = nBlockXEnd - nBlockXStart + 1;
    const int nYBlocks = nBlockYEnd - nBlockYStart + 1;
    const int nStrilePerBlock =
        m_nPlanarConfig == PLANARCONFIG_CONTIG ? 1 : nBandCount;
    const int nBlocks = nXBlocks * nYBlocks * nStrilePerBlock;

    GTiffDecompressContext sContext;
    sContext.poHandle = reinterpret_cast<VSIVirtualHandle *>(
        VSI_TIFFGetVSILFile(TIFFClientdata(m_hTIFF)));
    sContext.bHasPRead =
        sContext.poHandle->HasPRead()
#ifdef DEBUG
        && CPLTestBool(CPLGetConfigOption("GTIFF_ALLOW_PREAD", "YES"))
#endif
        ;
    sContext.poDS = this;
    sContext.eDT = GetRasterBand(1)->GetRasterDataType();
    sContext.nXOff = nXOff;
    sContext.nYOff = nYOff;
    sContext.nXSize = nXSize;
    sContext.nYSize = nYSize;
    sContext.nBlockXStart = nBlockXStart;
    sContext.nBlockXEnd = nBlockXEnd;
    sContext.nBlockYStart = nBlockYStart;
    sContext.nBlockYEnd = nBlockYEnd;
    sContext.pabyData = static_cast<GByte *>(pData);
    sContext.eBufType = eBufType;
    sContext.nBufDTSize = GDALGetDataTypeSizeBytes(eBufType);
    sContext.nBandCount = nBandCount;
    sContext.panBandMap = panBandMap;
    sContext.nPixelSpace = nPixelSpace;
    sContext.nLineSpace = nLineSpace;
    sContext.nBandSpace = nBandSpace;
    sContext.bIsTiled = CPL_TO_BOOL(TIFFIsTiled(m_hTIFF));
    sContext.bTIFFIsBigEndian = CPL_TO_BOOL(TIFFIsBigEndian(m_hTIFF));
    sContext.nPredictor = PREDICTOR_NONE;
    sContext.nBlocksPerRow = DIV_ROUND_UP(nRasterXSize, m_nBlockXSize);

    if (m_bDirectIO)
    {
        sContext.bSkipBlockCache = true;
    }
    else if (nXOff == 0 && nYOff == 0 && nXSize == nRasterXSize &&
             nYSize == nRasterYSize)
    {
        if (m_nPlanarConfig == PLANARCONFIG_SEPARATE)
        {
            sContext.bSkipBlockCache = true;
        }
        else if (nBandCount == nBands)
        {
            sContext.bSkipBlockCache = true;
            for (int i = 0; i < nBands; ++i)
            {
                if (panBandMap[i] != i + 1)
                {
                    sContext.bSkipBlockCache = false;
                    break;
                }
            }
        }
    }

    if (m_nPlanarConfig == PLANARCONFIG_CONTIG && nBandCount == nBands &&
        nPixelSpace == nBands * static_cast<GSpacing>(sContext.nBufDTSize))
    {
        sContext.bUseBIPOptim = true;
        for (int i = 0; i < nBands; ++i)
        {
            if (panBandMap[i] != i + 1)
            {
                sContext.bUseBIPOptim = false;
                break;
            }
        }
    }

    if (m_nPlanarConfig == PLANARCONFIG_CONTIG &&
        (nBands == 3 || nBands == 4) && nBands == nBandCount &&
        (sContext.eDT == GDT_Byte || sContext.eDT == GDT_Int16 ||
         sContext.eDT == GDT_UInt16))
    {
        if (sContext.bSkipBlockCache)
        {
            if (sContext.eBufType == sContext.eDT &&
                nPixelSpace == sContext.nBufDTSize)
            {
                sContext.bUseDeinterleaveOptimNoBlockCache = true;
            }
        }
        else
        {
            sContext.bUseDeinterleaveOptimBlockCache = true;
            for (int i = 0; i < nBands; ++i)
            {
                if (panBandMap[i] != i + 1)
                {
                    sContext.bUseDeinterleaveOptimBlockCache = false;
                    break;
                }
            }
        }
    }

    // In contig mode, if only one band is requested, check if we have
    // enough cache to cache all bands.
    if (!sContext.bSkipBlockCache && nBands != 1 &&
        m_nPlanarConfig == PLANARCONFIG_CONTIG && nBandCount == 1)
    {
        const GIntBig nRequiredMem = static_cast<GIntBig>(nBands) * nXBlocks *
                                     nYBlocks * m_nBlockXSize * m_nBlockYSize *
                                     GDALGetDataTypeSizeBytes(sContext.eDT);
        if (nRequiredMem > GDALGetCacheMax64())
        {
            if (!m_bHasWarnedDisableAggressiveBandCaching)
            {
                CPLDebug("GTiff",
                         "Disable aggressive band caching. "
                         "Cache not big enough. "
                         "At least " CPL_FRMT_GIB " bytes necessary",
                         nRequiredMem);
                m_bHasWarnedDisableAggressiveBandCaching = true;
            }
        }
        else
        {
            sContext.bCacheAllBands = true;
            if ((nBands == 3 || nBands == 4) &&
                (sContext.eDT == GDT_Byte || sContext.eDT == GDT_Int16 ||
                 sContext.eDT == GDT_UInt16))
            {
                sContext.bUseDeinterleaveOptimBlockCache = true;
            }
        }
    }

    if (eAccess == GA_Update)
    {
        // Make sure to flush all dirty blocks we will access.
        std::vector<int> anBandsToCheck;
        if (m_nPlanarConfig == PLANARCONFIG_CONTIG && nBands > 1)
        {
            for (int i = 0; i < nBands; ++i)
            {
                anBandsToCheck.push_back(i);
            }
        }
        else
        {
            for (int i = 0; i < nBandCount; ++i)
            {
                anBandsToCheck.push_back(panBandMap[i] - 1);
            }
        }
        if (!anBandsToCheck.empty())
        {
            for (int y = 0; y < nYBlocks; ++y)
            {
                for (int x = 0; x < nXBlocks; ++x)
                {
                    for (const int iBand : anBandsToCheck)
                    {
                        auto poBlock = papoBands[iBand]->TryGetLockedBlockRef(
                            nBlockXStart + x, nBlockYStart + y);
                        if (poBlock)
                        {
                            CPLErr eErr = CE_None;
                            if (poBlock->GetDirty())
                            {
                                eErr = poBlock->Write();
                            }
                            poBlock->DropLock();
                            if (eErr == CE_Failure)
                            {
                                return CE_Failure;
                            }
                        }
                    }
                }
            }
        }

        // Flush last active multi-band contiguous buffer
        FlushBlockBuf();

        auto &oQueue =
            m_poBaseDS ? m_poBaseDS->m_asQueueJobIdx : m_asQueueJobIdx;
        if (!oQueue.empty())
        {
            for (int y = 0; y < nYBlocks; ++y)
            {
                for (int x = 0; x < nXBlocks; ++x)
                {
                    for (int i = 0; i < nStrilePerBlock; ++i)
                    {
                        int nBlockId =
                            nBlockXStart + x +
                            (nBlockYStart + y) * sContext.nBlocksPerRow;
                        if (m_nPlanarConfig == PLANARCONFIG_SEPARATE)
                            nBlockId += (panBandMap[i] - 1) * m_nBlocksPerBand;

                        WaitCompletionForBlock(nBlockId);
                    }
                }
            }
        }

        // Flush to file, and then to disk if using pread() interface
        VSI_TIFFFlushBufferedWrite(TIFFClientdata(m_hTIFF));
        if (sContext.bHasPRead)
            sContext.poHandle->Flush();
    }

    if (GTIFFSupportsPredictor(m_nCompression))
    {
        TIFFGetField(m_hTIFF, TIFFTAG_PREDICTOR, &sContext.nPredictor);
    }
    else if (m_nCompression == COMPRESSION_JPEG)
    {
        TIFFGetField(m_hTIFF, TIFFTAG_JPEGTABLES, &sContext.nJPEGTableSize,
                     &sContext.pJPEGTable);
        if (m_nPhotometric == PHOTOMETRIC_YCBCR)
        {
            TIFFGetFieldDefaulted(m_hTIFF, TIFFTAG_YCBCRSUBSAMPLING,
                                  &sContext.nYCrbCrSubSampling0,
                                  &sContext.nYCrbCrSubSampling1);
        }
    }
    TIFFGetField(m_hTIFF, TIFFTAG_EXTRASAMPLES, &sContext.nExtraSampleCount,
                 &sContext.pExtraSamples);

    // We need to do that as threads will access the block cache
    TemporarilyDropReadWriteLock();

    // Create one job per tile/strip
    vsi_l_offset nFileSize = 0;
    std::vector<GTiffDecompressJob> asJobs(nBlocks);
    int iJob = 0;
    for (int y = 0; y < nYBlocks; ++y)
    {
        for (int x = 0; x < nXBlocks; ++x)
        {
            for (int i = 0; i < nStrilePerBlock; ++i)
            {
                asJobs[iJob].psContext = &sContext;
                asJobs[iJob].iBand = m_nPlanarConfig == PLANARCONFIG_CONTIG
                                         ? -1
                                         : panBandMap[i] - 1;
                asJobs[iJob].nXBlock = nBlockXStart + x;
                asJobs[iJob].nYBlock = nBlockYStart + y;

                int nBlockId = asJobs[iJob].nXBlock +
                               asJobs[iJob].nYBlock * sContext.nBlocksPerRow;
                if (m_nPlanarConfig == PLANARCONFIG_SEPARATE)
                    nBlockId += asJobs[iJob].iBand * m_nBlocksPerBand;

                if (!sContext.bHasPRead)
                {
                    // Taking the mutex here is only needed when bHasPRead ==
                    // false since we could have concurrent uses of the handle,
                    // when when reading the TIFF TileOffsets / TileByteCounts
                    // array
                    std::lock_guard<std::mutex> oLock(sContext.oMutex);

                    IsBlockAvailable(nBlockId, &asJobs[iJob].nOffset,
                                     &asJobs[iJob].nSize);
                }
                else
                {
                    IsBlockAvailable(nBlockId, &asJobs[iJob].nOffset,
                                     &asJobs[iJob].nSize);
                }

                // Sanity check on block size
                if (asJobs[iJob].nSize > 100U * 1024 * 1024)
                {
                    if (nFileSize == 0)
                    {
                        std::lock_guard<std::mutex> oLock(sContext.oMutex);
                        sContext.poHandle->Seek(0, SEEK_END);
                        nFileSize = sContext.poHandle->Tell();
                    }
                    if (asJobs[iJob].nSize > nFileSize)
                    {
                        CPLError(CE_Failure, CPLE_AppDefined,
                                 "Cannot read " CPL_FRMT_GUIB
                                 " bytes at offset " CPL_FRMT_GUIB,
                                 static_cast<GUIntBig>(asJobs[iJob].nSize),
                                 static_cast<GUIntBig>(asJobs[iJob].nOffset));

                        std::lock_guard<std::mutex> oLock(sContext.oMutex);
                        sContext.bSuccess = false;
                        break;
                    }
                }

                poQueue->SubmitJob(ThreadDecompressionFunc, &asJobs[iJob]);

                ++iJob;
            }
        }
    }

    // Wait for all jobs to have been completed
    poQueue->WaitCompletion();

    // Undo effect of above TemporarilyDropReadWriteLock()
    ReacquireReadWriteLock();

    return sContext.bSuccess ? CE_None : CE_Failure;
}

#endif

/************************************************************************/
/*                        FetchBufferVirtualMemIO                       */
/************************************************************************/

class FetchBufferVirtualMemIO final
{
    const GByte *pabySrcData;
    size_t nMappingSize;
    GByte *pTempBuffer;

  public:
    FetchBufferVirtualMemIO(const GByte *pabySrcDataIn, size_t nMappingSizeIn,
                            GByte *pTempBufferIn)
        : pabySrcData(pabySrcDataIn), nMappingSize(nMappingSizeIn),
          pTempBuffer(pTempBufferIn)
    {
    }

    const GByte *FetchBytes(vsi_l_offset nOffset, int nPixels, int nDTSize,
                            bool bIsByteSwapped, bool bIsComplex, int nBlockId)
    {
        if (nOffset + nPixels * nDTSize > nMappingSize)
        {
            CPLError(CE_Failure, CPLE_FileIO, "Missing data for block %d",
                     nBlockId);
            return nullptr;
        }
        if (!bIsByteSwapped)
            return pabySrcData + nOffset;
        memcpy(pTempBuffer, pabySrcData + nOffset, nPixels * nDTSize);
        if (bIsComplex)
            GDALSwapWords(pTempBuffer, nDTSize / 2, 2 * nPixels, nDTSize / 2);
        else
            GDALSwapWords(pTempBuffer, nDTSize, nPixels, nDTSize);
        return pTempBuffer;
    }

    bool FetchBytes(GByte *pabyDstBuffer, vsi_l_offset nOffset, int nPixels,
                    int nDTSize, bool bIsByteSwapped, bool bIsComplex,
                    int nBlockId)
    {
        if (nOffset + nPixels * nDTSize > nMappingSize)
        {
            CPLError(CE_Failure, CPLE_FileIO, "Missing data for block %d",
                     nBlockId);
            return false;
        }
        memcpy(pabyDstBuffer, pabySrcData + nOffset, nPixels * nDTSize);
        if (bIsByteSwapped)
        {
            if (bIsComplex)
                GDALSwapWords(pabyDstBuffer, nDTSize / 2, 2 * nPixels,
                              nDTSize / 2);
            else
                GDALSwapWords(pabyDstBuffer, nDTSize, nPixels, nDTSize);
        }
        return true;
    }

    static const bool bMinimizeIO = false;
};

/************************************************************************/
/*                         VirtualMemIO()                               */
/************************************************************************/

int GTiffDataset::VirtualMemIO(GDALRWFlag eRWFlag, int nXOff, int nYOff,
                               int nXSize, int nYSize, void *pData,
                               int nBufXSize, int nBufYSize,
                               GDALDataType eBufType, int nBandCount,
                               int *panBandMap, GSpacing nPixelSpace,
                               GSpacing nLineSpace, GSpacing nBandSpace,
                               GDALRasterIOExtraArg *psExtraArg)
{
    if (eAccess == GA_Update || eRWFlag == GF_Write || m_bStreamingIn)
        return -1;

    // Only know how to deal with nearest neighbour in this optimized routine.
    if ((nXSize != nBufXSize || nYSize != nBufYSize) && psExtraArg != nullptr &&
        psExtraArg->eResampleAlg != GRIORA_NearestNeighbour)
    {
        return -1;
    }

    const GDALDataType eDataType = GetRasterBand(1)->GetRasterDataType();
    const int nDTSizeBits = GDALGetDataTypeSizeBits(eDataType);
    if (!(m_nCompression == COMPRESSION_NONE &&
          (m_nPhotometric == PHOTOMETRIC_MINISBLACK ||
           m_nPhotometric == PHOTOMETRIC_RGB ||
           m_nPhotometric == PHOTOMETRIC_PALETTE) &&
          m_nBitsPerSample == nDTSizeBits))
    {
        m_eVirtualMemIOUsage = VirtualMemIOEnum::NO;
        return -1;
    }

    size_t nMappingSize = 0;
    GByte *pabySrcData = nullptr;
    if (STARTS_WITH(m_pszFilename, "/vsimem/"))
    {
        vsi_l_offset nDataLength = 0;
        pabySrcData = VSIGetMemFileBuffer(m_pszFilename, &nDataLength, FALSE);
        nMappingSize = static_cast<size_t>(nDataLength);
        if (pabySrcData == nullptr)
            return -1;
    }
    else if (m_psVirtualMemIOMapping == nullptr)
    {
        VSILFILE *fp = VSI_TIFFGetVSILFile(TIFFClientdata(m_hTIFF));
        if (!CPLIsVirtualMemFileMapAvailable() ||
            VSIFGetNativeFileDescriptorL(fp) == nullptr)
        {
            m_eVirtualMemIOUsage = VirtualMemIOEnum::NO;
            return -1;
        }
        if (VSIFSeekL(fp, 0, SEEK_END) != 0)
        {
            m_eVirtualMemIOUsage = VirtualMemIOEnum::NO;
            return -1;
        }
        const vsi_l_offset nLength = VSIFTellL(fp);
        if (static_cast<size_t>(nLength) != nLength)
        {
            m_eVirtualMemIOUsage = VirtualMemIOEnum::NO;
            return -1;
        }
        if (m_eVirtualMemIOUsage == VirtualMemIOEnum::IF_ENOUGH_RAM)
        {
            GIntBig nRAM = CPLGetUsablePhysicalRAM();
            if (static_cast<GIntBig>(nLength) > nRAM)
            {
                CPLDebug("GTiff",
                         "Not enough RAM to map whole file into memory.");
                m_eVirtualMemIOUsage = VirtualMemIOEnum::NO;
                return -1;
            }
        }
        m_psVirtualMemIOMapping = CPLVirtualMemFileMapNew(
            fp, 0, nLength, VIRTUALMEM_READONLY, nullptr, nullptr);
        if (m_psVirtualMemIOMapping == nullptr)
        {
            m_eVirtualMemIOUsage = VirtualMemIOEnum::NO;
            return -1;
        }
        m_eVirtualMemIOUsage = VirtualMemIOEnum::YES;
    }

    if (m_psVirtualMemIOMapping)
    {
#ifdef DEBUG
        CPLDebug("GTiff", "Using VirtualMemIO");
#endif
        nMappingSize = CPLVirtualMemGetSize(m_psVirtualMemIOMapping);
        pabySrcData =
            static_cast<GByte *>(CPLVirtualMemGetAddr(m_psVirtualMemIOMapping));
    }

    if (TIFFIsByteSwapped(m_hTIFF) && m_pTempBufferForCommonDirectIO == nullptr)
    {
        const int nDTSize = nDTSizeBits / 8;
        size_t nTempBufferForCommonDirectIOSize = static_cast<size_t>(
            m_nBlockXSize * nDTSize *
            (m_nPlanarConfig == PLANARCONFIG_CONTIG ? nBands : 1));
        if (TIFFIsTiled(m_hTIFF))
            nTempBufferForCommonDirectIOSize *= m_nBlockYSize;

        m_pTempBufferForCommonDirectIO = static_cast<GByte *>(
            VSI_MALLOC_VERBOSE(nTempBufferForCommonDirectIOSize));
        if (m_pTempBufferForCommonDirectIO == nullptr)
            return CE_Failure;
    }
    FetchBufferVirtualMemIO oFetcher(pabySrcData, nMappingSize,
                                     m_pTempBufferForCommonDirectIO);

    return CommonDirectIO(oFetcher, nXOff, nYOff, nXSize, nYSize, pData,
                          nBufXSize, nBufYSize, eBufType, nBandCount,
                          panBandMap, nPixelSpace, nLineSpace, nBandSpace);
}

/************************************************************************/
/*                   CopyContigByteMultiBand()                          */
/************************************************************************/

static inline void CopyContigByteMultiBand(const GByte *CPL_RESTRICT pabySrc,
                                           int nSrcStride,
                                           GByte *CPL_RESTRICT pabyDest,
                                           int nDestStride, int nIters,
                                           int nBandCount)
{
    if (nBandCount == 3)
    {
        if (nSrcStride == 3 && nDestStride == 4)
        {
            while (nIters >= 8)
            {
                pabyDest[4 * 0 + 0] = pabySrc[3 * 0 + 0];
                pabyDest[4 * 0 + 1] = pabySrc[3 * 0 + 1];
                pabyDest[4 * 0 + 2] = pabySrc[3 * 0 + 2];
                pabyDest[4 * 1 + 0] = pabySrc[3 * 1 + 0];
                pabyDest[4 * 1 + 1] = pabySrc[3 * 1 + 1];
                pabyDest[4 * 1 + 2] = pabySrc[3 * 1 + 2];
                pabyDest[4 * 2 + 0] = pabySrc[3 * 2 + 0];
                pabyDest[4 * 2 + 1] = pabySrc[3 * 2 + 1];
                pabyDest[4 * 2 + 2] = pabySrc[3 * 2 + 2];
                pabyDest[4 * 3 + 0] = pabySrc[3 * 3 + 0];
                pabyDest[4 * 3 + 1] = pabySrc[3 * 3 + 1];
                pabyDest[4 * 3 + 2] = pabySrc[3 * 3 + 2];
                pabyDest[4 * 4 + 0] = pabySrc[3 * 4 + 0];
                pabyDest[4 * 4 + 1] = pabySrc[3 * 4 + 1];
                pabyDest[4 * 4 + 2] = pabySrc[3 * 4 + 2];
                pabyDest[4 * 5 + 0] = pabySrc[3 * 5 + 0];
                pabyDest[4 * 5 + 1] = pabySrc[3 * 5 + 1];
                pabyDest[4 * 5 + 2] = pabySrc[3 * 5 + 2];
                pabyDest[4 * 6 + 0] = pabySrc[3 * 6 + 0];
                pabyDest[4 * 6 + 1] = pabySrc[3 * 6 + 1];
                pabyDest[4 * 6 + 2] = pabySrc[3 * 6 + 2];
                pabyDest[4 * 7 + 0] = pabySrc[3 * 7 + 0];
                pabyDest[4 * 7 + 1] = pabySrc[3 * 7 + 1];
                pabyDest[4 * 7 + 2] = pabySrc[3 * 7 + 2];
                pabySrc += 3 * 8;
                pabyDest += 4 * 8;
                nIters -= 8;
            }
            while (nIters-- > 0)
            {
                pabyDest[0] = pabySrc[0];
                pabyDest[1] = pabySrc[1];
                pabyDest[2] = pabySrc[2];
                pabySrc += 3;
                pabyDest += 4;
            }
        }
        else
        {
            while (nIters-- > 0)
            {
                pabyDest[0] = pabySrc[0];
                pabyDest[1] = pabySrc[1];
                pabyDest[2] = pabySrc[2];
                pabySrc += nSrcStride;
                pabyDest += nDestStride;
            }
        }
    }
    else
    {
        while (nIters-- > 0)
        {
            for (int iBand = 0; iBand < nBandCount; ++iBand)
                pabyDest[iBand] = pabySrc[iBand];
            pabySrc += nSrcStride;
            pabyDest += nDestStride;
        }
    }
}

/************************************************************************/
/*                         CommonDirectIO()                             */
/************************************************************************/

// #define DEBUG_REACHED_VIRTUAL_MEM_IO
#ifdef DEBUG_REACHED_VIRTUAL_MEM_IO
static int anReachedVirtualMemIO[52] = {0};
#define REACHED(x) anReachedVirtualMemIO[x] = 1
#else
#define REACHED(x)
#endif

template <class FetchBuffer>
CPLErr GTiffDataset::CommonDirectIO(FetchBuffer &oFetcher, int nXOff, int nYOff,
                                    int nXSize, int nYSize, void *pData,
                                    int nBufXSize, int nBufYSize,
                                    GDALDataType eBufType, int nBandCount,
                                    int *panBandMap, GSpacing nPixelSpace,
                                    GSpacing nLineSpace, GSpacing nBandSpace)
{
    const GDALDataType eDataType = GetRasterBand(1)->GetRasterDataType();
    const int nDTSize = GDALGetDataTypeSizeBytes(eDataType);
    const bool bIsComplex = CPL_TO_BOOL(GDALDataTypeIsComplex(eDataType));
    const int nBufDTSize = GDALGetDataTypeSizeBytes(eBufType);

    // Get strip offsets.
    toff_t *panOffsets = nullptr;
    if (!TIFFGetField(m_hTIFF,
                      (TIFFIsTiled(m_hTIFF)) ? TIFFTAG_TILEOFFSETS
                                             : TIFFTAG_STRIPOFFSETS,
                      &panOffsets) ||
        panOffsets == nullptr)
    {
        return CE_Failure;
    }

    bool bUseContigImplementation = m_nPlanarConfig == PLANARCONFIG_CONTIG &&
                                    nBandCount > 1 && nBandSpace == nBufDTSize;
    if (bUseContigImplementation)
    {
        for (int iBand = 0; iBand < nBandCount; ++iBand)
        {
            const int nBand = panBandMap[iBand];
            if (nBand != iBand + 1)
            {
                bUseContigImplementation = false;
                break;
            }
        }
    }

    const int nBandsPerBlock =
        m_nPlanarConfig == PLANARCONFIG_SEPARATE ? 1 : nBands;
    const int nBandsPerBlockDTSize = nBandsPerBlock * nDTSize;
    const int nBlocksPerRow = DIV_ROUND_UP(nRasterXSize, m_nBlockXSize);
    const bool bNoTypeChange = (eDataType == eBufType);
    const bool bNoXResampling = (nXSize == nBufXSize);
    const bool bNoXResamplingNoTypeChange = (bNoTypeChange && bNoXResampling);
    const bool bByteOnly = (bNoTypeChange && nDTSize == 1);
    const bool bByteNoXResampling = (bByteOnly && bNoXResamplingNoTypeChange);
    const bool bIsByteSwapped = CPL_TO_BOOL(TIFFIsByteSwapped(m_hTIFF));
    const double dfSrcXInc = nXSize / static_cast<double>(nBufXSize);
    const double dfSrcYInc = nYSize / static_cast<double>(nBufYSize);

    int bNoDataSetIn = FALSE;
    double dfNoData = GetRasterBand(1)->GetNoDataValue(&bNoDataSetIn);
    GByte abyNoData = 0;
    if (!bNoDataSetIn)
        dfNoData = 0;
    else if (dfNoData >= 0 && dfNoData <= 255)
        abyNoData = static_cast<GByte>(dfNoData + 0.5);

    if (FetchBuffer::bMinimizeIO && TIFFIsTiled(m_hTIFF) && bNoXResampling &&
        (nYSize == nBufYSize) && m_nPlanarConfig == PLANARCONFIG_CONTIG &&
        nBandCount > 1)
    {
        GByte *pabyData = static_cast<GByte *>(pData);
        for (int y = 0; y < nBufYSize;)
        {
            const int nSrcLine = nYOff + y;
            const int nBlockYOff = nSrcLine / m_nBlockYSize;
            const int nYOffsetInBlock = nSrcLine % m_nBlockYSize;
            const int nUsedBlockHeight =
                std::min(nBufYSize - y, m_nBlockYSize - nYOffsetInBlock);

            int nBlockXOff = nXOff / m_nBlockXSize;
            int nXOffsetInBlock = nXOff % m_nBlockXSize;
            int nBlockId = nBlockXOff + nBlockYOff * nBlocksPerRow;

            int x = 0;
            while (x < nBufXSize)
            {
                const toff_t nCurOffset = panOffsets[nBlockId];
                const int nUsedBlockWidth =
                    std::min(m_nBlockXSize - nXOffsetInBlock, nBufXSize - x);

                if (nCurOffset == 0)
                {
                    REACHED(30);
                    for (int k = 0; k < nUsedBlockHeight; ++k)
                    {
                        GByte *pabyLocalData =
                            pabyData + (y + k) * nLineSpace + x * nPixelSpace;
                        for (int iBand = 0; iBand < nBandCount; ++iBand)
                        {
                            GByte *pabyLocalDataBand =
                                pabyLocalData + iBand * nBandSpace;

                            GDALCopyWords(&dfNoData, GDT_Float64, 0,
                                          pabyLocalDataBand, eBufType,
                                          static_cast<int>(nPixelSpace),
                                          nUsedBlockWidth);
                        }
                    }
                }
                else
                {
                    const int nByteOffsetInBlock =
                        nYOffsetInBlock * m_nBlockXSize * nBandsPerBlockDTSize;
                    const GByte *pabyLocalSrcDataK0 = oFetcher.FetchBytes(
                        nCurOffset + nByteOffsetInBlock,
                        m_nBlockXSize * nUsedBlockHeight * nBandsPerBlock,
                        nDTSize, bIsByteSwapped, bIsComplex, nBlockId);
                    if (pabyLocalSrcDataK0 == nullptr)
                        return CE_Failure;

                    for (int k = 0; k < nUsedBlockHeight; ++k)
                    {
                        GByte *pabyLocalData =
                            pabyData + (y + k) * nLineSpace + x * nPixelSpace;
                        const GByte *pabyLocalSrcData =
                            pabyLocalSrcDataK0 +
                            (k * m_nBlockXSize + nXOffsetInBlock) *
                                nBandsPerBlockDTSize;

                        if (bUseContigImplementation && nBands == nBandCount &&
                            nPixelSpace == nBandsPerBlockDTSize)
                        {
                            REACHED(31);
                            GDALCopyWords(pabyLocalSrcData, eDataType, nDTSize,
                                          pabyLocalData, eBufType, nBufDTSize,
                                          nUsedBlockWidth * nBands);
                        }
                        else
                        {
                            REACHED(32);
                            for (int iBand = 0; iBand < nBandCount; ++iBand)
                            {
                                GByte *pabyLocalDataBand =
                                    pabyLocalData + iBand * nBandSpace;
                                const GByte *pabyLocalSrcDataBand =
                                    pabyLocalSrcData +
                                    (panBandMap[iBand] - 1) * nDTSize;

                                GDALCopyWords(pabyLocalSrcDataBand, eDataType,
                                              nBandsPerBlockDTSize,
                                              pabyLocalDataBand, eBufType,
                                              static_cast<int>(nPixelSpace),
                                              nUsedBlockWidth);
                            }
                        }
                    }
                }

                nXOffsetInBlock = 0;
                ++nBlockXOff;
                ++nBlockId;
                x += nUsedBlockWidth;
            }

            y += nUsedBlockHeight;
        }
    }
    else if (FetchBuffer::bMinimizeIO && TIFFIsTiled(m_hTIFF) &&
             bNoXResampling && (nYSize == nBufYSize))
    // && (m_nPlanarConfig == PLANARCONFIG_SEPARATE || nBandCount == 1) )
    {
        for (int iBand = 0; iBand < nBandCount; ++iBand)
        {
            GByte *pabyData = static_cast<GByte *>(pData) + iBand * nBandSpace;
            const int nBand = panBandMap[iBand];
            for (int y = 0; y < nBufYSize;)
            {
                const int nSrcLine = nYOff + y;
                const int m_nBlockYOff = nSrcLine / m_nBlockYSize;
                const int nYOffsetIm_nBlock = nSrcLine % m_nBlockYSize;
                const int nUsedBlockHeight =
                    std::min(nBufYSize - y, m_nBlockYSize - nYOffsetIm_nBlock);

                int nBlockXOff = nXOff / m_nBlockXSize;
                int nXOffsetInBlock = nXOff % m_nBlockXSize;
                int nBlockId = nBlockXOff + m_nBlockYOff * nBlocksPerRow;
                if (m_nPlanarConfig == PLANARCONFIG_SEPARATE)
                {
                    REACHED(33);
                    nBlockId += m_nBlocksPerBand * (nBand - 1);
                }
                else
                {
                    REACHED(34);
                }

                int x = 0;
                while (x < nBufXSize)
                {
                    const toff_t nCurOffset = panOffsets[nBlockId];
                    const int nUsedBlockWidth = std::min(
                        m_nBlockXSize - nXOffsetInBlock, nBufXSize - x);

                    if (nCurOffset == 0)
                    {
                        REACHED(35);
                        for (int k = 0; k < nUsedBlockHeight; ++k)
                        {
                            GByte *pabyLocalData = pabyData +
                                                   (y + k) * nLineSpace +
                                                   x * nPixelSpace;

                            GDALCopyWords(&dfNoData, GDT_Float64, 0,
                                          pabyLocalData, eBufType,
                                          static_cast<int>(nPixelSpace),
                                          nUsedBlockWidth);
                        }
                    }
                    else
                    {
                        const int nByteOffsetIm_nBlock = nYOffsetIm_nBlock *
                                                         m_nBlockXSize *
                                                         nBandsPerBlockDTSize;
                        const GByte *pabyLocalSrcDataK0 = oFetcher.FetchBytes(
                            nCurOffset + nByteOffsetIm_nBlock,
                            m_nBlockXSize * nUsedBlockHeight * nBandsPerBlock,
                            nDTSize, bIsByteSwapped, bIsComplex, nBlockId);
                        if (pabyLocalSrcDataK0 == nullptr)
                            return CE_Failure;

                        if (m_nPlanarConfig == PLANARCONFIG_CONTIG)
                        {
                            REACHED(36);
                            pabyLocalSrcDataK0 += (nBand - 1) * nDTSize;
                        }
                        else
                        {
                            REACHED(37);
                        }

                        for (int k = 0; k < nUsedBlockHeight; ++k)
                        {
                            GByte *pabyLocalData = pabyData +
                                                   (y + k) * nLineSpace +
                                                   x * nPixelSpace;
                            const GByte *pabyLocalSrcData =
                                pabyLocalSrcDataK0 +
                                (k * m_nBlockXSize + nXOffsetInBlock) *
                                    nBandsPerBlockDTSize;

                            GDALCopyWords(
                                pabyLocalSrcData, eDataType,
                                nBandsPerBlockDTSize, pabyLocalData, eBufType,
                                static_cast<int>(nPixelSpace), nUsedBlockWidth);
                        }
                    }

                    nXOffsetInBlock = 0;
                    ++nBlockXOff;
                    ++nBlockId;
                    x += nUsedBlockWidth;
                }

                y += nUsedBlockHeight;
            }
        }
    }
    else if (FetchBuffer::bMinimizeIO && TIFFIsTiled(m_hTIFF) &&
             m_nPlanarConfig == PLANARCONFIG_CONTIG && nBandCount > 1)
    {
        GByte *pabyData = static_cast<GByte *>(pData);
        int anSrcYOffset[256] = {0};
        for (int y = 0; y < nBufYSize;)
        {
            const double dfYOffStart = nYOff + (y + 0.5) * dfSrcYInc;
            const int nSrcLine = static_cast<int>(dfYOffStart);
            const int nYOffsetIm_nBlock = nSrcLine % m_nBlockYSize;
            const int m_nBlockYOff = nSrcLine / m_nBlockYSize;
            const int nBaseByteOffsetIm_nBlock =
                nYOffsetIm_nBlock * m_nBlockXSize * nBandsPerBlockDTSize;
            int ychunk = 1;
            int nLastSrcLineK = nSrcLine;
            anSrcYOffset[0] = 0;
            for (int k = 1; k < nBufYSize - y; ++k)
            {
                int nSrcLineK =
                    nYOff + static_cast<int>((y + k + 0.5) * dfSrcYInc);
                const int nBlockYOffK = nSrcLineK / m_nBlockYSize;
                if (k < 256)
                    anSrcYOffset[k] =
                        ((nSrcLineK % m_nBlockYSize) - nYOffsetIm_nBlock) *
                        m_nBlockXSize * nBandsPerBlockDTSize;
                if (nBlockYOffK != m_nBlockYOff)
                {
                    break;
                }
                ++ychunk;
                nLastSrcLineK = nSrcLineK;
            }
            const int nUsedBlockHeight = nLastSrcLineK - nSrcLine + 1;
            // CPLAssert(nUsedBlockHeight <= m_nBlockYSize);

            double dfSrcX = nXOff + 0.5 * dfSrcXInc;
            int nCurBlockXOff = 0;
            int nNextBlockXOff = 0;
            toff_t nCurOffset = 0;
            const GByte *pabyLocalSrcDataStartLine = nullptr;
            for (int x = 0; x < nBufXSize; ++x, dfSrcX += dfSrcXInc)
            {
                const int nSrcPixel = static_cast<int>(dfSrcX);
                if (nSrcPixel >= nNextBlockXOff)
                {
                    const int nBlockXOff = nSrcPixel / m_nBlockXSize;
                    nCurBlockXOff = nBlockXOff * m_nBlockXSize;
                    nNextBlockXOff = nCurBlockXOff + m_nBlockXSize;
                    const int nBlockId =
                        nBlockXOff + m_nBlockYOff * nBlocksPerRow;
                    nCurOffset = panOffsets[nBlockId];
                    if (nCurOffset != 0)
                    {
                        pabyLocalSrcDataStartLine = oFetcher.FetchBytes(
                            nCurOffset + nBaseByteOffsetIm_nBlock,
                            m_nBlockXSize * nBandsPerBlock * nUsedBlockHeight,
                            nDTSize, bIsByteSwapped, bIsComplex, nBlockId);
                        if (pabyLocalSrcDataStartLine == nullptr)
                            return CE_Failure;
                    }
                }

                if (nCurOffset == 0)
                {
                    REACHED(38);

                    for (int k = 0; k < ychunk; ++k)
                    {
                        GByte *const pabyLocalData =
                            pabyData + (y + k) * nLineSpace + x * nPixelSpace;
                        for (int iBand = 0; iBand < nBandCount; ++iBand)
                        {
                            GDALCopyWords(&dfNoData, GDT_Float64, 0,
                                          pabyLocalData + nBandSpace * iBand,
                                          eBufType, 0, 1);
                        }
                    }
                }
                else
                {
                    const int nXOffsetInBlock = nSrcPixel - nCurBlockXOff;
                    double dfYOff = dfYOffStart;
                    const GByte *const pabyLocalSrcDataK0 =
                        pabyLocalSrcDataStartLine +
                        nXOffsetInBlock * nBandsPerBlockDTSize;
                    GByte *pabyLocalData =
                        pabyData + y * nLineSpace + x * nPixelSpace;
                    for (int k = 0; k < ychunk;
                         ++k, pabyLocalData += nLineSpace)
                    {
                        const GByte *pabyLocalSrcData = nullptr;
                        if (ychunk <= 256)
                        {
                            REACHED(39);
                            pabyLocalSrcData =
                                pabyLocalSrcDataK0 + anSrcYOffset[k];
                        }
                        else
                        {
                            REACHED(40);
                            const int nYOffsetIm_nBlockK =
                                static_cast<int>(dfYOff) % m_nBlockYSize;
                            // CPLAssert(
                            //     nYOffsetIm_nBlockK - nYOffsetIm_nBlock <=
                            //     nUsedBlockHeight);
                            pabyLocalSrcData =
                                pabyLocalSrcDataK0 +
                                (nYOffsetIm_nBlockK - nYOffsetIm_nBlock) *
                                    m_nBlockXSize * nBandsPerBlockDTSize;
                            dfYOff += dfSrcYInc;
                        }

                        if (bByteOnly)
                        {
                            REACHED(41);
                            for (int iBand = 0; iBand < nBandCount; ++iBand)
                            {
                                GByte *pabyLocalDataBand =
                                    pabyLocalData + iBand * nBandSpace;
                                const GByte *pabyLocalSrcDataBand =
                                    pabyLocalSrcData + (panBandMap[iBand] - 1);
                                *pabyLocalDataBand = *pabyLocalSrcDataBand;
                            }
                        }
                        else
                        {
                            REACHED(42);
                            for (int iBand = 0; iBand < nBandCount; ++iBand)
                            {
                                GByte *pabyLocalDataBand =
                                    pabyLocalData + iBand * nBandSpace;
                                const GByte *pabyLocalSrcDataBand =
                                    pabyLocalSrcData +
                                    (panBandMap[iBand] - 1) * nDTSize;

                                GDALCopyWords(pabyLocalSrcDataBand, eDataType,
                                              0, pabyLocalDataBand, eBufType, 0,
                                              1);
                            }
                        }
                    }
                }
            }

            y += ychunk;
        }
    }
    else if (FetchBuffer::bMinimizeIO && TIFFIsTiled(m_hTIFF))
    // && (m_nPlanarConfig == PLANARCONFIG_SEPARATE || nBandCount == 1) )
    {
        for (int iBand = 0; iBand < nBandCount; ++iBand)
        {
            GByte *pabyData = static_cast<GByte *>(pData) + iBand * nBandSpace;
            const int nBand = panBandMap[iBand];
            int anSrcYOffset[256] = {0};
            for (int y = 0; y < nBufYSize;)
            {
                const double dfYOffStart = nYOff + (y + 0.5) * dfSrcYInc;
                const int nSrcLine = static_cast<int>(dfYOffStart);
                const int nYOffsetIm_nBlock = nSrcLine % m_nBlockYSize;
                const int m_nBlockYOff = nSrcLine / m_nBlockYSize;
                const int nBaseByteOffsetIm_nBlock =
                    nYOffsetIm_nBlock * m_nBlockXSize * nBandsPerBlockDTSize;
                int ychunk = 1;
                int nLastSrcLineK = nSrcLine;
                anSrcYOffset[0] = 0;
                for (int k = 1; k < nBufYSize - y; ++k)
                {
                    const int nSrcLineK =
                        nYOff + static_cast<int>((y + k + 0.5) * dfSrcYInc);
                    const int nBlockYOffK = nSrcLineK / m_nBlockYSize;
                    if (k < 256)
                        anSrcYOffset[k] =
                            ((nSrcLineK % m_nBlockYSize) - nYOffsetIm_nBlock) *
                            m_nBlockXSize * nBandsPerBlockDTSize;
                    if (nBlockYOffK != m_nBlockYOff)
                    {
                        break;
                    }
                    ++ychunk;
                    nLastSrcLineK = nSrcLineK;
                }
                const int nUsedBlockHeight = nLastSrcLineK - nSrcLine + 1;
                // CPLAssert(nUsedBlockHeight <= m_nBlockYSize);

                double dfSrcX = nXOff + 0.5 * dfSrcXInc;
                int nCurBlockXOff = 0;
                int nNextBlockXOff = 0;
                toff_t nCurOffset = 0;
                const GByte *pabyLocalSrcDataStartLine = nullptr;
                for (int x = 0; x < nBufXSize; ++x, dfSrcX += dfSrcXInc)
                {
                    int nSrcPixel = static_cast<int>(dfSrcX);
                    if (nSrcPixel >= nNextBlockXOff)
                    {
                        const int nBlockXOff = nSrcPixel / m_nBlockXSize;
                        nCurBlockXOff = nBlockXOff * m_nBlockXSize;
                        nNextBlockXOff = nCurBlockXOff + m_nBlockXSize;
                        int nBlockId =
                            nBlockXOff + m_nBlockYOff * nBlocksPerRow;
                        if (m_nPlanarConfig == PLANARCONFIG_SEPARATE)
                        {
                            REACHED(43);
                            nBlockId += m_nBlocksPerBand * (nBand - 1);
                        }
                        else
                        {
                            REACHED(44);
                        }
                        nCurOffset = panOffsets[nBlockId];
                        if (nCurOffset != 0)
                        {
                            pabyLocalSrcDataStartLine = oFetcher.FetchBytes(
                                nCurOffset + nBaseByteOffsetIm_nBlock,
                                m_nBlockXSize * nBandsPerBlock *
                                    nUsedBlockHeight,
                                nDTSize, bIsByteSwapped, bIsComplex, nBlockId);
                            if (pabyLocalSrcDataStartLine == nullptr)
                                return CE_Failure;

                            if (m_nPlanarConfig == PLANARCONFIG_CONTIG)
                            {
                                REACHED(45);
                                pabyLocalSrcDataStartLine +=
                                    (nBand - 1) * nDTSize;
                            }
                            else
                            {
                                REACHED(46);
                            }
                        }
                    }

                    if (nCurOffset == 0)
                    {
                        REACHED(47);

                        for (int k = 0; k < ychunk; ++k)
                        {
                            GByte *const pabyLocalData = pabyData +
                                                         (y + k) * nLineSpace +
                                                         x * nPixelSpace;

                            GDALCopyWords(&dfNoData, GDT_Float64, 0,
                                          pabyLocalData, eBufType, 0, 1);
                        }
                    }
                    else
                    {
                        const int nXOffsetInBlock = nSrcPixel - nCurBlockXOff;
                        double dfYOff = dfYOffStart;
                        const GByte *const pabyLocalSrcDataK0 =
                            pabyLocalSrcDataStartLine +
                            nXOffsetInBlock * nBandsPerBlockDTSize;
                        GByte *pabyLocalData =
                            pabyData + y * nLineSpace + x * nPixelSpace;
                        for (int k = 0; k < ychunk;
                             ++k, pabyLocalData += nLineSpace)
                        {
                            const GByte *pabyLocalSrcData = nullptr;
                            if (ychunk <= 256)
                            {
                                REACHED(48);
                                pabyLocalSrcData =
                                    pabyLocalSrcDataK0 + anSrcYOffset[k];
                            }
                            else
                            {
                                REACHED(49);
                                const int nYOffsetIm_nBlockK =
                                    static_cast<int>(dfYOff) % m_nBlockYSize;
                                // CPLAssert(
                                //     nYOffsetIm_nBlockK - nYOffsetIm_nBlock <=
                                //     nUsedBlockHeight);
                                pabyLocalSrcData =
                                    pabyLocalSrcDataK0 +
                                    (nYOffsetIm_nBlockK - nYOffsetIm_nBlock) *
                                        m_nBlockXSize * nBandsPerBlockDTSize;
                                dfYOff += dfSrcYInc;
                            }

                            if (bByteOnly)
                            {
                                REACHED(50);

                                *pabyLocalData = *pabyLocalSrcData;
                            }
                            else
                            {
                                REACHED(51);

                                GDALCopyWords(pabyLocalSrcData, eDataType, 0,
                                              pabyLocalData, eBufType, 0, 1);
                            }
                        }
                    }
                }

                y += ychunk;
            }
        }
    }
    else if (bUseContigImplementation)
    {
        if (!FetchBuffer::bMinimizeIO && TIFFIsTiled(m_hTIFF))
        {
            GByte *pabyData = static_cast<GByte *>(pData);
            for (int y = 0; y < nBufYSize; ++y)
            {
                const int nSrcLine =
                    nYOff + static_cast<int>((y + 0.5) * dfSrcYInc);
                const int m_nBlockYOff = nSrcLine / m_nBlockYSize;
                const int nYOffsetIm_nBlock = nSrcLine % m_nBlockYSize;
                const int nBaseByteOffsetIm_nBlock =
                    nYOffsetIm_nBlock * m_nBlockXSize * nBandsPerBlockDTSize;

                if (bNoXResampling)
                {
                    GByte *pabyLocalData = pabyData + y * nLineSpace;
                    int nBlockXOff = nXOff / m_nBlockXSize;
                    int nXOffsetInBlock = nXOff % m_nBlockXSize;
                    int nBlockId = nBlockXOff + m_nBlockYOff * nBlocksPerRow;

                    int x = 0;
                    while (x < nBufXSize)
                    {
                        const int nByteOffsetIm_nBlock =
                            nBaseByteOffsetIm_nBlock +
                            nXOffsetInBlock * nBandsPerBlockDTSize;
                        const toff_t nCurOffset = panOffsets[nBlockId];
                        const int nUsedBlockWidth = std::min(
                            m_nBlockXSize - nXOffsetInBlock, nBufXSize - x);

                        int nIters = nUsedBlockWidth;
                        if (nCurOffset == 0)
                        {
                            if (bByteNoXResampling)
                            {
                                REACHED(0);
                                while (nIters-- > 0)
                                {
                                    for (int iBand = 0; iBand < nBandCount;
                                         ++iBand)
                                    {
                                        pabyLocalData[iBand] = abyNoData;
                                    }
                                    pabyLocalData += nPixelSpace;
                                }
                            }
                            else
                            {
                                REACHED(1);
                                while (nIters-- > 0)
                                {
                                    GDALCopyWords(&dfNoData, GDT_Float64, 0,
                                                  pabyLocalData, eBufType,
                                                  static_cast<int>(nBandSpace),
                                                  nBandCount);
                                    pabyLocalData += nPixelSpace;
                                }
                            }
                        }
                        else
                        {
                            if (bNoTypeChange && nBands == nBandCount &&
                                nPixelSpace == nBandsPerBlockDTSize)
                            {
                                REACHED(2);
                                if (!oFetcher.FetchBytes(
                                        pabyLocalData,
                                        nCurOffset + nByteOffsetIm_nBlock,
                                        nIters * nBandsPerBlock, nDTSize,
                                        bIsByteSwapped, bIsComplex, nBlockId))
                                {
                                    return CE_Failure;
                                }
                                pabyLocalData +=
                                    nIters * nBandsPerBlock * nDTSize;
                            }
                            else
                            {
                                const GByte *pabyLocalSrcData =
                                    oFetcher.FetchBytes(
                                        nCurOffset + nByteOffsetIm_nBlock,
                                        nIters * nBandsPerBlock, nDTSize,
                                        bIsByteSwapped, bIsComplex, nBlockId);
                                if (pabyLocalSrcData == nullptr)
                                    return CE_Failure;
                                if (bByteNoXResampling)
                                {
                                    REACHED(3);
                                    CopyContigByteMultiBand(
                                        pabyLocalSrcData, nBandsPerBlockDTSize,
                                        pabyLocalData,
                                        static_cast<int>(nPixelSpace), nIters,
                                        nBandCount);
                                    pabyLocalData += nIters * nPixelSpace;
                                }
                                else
                                {
                                    REACHED(4);
                                    while (nIters-- > 0)
                                    {
                                        GDALCopyWords(
                                            pabyLocalSrcData, eDataType,
                                            nDTSize, pabyLocalData, eBufType,
                                            static_cast<int>(nBandSpace),
                                            nBandCount);
                                        pabyLocalSrcData +=
                                            nBandsPerBlockDTSize;
                                        pabyLocalData += nPixelSpace;
                                    }
                                }
                            }
                        }

                        nXOffsetInBlock = 0;
                        ++nBlockXOff;
                        ++nBlockId;
                        x += nUsedBlockWidth;
                    }
                }
                else  // Contig, tiled, potential resampling & data type change.
                {
                    const GByte *pabyLocalSrcDataStartLine = nullptr;
                    GByte *pabyLocalData = pabyData + y * nLineSpace;
                    double dfSrcX = nXOff + 0.5 * dfSrcXInc;
                    int nCurBlockXOff = 0;
                    int nNextBlockXOff = 0;
                    toff_t nCurOffset = 0;
                    for (int x = 0; x < nBufXSize; ++x, dfSrcX += dfSrcXInc)
                    {
                        int nSrcPixel = static_cast<int>(dfSrcX);
                        if (nSrcPixel >= nNextBlockXOff)
                        {
                            const int nBlockXOff = nSrcPixel / m_nBlockXSize;
                            nCurBlockXOff = nBlockXOff * m_nBlockXSize;
                            nNextBlockXOff = nCurBlockXOff + m_nBlockXSize;
                            const int nBlockId =
                                nBlockXOff + m_nBlockYOff * nBlocksPerRow;
                            nCurOffset = panOffsets[nBlockId];
                            if (nCurOffset != 0)
                            {
                                pabyLocalSrcDataStartLine = oFetcher.FetchBytes(
                                    nCurOffset + nBaseByteOffsetIm_nBlock,
                                    m_nBlockXSize * nBandsPerBlock, nDTSize,
                                    bIsByteSwapped, bIsComplex, nBlockId);
                                if (pabyLocalSrcDataStartLine == nullptr)
                                    return CE_Failure;
                            }
                        }
                        const int nXOffsetInBlock = nSrcPixel - nCurBlockXOff;

                        if (nCurOffset == 0)
                        {
                            REACHED(5);
                            GDALCopyWords(&dfNoData, GDT_Float64, 0,
                                          pabyLocalData, eBufType,
                                          static_cast<int>(nBandSpace),
                                          nBandCount);
                            pabyLocalData += nPixelSpace;
                        }
                        else
                        {
                            const GByte *pabyLocalSrcData =
                                pabyLocalSrcDataStartLine +
                                nXOffsetInBlock * nBandsPerBlockDTSize;

                            REACHED(6);
                            if (bByteOnly)
                            {
                                for (int iBand = 0; iBand < nBands; ++iBand)
                                    pabyLocalData[iBand] =
                                        pabyLocalSrcData[iBand];
                            }
                            else
                            {
                                GDALCopyWords(pabyLocalSrcData, eDataType,
                                              nDTSize, pabyLocalData, eBufType,
                                              static_cast<int>(nBandSpace),
                                              nBandCount);
                            }
                            pabyLocalData += nPixelSpace;
                        }
                    }
                }
            }
        }
        else  // Contig, striped organized.
        {
            GByte *pabyData = static_cast<GByte *>(pData);
            for (int y = 0; y < nBufYSize; ++y)
            {
                const int nSrcLine =
                    nYOff + static_cast<int>((y + 0.5) * dfSrcYInc);
                const int m_nBlockYOff = nSrcLine / m_nBlockYSize;
                const int nYOffsetIm_nBlock = nSrcLine % m_nBlockYSize;
                const int nBlockId = m_nBlockYOff;
                const toff_t nCurOffset = panOffsets[nBlockId];
                if (nCurOffset == 0)
                {
                    REACHED(7);
                    for (int x = 0; x < nBufXSize; ++x)
                    {
                        GDALCopyWords(
                            &dfNoData, GDT_Float64, 0,
                            pabyData + y * nLineSpace + x * nPixelSpace,
                            eBufType, static_cast<int>(nBandSpace), nBandCount);
                    }
                }
                else
                {
                    GByte *pabyLocalData = pabyData + y * nLineSpace;
                    const int nBaseByteOffsetIm_nBlock =
                        (nYOffsetIm_nBlock * m_nBlockXSize + nXOff) *
                        nBandsPerBlockDTSize;

                    if (bNoXResamplingNoTypeChange && nBands == nBandCount &&
                        nPixelSpace == nBandsPerBlockDTSize)
                    {
                        REACHED(8);
                        if (!oFetcher.FetchBytes(
                                pabyLocalData,
                                nCurOffset + nBaseByteOffsetIm_nBlock,
                                nXSize * nBandsPerBlock, nDTSize,
                                bIsByteSwapped, bIsComplex, nBlockId))
                        {
                            return CE_Failure;
                        }
                    }
                    else
                    {
                        const GByte *pabyLocalSrcData = oFetcher.FetchBytes(
                            nCurOffset + nBaseByteOffsetIm_nBlock,
                            nXSize * nBandsPerBlock, nDTSize, bIsByteSwapped,
                            bIsComplex, nBlockId);
                        if (pabyLocalSrcData == nullptr)
                            return CE_Failure;

                        if (bByteNoXResampling)
                        {
                            REACHED(9);
                            CopyContigByteMultiBand(
                                pabyLocalSrcData, nBandsPerBlockDTSize,
                                pabyLocalData, static_cast<int>(nPixelSpace),
                                nBufXSize, nBandCount);
                        }
                        else if (bByteOnly)
                        {
                            REACHED(10);
                            double dfSrcX = 0.5 * dfSrcXInc;
                            for (int x = 0; x < nBufXSize;
                                 ++x, dfSrcX += dfSrcXInc)
                            {
                                const int nSrcPixelMinusXOff =
                                    static_cast<int>(dfSrcX);
                                for (int iBand = 0; iBand < nBandCount; ++iBand)
                                {
                                    pabyLocalData[x * nPixelSpace + iBand] =
                                        pabyLocalSrcData
                                            [nSrcPixelMinusXOff *
                                                 nBandsPerBlockDTSize +
                                             iBand];
                                }
                            }
                        }
                        else
                        {
                            REACHED(11);
                            double dfSrcX = 0.5 * dfSrcXInc;
                            for (int x = 0; x < nBufXSize;
                                 ++x, dfSrcX += dfSrcXInc)
                            {
                                int nSrcPixelMinusXOff =
                                    static_cast<int>(dfSrcX);
                                GDALCopyWords(
                                    pabyLocalSrcData + nSrcPixelMinusXOff *
                                                           nBandsPerBlockDTSize,
                                    eDataType, nDTSize,
                                    pabyLocalData + x * nPixelSpace, eBufType,
                                    static_cast<int>(nBandSpace), nBandCount);
                            }
                        }
                    }
                }
            }
        }
    }
    else  // Non-contig reading case.
    {
        if (!FetchBuffer::bMinimizeIO && TIFFIsTiled(m_hTIFF))
        {
            for (int iBand = 0; iBand < nBandCount; ++iBand)
            {
                const int nBand = panBandMap[iBand];
                GByte *const pabyData =
                    static_cast<GByte *>(pData) + iBand * nBandSpace;
                for (int y = 0; y < nBufYSize; ++y)
                {
                    const int nSrcLine =
                        nYOff + static_cast<int>((y + 0.5) * dfSrcYInc);
                    const int m_nBlockYOff = nSrcLine / m_nBlockYSize;
                    const int nYOffsetIm_nBlock = nSrcLine % m_nBlockYSize;

                    int nBaseByteOffsetIm_nBlock = nYOffsetIm_nBlock *
                                                   m_nBlockXSize *
                                                   nBandsPerBlockDTSize;
                    if (m_nPlanarConfig == PLANARCONFIG_CONTIG)
                    {
                        REACHED(12);
                        nBaseByteOffsetIm_nBlock += (nBand - 1) * nDTSize;
                    }
                    else
                    {
                        REACHED(13);
                    }

                    if (bNoXResampling)
                    {
                        GByte *pabyLocalData = pabyData + y * nLineSpace;
                        int nBlockXOff = nXOff / m_nBlockXSize;
                        int nBlockId =
                            nBlockXOff + m_nBlockYOff * nBlocksPerRow;
                        if (m_nPlanarConfig == PLANARCONFIG_SEPARATE)
                        {
                            REACHED(14);
                            nBlockId += m_nBlocksPerBand * (nBand - 1);
                        }
                        else
                        {
                            REACHED(15);
                        }
                        int nXOffsetInBlock = nXOff % m_nBlockXSize;

                        int x = 0;
                        while (x < nBufXSize)
                        {
                            const int nByteOffsetIm_nBlock =
                                nBaseByteOffsetIm_nBlock +
                                nXOffsetInBlock * nBandsPerBlockDTSize;
                            const toff_t nCurOffset = panOffsets[nBlockId];
                            const int nUsedBlockWidth = std::min(
                                m_nBlockXSize - nXOffsetInBlock, nBufXSize - x);
                            int nIters = nUsedBlockWidth;

                            if (nCurOffset == 0)
                            {
                                REACHED(16);
                                GDALCopyWords(&dfNoData, GDT_Float64, 0,
                                              pabyLocalData, eBufType,
                                              static_cast<int>(nPixelSpace),
                                              nIters);
                                pabyLocalData += nIters * nPixelSpace;
                            }
                            else
                            {
                                if (bNoTypeChange &&
                                    nPixelSpace == nBandsPerBlockDTSize)
                                {
                                    REACHED(17);
                                    if (!oFetcher.FetchBytes(
                                            pabyLocalData,
                                            nCurOffset + nByteOffsetIm_nBlock,
                                            (nIters - 1) * nBandsPerBlock + 1,
                                            nDTSize, bIsByteSwapped, bIsComplex,
                                            nBlockId))
                                    {
                                        return CE_Failure;
                                    }
                                    pabyLocalData += nIters * nPixelSpace;
                                }
                                else
                                {
                                    const GByte *pabyLocalSrcData =
                                        oFetcher.FetchBytes(
                                            nCurOffset + nByteOffsetIm_nBlock,
                                            (nIters - 1) * nBandsPerBlock + 1,
                                            nDTSize, bIsByteSwapped, bIsComplex,
                                            nBlockId);
                                    if (pabyLocalSrcData == nullptr)
                                        return CE_Failure;

                                    REACHED(18);
                                    GDALCopyWords(pabyLocalSrcData, eDataType,
                                                  nBandsPerBlockDTSize,
                                                  pabyLocalData, eBufType,
                                                  static_cast<int>(nPixelSpace),
                                                  nIters);
                                    pabyLocalData += nIters * nPixelSpace;
                                }
                            }

                            nXOffsetInBlock = 0;
                            ++nBlockXOff;
                            ++nBlockId;
                            x += nUsedBlockWidth;
                        }
                    }
                    else
                    {
                        // Non-contig reading, tiled, potential resampling and
                        // data type change.

                        const GByte *pabyLocalSrcDataStartLine = nullptr;
                        GByte *pabyLocalData = pabyData + y * nLineSpace;
                        double dfSrcX = nXOff + 0.5 * dfSrcXInc;
                        int nCurBlockXOff = 0;
                        int nNextBlockXOff = 0;
                        toff_t nCurOffset = 0;
                        for (int x = 0; x < nBufXSize; ++x, dfSrcX += dfSrcXInc)
                        {
                            const int nSrcPixel = static_cast<int>(dfSrcX);
                            if (nSrcPixel >= nNextBlockXOff)
                            {
                                const int nBlockXOff =
                                    nSrcPixel / m_nBlockXSize;
                                nCurBlockXOff = nBlockXOff * m_nBlockXSize;
                                nNextBlockXOff = nCurBlockXOff + m_nBlockXSize;
                                int nBlockId =
                                    nBlockXOff + m_nBlockYOff * nBlocksPerRow;
                                if (m_nPlanarConfig == PLANARCONFIG_SEPARATE)
                                {
                                    REACHED(19);
                                    nBlockId += m_nBlocksPerBand * (nBand - 1);
                                }
                                else
                                {
                                    REACHED(20);
                                }
                                nCurOffset = panOffsets[nBlockId];
                                if (nCurOffset != 0)
                                {
                                    pabyLocalSrcDataStartLine =
                                        oFetcher.FetchBytes(
                                            nCurOffset +
                                                nBaseByteOffsetIm_nBlock,
                                            m_nBlockXSize * nBandsPerBlock,
                                            nDTSize, bIsByteSwapped, bIsComplex,
                                            nBlockId);
                                    if (pabyLocalSrcDataStartLine == nullptr)
                                        return CE_Failure;
                                }
                            }
                            const int nXOffsetInBlock =
                                nSrcPixel - nCurBlockXOff;

                            if (nCurOffset == 0)
                            {
                                REACHED(21);
                                GDALCopyWords(&dfNoData, GDT_Float64, 0,
                                              pabyLocalData, eBufType, 0, 1);
                                pabyLocalData += nPixelSpace;
                            }
                            else
                            {
                                const GByte *pabyLocalSrcData =
                                    pabyLocalSrcDataStartLine +
                                    nXOffsetInBlock * nBandsPerBlockDTSize;

                                REACHED(22);
                                if (bByteOnly)
                                {
                                    *pabyLocalData = *pabyLocalSrcData;
                                }
                                else
                                {
                                    GDALCopyWords(pabyLocalSrcData, eDataType,
                                                  0, pabyLocalData, eBufType, 0,
                                                  1);
                                }
                                pabyLocalData += nPixelSpace;
                            }
                        }
                    }
                }
            }
        }
        else  // Non-contig reading, striped.
        {
            for (int iBand = 0; iBand < nBandCount; ++iBand)
            {
                const int nBand = panBandMap[iBand];
                GByte *pabyData =
                    static_cast<GByte *>(pData) + iBand * nBandSpace;
                for (int y = 0; y < nBufYSize; ++y)
                {
                    const int nSrcLine =
                        nYOff + static_cast<int>((y + 0.5) * dfSrcYInc);
                    const int m_nBlockYOff = nSrcLine / m_nBlockYSize;
                    const int nYOffsetIm_nBlock = nSrcLine % m_nBlockYSize;
                    int nBlockId = m_nBlockYOff;
                    if (m_nPlanarConfig == PLANARCONFIG_SEPARATE)
                    {
                        REACHED(23);
                        nBlockId += m_nBlocksPerBand * (nBand - 1);
                    }
                    else
                    {
                        REACHED(24);
                    }
                    const toff_t nCurOffset = panOffsets[nBlockId];
                    if (nCurOffset == 0)
                    {
                        REACHED(25);
                        GDALCopyWords(&dfNoData, GDT_Float64, 0,
                                      pabyData + y * nLineSpace, eBufType,
                                      static_cast<int>(nPixelSpace), nBufXSize);
                    }
                    else
                    {
                        int nBaseByteOffsetIm_nBlock =
                            (nYOffsetIm_nBlock * m_nBlockXSize + nXOff) *
                            nBandsPerBlockDTSize;
                        if (m_nPlanarConfig == PLANARCONFIG_CONTIG)
                            nBaseByteOffsetIm_nBlock += (nBand - 1) * nDTSize;

                        GByte *pabyLocalData = pabyData + y * nLineSpace;
                        if (bNoXResamplingNoTypeChange &&
                            nPixelSpace == nBandsPerBlockDTSize)
                        {
                            REACHED(26);
                            if (!oFetcher.FetchBytes(
                                    pabyLocalData,
                                    nCurOffset + nBaseByteOffsetIm_nBlock,
                                    (nXSize - 1) * nBandsPerBlock + 1, nDTSize,
                                    bIsByteSwapped, bIsComplex, nBlockId))
                            {
                                return CE_Failure;
                            }
                        }
                        else
                        {
                            const GByte *pabyLocalSrcData = oFetcher.FetchBytes(
                                nCurOffset + nBaseByteOffsetIm_nBlock,
                                (nXSize - 1) * nBandsPerBlock + 1, nDTSize,
                                bIsByteSwapped, bIsComplex, nBlockId);
                            if (pabyLocalSrcData == nullptr)
                                return CE_Failure;

                            if (bNoXResamplingNoTypeChange)
                            {
                                REACHED(27);
                                GDALCopyWords(pabyLocalSrcData, eDataType,
                                              nBandsPerBlockDTSize,
                                              pabyLocalData, eBufType,
                                              static_cast<int>(nPixelSpace),
                                              nBufXSize);
                            }
                            else if (bByteOnly)
                            {
                                REACHED(28);
                                double dfSrcX = 0.5 * dfSrcXInc;
                                for (int x = 0; x < nBufXSize;
                                     ++x, dfSrcX += dfSrcXInc)
                                {
                                    const int nSrcPixelMinusXOff =
                                        static_cast<int>(dfSrcX);
                                    pabyLocalData[x * nPixelSpace] =
                                        pabyLocalSrcData[nSrcPixelMinusXOff *
                                                         nBandsPerBlockDTSize];
                                }
                            }
                            else
                            {
                                REACHED(29);
                                double dfSrcX = 0.5 * dfSrcXInc;
                                for (int x = 0; x < nBufXSize;
                                     ++x, dfSrcX += dfSrcXInc)
                                {
                                    const int nSrcPixelMinusXOff =
                                        static_cast<int>(dfSrcX);
                                    GDALCopyWords(pabyLocalSrcData +
                                                      nSrcPixelMinusXOff *
                                                          nBandsPerBlockDTSize,
                                                  eDataType, 0,
                                                  pabyLocalData +
                                                      x * nPixelSpace,
                                                  eBufType, 0, 1);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return CE_None;
}

/************************************************************************/
/*                           DirectIO()                                 */
/************************************************************************/

// Reads directly bytes from the file using ReadMultiRange(), and by-pass
// block reading. Restricted to simple TIFF configurations
// (uncompressed data, standard data types). Particularly useful to extract
// sub-windows of data on a large /vsicurl dataset).
// Returns -1 if DirectIO() can't be supported on that file.

int GTiffDataset::DirectIO(GDALRWFlag eRWFlag, int nXOff, int nYOff, int nXSize,
                           int nYSize, void *pData, int nBufXSize,
                           int nBufYSize, GDALDataType eBufType, int nBandCount,
                           int *panBandMap, GSpacing nPixelSpace,
                           GSpacing nLineSpace, GSpacing nBandSpace,
                           GDALRasterIOExtraArg *psExtraArg)
{
    auto poProtoBand = cpl::down_cast<GTiffRasterBand *>(papoBands[0]);
    const GDALDataType eDataType = poProtoBand->GetRasterDataType();
    const int nDTSizeBits = GDALGetDataTypeSizeBits(eDataType);
    if (!(eRWFlag == GF_Read && m_nCompression == COMPRESSION_NONE &&
          (m_nPhotometric == PHOTOMETRIC_MINISBLACK ||
           m_nPhotometric == PHOTOMETRIC_RGB ||
           m_nPhotometric == PHOTOMETRIC_PALETTE) &&
          poProtoBand->IsBaseGTiffClass()))
    {
        return -1;
    }
    Crystalize();

    // Only know how to deal with nearest neighbour in this optimized routine.
    if ((nXSize != nBufXSize || nYSize != nBufYSize) && psExtraArg != nullptr &&
        psExtraArg->eResampleAlg != GRIORA_NearestNeighbour)
    {
        return -1;
    }

    // If the file is band interleave or only one band is requested, then
    // fallback to band DirectIO.
    bool bUseBandRasterIO = false;
    if (m_nPlanarConfig == PLANARCONFIG_SEPARATE || nBandCount == 1)
    {
        bUseBandRasterIO = true;
    }
    else
    {
        // For simplicity, only deals with "naturally ordered" bands.
        for (int iBand = 0; iBand < nBandCount; ++iBand)
        {
            if (panBandMap[iBand] != iBand + 1)
            {
                bUseBandRasterIO = true;
                break;
            }
        }
    }
    if (bUseBandRasterIO)
    {
        CPLErr eErr = CE_None;
        for (int iBand = 0; eErr == CE_None && iBand < nBandCount; ++iBand)
        {
            eErr =
                GetRasterBand(panBandMap[iBand])
                    ->RasterIO(eRWFlag, nXOff, nYOff, nXSize, nYSize,
                               static_cast<GByte *>(pData) + iBand * nBandSpace,
                               nBufXSize, nBufYSize, eBufType, nPixelSpace,
                               nLineSpace, psExtraArg);
        }
        return eErr;
    }

#if DEBUG_VERBOSE
    CPLDebug("GTiff", "DirectIO(%d,%d,%d,%d -> %dx%d)", nXOff, nYOff, nXSize,
             nYSize, nBufXSize, nBufYSize);
#endif

    // No need to look if overviews can satisfy the request as it has already */
    // been done in GTiffDataset::IRasterIO().

    // Make sure that TIFFTAG_STRIPOFFSETS is up-to-date.
    if (eAccess == GA_Update)
    {
        FlushCache(false);
        VSI_TIFFFlushBufferedWrite(TIFFClientdata(m_hTIFF));
    }

    if (TIFFIsTiled(m_hTIFF))
    {
        const int nDTSize = nDTSizeBits / 8;
        const size_t nTempBufferForCommonDirectIOSize = static_cast<size_t>(
            static_cast<GPtrDiff_t>(m_nBlockXSize) * m_nBlockYSize * nDTSize *
            ((m_nPlanarConfig == PLANARCONFIG_CONTIG) ? nBands : 1));
        if (m_pTempBufferForCommonDirectIO == nullptr)
        {
            m_pTempBufferForCommonDirectIO = static_cast<GByte *>(
                VSI_MALLOC_VERBOSE(nTempBufferForCommonDirectIOSize));
            if (m_pTempBufferForCommonDirectIO == nullptr)
                return CE_Failure;
        }

        VSILFILE *fp = VSI_TIFFGetVSILFile(TIFFClientdata(m_hTIFF));
        FetchBufferDirectIO oFetcher(fp, m_pTempBufferForCommonDirectIO,
                                     nTempBufferForCommonDirectIOSize);

        return CommonDirectIO(oFetcher, nXOff, nYOff, nXSize, nYSize, pData,
                              nBufXSize, nBufYSize, eBufType, nBandCount,
                              panBandMap, nPixelSpace, nLineSpace, nBandSpace);
    }

    // Get strip offsets.
    toff_t *panTIFFOffsets = nullptr;
    if (!TIFFGetField(m_hTIFF, TIFFTAG_STRIPOFFSETS, &panTIFFOffsets) ||
        panTIFFOffsets == nullptr)
    {
        return CE_Failure;
    }

    // Sub-sampling or over-sampling can only be done at last stage.
    int nReqXSize = nXSize;
    // Can do sub-sampling at the extraction stage.
    const int nReqYSize = std::min(nBufYSize, nYSize);
    void **ppData =
        static_cast<void **>(VSI_MALLOC_VERBOSE(nReqYSize * sizeof(void *)));
    vsi_l_offset *panOffsets = static_cast<vsi_l_offset *>(
        VSI_MALLOC_VERBOSE(nReqYSize * sizeof(vsi_l_offset)));
    size_t *panSizes =
        static_cast<size_t *>(VSI_MALLOC_VERBOSE(nReqYSize * sizeof(size_t)));
    const int nDTSize = GDALGetDataTypeSizeBytes(eDataType);
    void *pTmpBuffer = nullptr;
    int eErr = CE_None;
    int nContigBands = nBands;
    const int nSrcPixelSize = nDTSize * nContigBands;

    if (ppData == nullptr || panOffsets == nullptr || panSizes == nullptr)
    {
        eErr = CE_Failure;
    }
    // For now we always allocate a temp buffer as it is easier.
    else
    // if( nXSize != nBufXSize || nYSize != nBufYSize ||
    //   eBufType != eDataType ||
    //   nPixelSpace != GDALGetDataTypeSizeBytes(eBufType) ||
    //   check if the user buffer is large enough )
    {
        // We need a temporary buffer for over-sampling/sub-sampling
        // and/or data type conversion.
        pTmpBuffer = VSI_MALLOC_VERBOSE(nReqXSize * nReqYSize * nSrcPixelSize);
        if (pTmpBuffer == nullptr)
            eErr = CE_Failure;
    }

    // Prepare data extraction.
    const double dfSrcYInc = nYSize / static_cast<double>(nBufYSize);

    for (int iLine = 0; eErr == CE_None && iLine < nReqYSize; ++iLine)
    {
        ppData[iLine] = static_cast<GByte *>(pTmpBuffer) +
                        iLine * nReqXSize * nSrcPixelSize;
        int nSrcLine = 0;
        if (nBufYSize < nYSize)  // Sub-sampling in y.
            nSrcLine = nYOff + static_cast<int>((iLine + 0.5) * dfSrcYInc);
        else
            nSrcLine = nYOff + iLine;

        const int nBlockXOff = 0;
        const int nBlockYOff = nSrcLine / m_nBlockYSize;
        const int nYOffsetInBlock = nSrcLine % m_nBlockYSize;
        const int nBlocksPerRow = DIV_ROUND_UP(nRasterXSize, m_nBlockXSize);
        const int nBlockId = nBlockXOff + nBlockYOff * nBlocksPerRow;

        panOffsets[iLine] = panTIFFOffsets[nBlockId];
        if (panOffsets[iLine] == 0)  // We don't support sparse files.
            eErr = -1;

        panOffsets[iLine] +=
            (nXOff +
             static_cast<vsi_l_offset>(nYOffsetInBlock) * m_nBlockXSize) *
            nSrcPixelSize;
        panSizes[iLine] = nReqXSize * nSrcPixelSize;
    }

    // Extract data from the file.
    if (eErr == CE_None)
    {
        VSILFILE *fp = VSI_TIFFGetVSILFile(TIFFClientdata(m_hTIFF));
        const int nRet =
            VSIFReadMultiRangeL(nReqYSize, ppData, panOffsets, panSizes, fp);
        if (nRet != 0)
            eErr = CE_Failure;
    }

    // Byte-swap if necessary.
    if (eErr == CE_None && TIFFIsByteSwapped(m_hTIFF))
    {
        for (int iLine = 0; iLine < nReqYSize; ++iLine)
        {
            if (GDALDataTypeIsComplex(eDataType))
                GDALSwapWords(ppData[iLine], nDTSize / 2,
                              2 * nReqXSize * nContigBands, nDTSize / 2);
            else
                GDALSwapWords(ppData[iLine], nDTSize, nReqXSize * nContigBands,
                              nDTSize);
        }
    }

    // Over-sampling/sub-sampling and/or data type conversion.
    const double dfSrcXInc = nXSize / static_cast<double>(nBufXSize);
    if (eErr == CE_None && pTmpBuffer != nullptr)
    {
        for (int iY = 0; iY < nBufYSize; ++iY)
        {
            const int iSrcY = nBufYSize <= nYSize
                                  ? iY
                                  : static_cast<int>((iY + 0.5) * dfSrcYInc);
            // Optimization: no resampling, no data type change, number of
            // bands requested == number of bands and buffer is packed
            // pixel-interleaved.
            if (nBufXSize == nXSize && nContigBands == nBandCount &&
                eDataType == eBufType && nBandSpace == nDTSize &&
                nPixelSpace == nBandCount * nBandSpace)
            {
                memcpy(static_cast<GByte *>(pData) + iY * nLineSpace,
                       ppData[iSrcY],
                       static_cast<size_t>(nReqXSize * nPixelSpace));
            }
            // Other optimization: no resampling, no data type change,
            // data type is Byte.
            else if (nBufXSize == nXSize && eDataType == eBufType &&
                     eDataType == GDT_Byte)
            {
                GByte *pabySrcData = static_cast<GByte *>(ppData[iSrcY]);
                GByte *pabyDstData =
                    static_cast<GByte *>(pData) + iY * nLineSpace;
                if (nBandSpace == 1 && nPixelSpace > nBandCount)
                {
                    // Buffer is pixel-interleaved (with some stridding
                    // between pixels).
                    CopyContigByteMultiBand(
                        pabySrcData, nSrcPixelSize, pabyDstData,
                        static_cast<int>(nPixelSpace), nBufXSize, nBandCount);
                }
                else
                {
                    for (int iBand = 0; iBand < nBandCount; ++iBand)
                    {
                        GDALCopyWords(
                            pabySrcData + iBand, GDT_Byte, nSrcPixelSize,
                            pabyDstData + iBand * nBandSpace, GDT_Byte,
                            static_cast<int>(nPixelSpace), nBufXSize);
                    }
                }
            }
            else  // General case.
            {
                for (int iBand = 0; iBand < nBandCount; ++iBand)
                {
                    GByte *pabySrcData =
                        static_cast<GByte *>(ppData[iSrcY]) + iBand * nDTSize;
                    GByte *pabyDstData = static_cast<GByte *>(pData) +
                                         iBand * nBandSpace + iY * nLineSpace;
                    if (eDataType == GDT_Byte && eBufType == GDT_Byte)
                    {
                        double dfSrcX = 0.5 * dfSrcXInc;
                        for (int iX = 0; iX < nBufXSize;
                             ++iX, dfSrcX += dfSrcXInc)
                        {
                            int iSrcX = static_cast<int>(dfSrcX);
                            pabyDstData[iX * nPixelSpace] =
                                pabySrcData[iSrcX * nSrcPixelSize];
                        }
                    }
                    else
                    {
                        double dfSrcX = 0.5 * dfSrcXInc;
                        for (int iX = 0; iX < nBufXSize;
                             ++iX, dfSrcX += dfSrcXInc)
                        {
                            int iSrcX = static_cast<int>(dfSrcX);
                            GDALCopyWords(pabySrcData + iSrcX * nSrcPixelSize,
                                          eDataType, 0,
                                          pabyDstData + iX * nPixelSpace,
                                          eBufType, 0, 1);
                        }
                    }
                }
            }
        }
    }

    CPLFree(pTmpBuffer);
    CPLFree(ppData);
    CPLFree(panOffsets);
    CPLFree(panSizes);

    return eErr;
}

/************************************************************************/
/*                         CacheMultiRange()                            */
/************************************************************************/

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
static bool CheckTrailer(const GByte *strileData, vsi_l_offset nStrileSize)
{
    GByte abyTrailer[4];
    memcpy(abyTrailer, strileData + nStrileSize, 4);
    GByte abyLastBytes[4] = {};
    if (nStrileSize >= 4)
        memcpy(abyLastBytes, strileData + nStrileSize - 4, 4);
    else
    {
        // The last bytes will be zero due to the above {} initialization,
        // and that's what should be in abyTrailer too when the trailer is
        // correct.
        memcpy(abyLastBytes, strileData, static_cast<size_t>(nStrileSize));
    }
    return memcmp(abyTrailer, abyLastBytes, 4) == 0;
}
#endif

void *GTiffRasterBand::CacheMultiRange(int nXOff, int nYOff, int nXSize,
                                       int nYSize, int nBufXSize, int nBufYSize,
                                       GDALRasterIOExtraArg *psExtraArg)
{
    void *pBufferedData = nullptr;
    // Same logic as in GDALRasterBand::IRasterIO()
    double dfXOff = nXOff;
    double dfYOff = nYOff;
    double dfXSize = nXSize;
    double dfYSize = nYSize;
    if (psExtraArg->bFloatingPointWindowValidity)
    {
        dfXOff = psExtraArg->dfXOff;
        dfYOff = psExtraArg->dfYOff;
        dfXSize = psExtraArg->dfXSize;
        dfYSize = psExtraArg->dfYSize;
    }
    const double dfSrcXInc = dfXSize / static_cast<double>(nBufXSize);
    const double dfSrcYInc = dfYSize / static_cast<double>(nBufYSize);
    const double EPS = 1e-10;
    const int nBlockX1 =
        static_cast<int>(std::max(0.0, (0 + 0.5) * dfSrcXInc + dfXOff + EPS)) /
        nBlockXSize;
    const int nBlockY1 =
        static_cast<int>(std::max(0.0, (0 + 0.5) * dfSrcYInc + dfYOff + EPS)) /
        nBlockYSize;
    const int nBlockX2 =
        static_cast<int>(
            std::min(static_cast<double>(nRasterXSize - 1),
                     (nBufXSize - 1 + 0.5) * dfSrcXInc + dfXOff + EPS)) /
        nBlockXSize;
    const int nBlockY2 =
        static_cast<int>(
            std::min(static_cast<double>(nRasterYSize - 1),
                     (nBufYSize - 1 + 0.5) * dfSrcYInc + dfYOff + EPS)) /
        nBlockYSize;
#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    const int nBlockXCount = DIV_ROUND_UP(nRasterXSize, nBlockXSize);
    const int nBlockYCount = DIV_ROUND_UP(nRasterYSize, nBlockYSize);
    const int nBlockCount = nBlockXCount * nBlockYCount;
    struct StrileData
    {
        vsi_l_offset nOffset;
        vsi_l_offset nByteCount;
        bool bTryMask;
    };
    std::map<int, StrileData> oMapStrileToOffsetByteCount;

    // Dedicated method to retrieved the offset and size in an efficient way
    // when m_bBlockOrderRowMajor and m_bLeaderSizeAsUInt4 conditions are
    // met.
    // Except for the last block, we just read the offset from the TIFF offset
    // array, and retrieve the size in the leader 4 bytes that come before the
    // payload.
    auto OptimizedRetrievalOfOffsetSize =
        [&](int nBlockId, vsi_l_offset &nOffset, vsi_l_offset &nSize,
            size_t nTotalSize, size_t nMaxRawBlockCacheSize)
    {
        bool bTryMask = m_poGDS->m_bMaskInterleavedWithImagery;
        nOffset = TIFFGetStrileOffset(m_poGDS->m_hTIFF, nBlockId);
        if (nOffset >= 4)
        {
            if (nBlockId == nBlockCount - 1)
            {
                // Special case for the last block. As there is no next block
                // from which to retrieve an offset, use the good old method
                // that consists in reading the ByteCount array.
                if (bTryMask && m_poGDS->GetRasterBand(1)->GetMaskBand() &&
                    m_poGDS->m_poMaskDS)
                {
                    auto nMaskOffset = TIFFGetStrileOffset(
                        m_poGDS->m_poMaskDS->m_hTIFF, nBlockId);
                    if (nMaskOffset)
                    {
                        nSize = nMaskOffset +
                                TIFFGetStrileByteCount(
                                    m_poGDS->m_poMaskDS->m_hTIFF, nBlockId) -
                                nOffset;
                    }
                    else
                    {
                        bTryMask = false;
                    }
                }
                if (nSize == 0)
                {
                    nSize = TIFFGetStrileByteCount(m_poGDS->m_hTIFF, nBlockId);
                }
                if (nSize && m_poGDS->m_bTrailerRepeatedLast4BytesRepeated)
                {
                    nSize += 4;
                }
            }
            else
            {
                auto nOffsetNext =
                    TIFFGetStrileOffset(m_poGDS->m_hTIFF, nBlockId + 1);
                if (nOffsetNext > nOffset)
                {
                    nSize = nOffsetNext - nOffset;
                }
                else
                {
                    // Shouldn't happen for a compliant file
                    if (nOffsetNext != 0)
                    {
                        CPLDebug("GTiff", "Tile %d is not located after %d",
                                 nBlockId + 1, nBlockId);
                    }
                    bTryMask = false;
                    nSize = TIFFGetStrileByteCount(m_poGDS->m_hTIFF, nBlockId);
                    if (m_poGDS->m_bTrailerRepeatedLast4BytesRepeated)
                        nSize += 4;
                }
            }
            if (nSize)
            {
                nOffset -= 4;
                nSize += 4;
                if (nTotalSize + nSize < nMaxRawBlockCacheSize)
                {
                    StrileData data;
                    data.nOffset = nOffset;
                    data.nByteCount = nSize;
                    data.bTryMask = bTryMask;
                    oMapStrileToOffsetByteCount[nBlockId] = data;
                }
            }
        }
        else
        {
            // Sparse tile
            StrileData data;
            data.nOffset = 0;
            data.nByteCount = 0;
            data.bTryMask = false;
            oMapStrileToOffsetByteCount[nBlockId] = data;
        }
    };

    // This lambda fills m_poDS->m_oCacheStrileToOffsetByteCount (and
    // m_poDS->m_poMaskDS->m_oCacheStrileToOffsetByteCount, when there is a
    // mask) from the temporary oMapStrileToOffsetByteCount.
    auto FillCacheStrileToOffsetByteCount =
        [&](const std::vector<vsi_l_offset> &anOffsets,
            const std::vector<size_t> &anSizes,
            const std::vector<void *> &apData)
    {
        CPLAssert(m_poGDS->m_bLeaderSizeAsUInt4);
        size_t i = 0;
        vsi_l_offset nLastOffset = 0;
        for (const auto &entry : oMapStrileToOffsetByteCount)
        {
            const auto nBlockId = entry.first;
            const auto nOffset = entry.second.nOffset;
            const auto nSize = entry.second.nByteCount;
            if (nOffset == 0)
            {
                // Sparse tile
                m_poGDS->m_oCacheStrileToOffsetByteCount.insert(
                    nBlockId, std::pair<vsi_l_offset, vsi_l_offset>(0, 0));
                continue;
            }

            if (nOffset < nLastOffset)
            {
                // shouldn't happen normally if tiles are sorted
                i = 0;
            }
            nLastOffset = nOffset;
            while (i < anOffsets.size() &&
                   !(nOffset >= anOffsets[i] &&
                     nOffset + nSize <= anOffsets[i] + anSizes[i]))
            {
                i++;
            }
            CPLAssert(i < anOffsets.size());
            CPLAssert(nOffset >= anOffsets[i]);
            CPLAssert(nOffset + nSize <= anOffsets[i] + anSizes[i]);
            GUInt32 nSizeFromLeader;
            memcpy(&nSizeFromLeader,
                   // cppcheck-suppress containerOutOfBounds
                   static_cast<GByte *>(apData[i]) + nOffset - anOffsets[i],
                   sizeof(nSizeFromLeader));
            CPL_LSBPTR32(&nSizeFromLeader);
            bool bOK = true;
            constexpr int nLeaderSize = 4;
            const int nTrailerSize =
                (m_poGDS->m_bTrailerRepeatedLast4BytesRepeated ? 4 : 0);
            if (nSizeFromLeader > nSize - nLeaderSize - nTrailerSize)
            {
                CPLDebug("GTiff",
                         "Inconsistent block size from in leader of block %d",
                         nBlockId);
                bOK = false;
            }
            else if (m_poGDS->m_bTrailerRepeatedLast4BytesRepeated)
            {
                // Check trailer consistency
                const GByte *strileData = static_cast<GByte *>(apData[i]) +
                                          nOffset - anOffsets[i] + nLeaderSize;
                if (!CheckTrailer(strileData, nSizeFromLeader))
                {
                    CPLDebug("GTiff", "Inconsistent trailer of block %d",
                             nBlockId);
                    bOK = false;
                }
            }
            if (!bOK)
            {
                return false;
            }

            {
                const vsi_l_offset nRealOffset = nOffset + nLeaderSize;
                const vsi_l_offset nRealSize = nSizeFromLeader;
#ifdef DEBUG_VERBOSE
                CPLDebug("GTiff",
                         "Block %d found at offset " CPL_FRMT_GUIB
                         " with size " CPL_FRMT_GUIB,
                         nBlockId, nRealOffset, nRealSize);
#endif
                m_poGDS->m_oCacheStrileToOffsetByteCount.insert(
                    nBlockId, std::pair<vsi_l_offset, vsi_l_offset>(nRealOffset,
                                                                    nRealSize));
            }

            // Processing of mask
            if (!(entry.second.bTryMask &&
                  m_poGDS->m_bMaskInterleavedWithImagery &&
                  m_poGDS->GetRasterBand(1)->GetMaskBand() &&
                  m_poGDS->m_poMaskDS))
            {
                continue;
            }

            bOK = false;
            const vsi_l_offset nMaskOffsetWithLeader =
                nOffset + nLeaderSize + nSizeFromLeader + nTrailerSize;
            if (nMaskOffsetWithLeader + nLeaderSize <=
                anOffsets[i] + anSizes[i])
            {
                GUInt32 nMaskSizeFromLeader;
                memcpy(&nMaskSizeFromLeader,
                       static_cast<GByte *>(apData[i]) + nMaskOffsetWithLeader -
                           anOffsets[i],
                       sizeof(nMaskSizeFromLeader));
                CPL_LSBPTR32(&nMaskSizeFromLeader);
                if (nMaskOffsetWithLeader + nLeaderSize + nMaskSizeFromLeader +
                        nTrailerSize <=
                    anOffsets[i] + anSizes[i])
                {
                    bOK = true;
                    if (m_poGDS->m_bTrailerRepeatedLast4BytesRepeated)
                    {
                        // Check trailer consistency
                        const GByte *strileMaskData =
                            static_cast<GByte *>(apData[i]) + nOffset -
                            anOffsets[i] + nLeaderSize + nSizeFromLeader +
                            nTrailerSize + nLeaderSize;
                        if (!CheckTrailer(strileMaskData, nMaskSizeFromLeader))
                        {
                            CPLDebug("GTiff",
                                     "Inconsistent trailer of mask of block %d",
                                     nBlockId);
                            bOK = false;
                        }
                    }
                }
                if (bOK)
                {
                    const vsi_l_offset nRealOffset = nOffset + nLeaderSize +
                                                     nSizeFromLeader +
                                                     nTrailerSize + nLeaderSize;
                    const vsi_l_offset nRealSize = nMaskSizeFromLeader;
#ifdef DEBUG_VERBOSE
                    CPLDebug("GTiff",
                             "Mask of block %d found at offset " CPL_FRMT_GUIB
                             " with size " CPL_FRMT_GUIB,
                             nBlockId, nRealOffset, nRealSize);
#endif

                    m_poGDS->m_poMaskDS->m_oCacheStrileToOffsetByteCount.insert(
                        nBlockId, std::pair<vsi_l_offset, vsi_l_offset>(
                                      nRealOffset, nRealSize));
                }
            }
            if (!bOK)
            {
                CPLDebug("GTiff",
                         "Mask for block %d is not properly interleaved with "
                         "imagery block",
                         nBlockId);
            }
        }
        return true;
    };
#endif

    thandle_t th = TIFFClientdata(m_poGDS->m_hTIFF);
    if (!VSI_TIFFHasCachedRanges(th))
    {
        std::vector<std::pair<vsi_l_offset, size_t>> aOffsetSize;
        size_t nTotalSize = 0;
        nBlocksPerRow = DIV_ROUND_UP(nRasterXSize, nBlockXSize);
        const unsigned int nMaxRawBlockCacheSize = atoi(
            CPLGetConfigOption("GDAL_MAX_RAW_BLOCK_CACHE_SIZE", "10485760"));
        bool bGoOn = true;
        for (int iY = nBlockY1; bGoOn && iY <= nBlockY2; iY++)
        {
            for (int iX = nBlockX1; bGoOn && iX <= nBlockX2; iX++)
            {
                GDALRasterBlock *poBlock = TryGetLockedBlockRef(iX, iY);
                if (poBlock != nullptr)
                {
                    poBlock->DropLock();
                    continue;
                }
                int nBlockId = iX + iY * nBlocksPerRow;
                if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
                    nBlockId += (nBand - 1) * m_poGDS->m_nBlocksPerBand;
                vsi_l_offset nOffset = 0;
                vsi_l_offset nSize = 0;

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
                if ((m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG ||
                     m_poGDS->nBands == 1) &&
                    !m_poGDS->m_bStreamingIn &&
                    m_poGDS->m_bBlockOrderRowMajor &&
                    m_poGDS->m_bLeaderSizeAsUInt4)
                {
                    OptimizedRetrievalOfOffsetSize(nBlockId, nOffset, nSize,
                                                   nTotalSize,
                                                   nMaxRawBlockCacheSize);
                }
                else
#endif
                {
                    CPL_IGNORE_RET_VAL(
                        m_poGDS->IsBlockAvailable(nBlockId, &nOffset, &nSize));
                }
                if (nSize)
                {
                    if (nTotalSize + nSize < nMaxRawBlockCacheSize)
                    {
#ifdef DEBUG_VERBOSE
                        CPLDebug("GTiff",
                                 "Precaching for block (%d, %d), " CPL_FRMT_GUIB
                                 "-" CPL_FRMT_GUIB,
                                 iX, iY, nOffset,
                                 nOffset + static_cast<size_t>(nSize) - 1);
#endif
                        aOffsetSize.push_back(std::pair<vsi_l_offset, size_t>(
                            nOffset, static_cast<size_t>(nSize)));
                        nTotalSize += static_cast<size_t>(nSize);
                    }
                    else
                    {
                        bGoOn = false;
                    }
                }
            }
        }

        std::sort(aOffsetSize.begin(), aOffsetSize.end());

        if (nTotalSize > 0)
        {
            pBufferedData = VSI_MALLOC_VERBOSE(nTotalSize);
            if (pBufferedData)
            {
                std::vector<vsi_l_offset> anOffsets;
                std::vector<size_t> anSizes;
                std::vector<void *> apData;
                anOffsets.push_back(aOffsetSize[0].first);
                apData.push_back(static_cast<GByte *>(pBufferedData));
                size_t nChunkSize = aOffsetSize[0].second;
                size_t nAccOffset = 0;
                // Try to merge contiguous or slightly overlapping ranges
                for (size_t i = 0; i < aOffsetSize.size() - 1; i++)
                {
                    if (aOffsetSize[i].first < aOffsetSize[i + 1].first &&
                        aOffsetSize[i].first + aOffsetSize[i].second >=
                            aOffsetSize[i + 1].first)
                    {
                        const auto overlap = aOffsetSize[i].first +
                                             aOffsetSize[i].second -
                                             aOffsetSize[i + 1].first;
                        // That should always be the case for well behaved
                        // TIFF files.
                        if (aOffsetSize[i + 1].second > overlap)
                        {
                            nChunkSize += static_cast<size_t>(
                                aOffsetSize[i + 1].second - overlap);
                        }
                    }
                    else
                    {
                        // terminate current block
                        anSizes.push_back(nChunkSize);
#ifdef DEBUG_VERBOSE
                        CPLDebug("GTiff",
                                 "Requesting range [" CPL_FRMT_GUIB
                                 "-" CPL_FRMT_GUIB "]",
                                 anOffsets.back(),
                                 anOffsets.back() + anSizes.back() - 1);
#endif
                        nAccOffset += nChunkSize;
                        // start a new range
                        anOffsets.push_back(aOffsetSize[i + 1].first);
                        apData.push_back(static_cast<GByte *>(pBufferedData) +
                                         nAccOffset);
                        nChunkSize = aOffsetSize[i + 1].second;
                    }
                }
                // terminate last block
                anSizes.push_back(nChunkSize);
#ifdef DEBUG_VERBOSE
                CPLDebug(
                    "GTiff",
                    "Requesting range [" CPL_FRMT_GUIB "-" CPL_FRMT_GUIB "]",
                    anOffsets.back(), anOffsets.back() + anSizes.back() - 1);
#endif

                VSILFILE *fp = VSI_TIFFGetVSILFile(th);

                if (VSIFReadMultiRangeL(static_cast<int>(anSizes.size()),
                                        &apData[0], &anOffsets[0], &anSizes[0],
                                        fp) == 0)
                {
#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
                    if (!oMapStrileToOffsetByteCount.empty() &&
                        !FillCacheStrileToOffsetByteCount(anOffsets, anSizes,
                                                          apData))
                    {
                        // Retry without optimization
                        CPLFree(pBufferedData);
                        m_poGDS->m_bLeaderSizeAsUInt4 = false;
                        void *pRet =
                            CacheMultiRange(nXOff, nYOff, nXSize, nYSize,
                                            nBufXSize, nBufYSize, psExtraArg);
                        m_poGDS->m_bLeaderSizeAsUInt4 = true;
                        return pRet;
                    }

#endif
                    VSI_TIFFSetCachedRanges(
                        th, static_cast<int>(anSizes.size()), &apData[0],
                        &anOffsets[0], &anSizes[0]);
                }
            }
        }
    }
    return pBufferedData;
}

/************************************************************************/
/*                            IRasterIO()                               */
/************************************************************************/

CPLErr GTiffRasterBand::IRasterIO(GDALRWFlag eRWFlag, int nXOff, int nYOff,
                                  int nXSize, int nYSize, void *pData,
                                  int nBufXSize, int nBufYSize,
                                  GDALDataType eBufType, GSpacing nPixelSpace,
                                  GSpacing nLineSpace,
                                  GDALRasterIOExtraArg *psExtraArg)
{
#if DEBUG_VERBOSE
    CPLDebug("GTiff", "RasterIO(%d, %d, %d, %d, %d, %d)", nXOff, nYOff, nXSize,
             nYSize, nBufXSize, nBufYSize);
#endif

    // Try to pass the request to the most appropriate overview dataset.
    if (nBufXSize < nXSize && nBufYSize < nYSize)
    {
        int bTried = FALSE;
        ++m_poGDS->m_nJPEGOverviewVisibilityCounter;
        const CPLErr eErr = TryOverviewRasterIO(
            eRWFlag, nXOff, nYOff, nXSize, nYSize, pData, nBufXSize, nBufYSize,
            eBufType, nPixelSpace, nLineSpace, psExtraArg, &bTried);
        --m_poGDS->m_nJPEGOverviewVisibilityCounter;
        if (bTried)
            return eErr;
    }

    if (m_poGDS->m_eVirtualMemIOUsage != GTiffDataset::VirtualMemIOEnum::NO)
    {
        const int nErr = m_poGDS->VirtualMemIO(
            eRWFlag, nXOff, nYOff, nXSize, nYSize, pData, nBufXSize, nBufYSize,
            eBufType, 1, &nBand, nPixelSpace, nLineSpace, 0, psExtraArg);
        if (nErr >= 0)
            return static_cast<CPLErr>(nErr);
    }
    if (m_poGDS->m_bDirectIO)
    {
        int nErr =
            DirectIO(eRWFlag, nXOff, nYOff, nXSize, nYSize, pData, nBufXSize,
                     nBufYSize, eBufType, nPixelSpace, nLineSpace, psExtraArg);
        if (nErr >= 0)
            return static_cast<CPLErr>(nErr);
    }

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    bool bCanUseMultiThreadedRead = false;
    if (eRWFlag == GF_Read && m_poGDS->m_poThreadPool != nullptr &&
        nXSize == nBufXSize && nYSize == nBufYSize &&
        m_poGDS->IsMultiThreadedReadCompatible())
    {
        const int nBlockX1 = nXOff / nBlockXSize;
        const int nBlockY1 = nYOff / nBlockYSize;
        const int nBlockX2 = (nXOff + nXSize - 1) / nBlockXSize;
        const int nBlockY2 = (nYOff + nYSize - 1) / nBlockYSize;
        const int nXBlocks = nBlockX2 - nBlockX1 + 1;
        const int nYBlocks = nBlockY2 - nBlockY1 + 1;
        if (nXBlocks > 1 || nYBlocks > 1)
        {
            bCanUseMultiThreadedRead = true;
        }
    }
#endif

    void *pBufferedData = nullptr;
    if (m_poGDS->eAccess == GA_ReadOnly && eRWFlag == GF_Read &&
        m_poGDS->HasOptimizedReadMultiRange())
    {
#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
        if (bCanUseMultiThreadedRead &&
            reinterpret_cast<VSIVirtualHandle *>(
                VSI_TIFFGetVSILFile(TIFFClientdata(m_poGDS->m_hTIFF)))
                ->HasPRead())
        {
            // use the multi-threaded implementation rather than the multi-range
            // one
        }
        else
        {
            bCanUseMultiThreadedRead = false;
#endif
            GTiffRasterBand *poBandForCache = this;

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
            if (!m_poGDS->m_bStreamingIn && m_poGDS->m_bBlockOrderRowMajor &&
                m_poGDS->m_bLeaderSizeAsUInt4 &&
                m_poGDS->m_bMaskInterleavedWithImagery &&
                m_poGDS->m_poImageryDS)
            {
                poBandForCache = cpl::down_cast<GTiffRasterBand *>(
                    m_poGDS->m_poImageryDS->GetRasterBand(1));
            }
#endif
            pBufferedData = poBandForCache->CacheMultiRange(
                nXOff, nYOff, nXSize, nYSize, nBufXSize, nBufYSize, psExtraArg);
#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
        }
#endif
    }

    if (eRWFlag == GF_Read && nXSize == nBufXSize && nYSize == nBufYSize)
    {
        const int nBlockX1 = nXOff / nBlockXSize;
        const int nBlockY1 = nYOff / nBlockYSize;
        const int nBlockX2 = (nXOff + nXSize - 1) / nBlockXSize;
        const int nBlockY2 = (nYOff + nYSize - 1) / nBlockYSize;
        const int nXBlocks = nBlockX2 - nBlockX1 + 1;
        const int nYBlocks = nBlockY2 - nBlockY1 + 1;

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
        if (bCanUseMultiThreadedRead)
        {
            return m_poGDS->MultiThreadedRead(nXOff, nYOff, nXSize, nYSize,
                                              pData, eBufType, 1, &nBand,
                                              nPixelSpace, nLineSpace, 0);
        }
        else
#endif
            if (m_poGDS->nBands != 1 &&
                m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG)
        {
            const GIntBig nRequiredMem =
                static_cast<GIntBig>(m_poGDS->nBands) * nXBlocks * nYBlocks *
                nBlockXSize * nBlockYSize * GDALGetDataTypeSizeBytes(eDataType);
            if (nRequiredMem > GDALGetCacheMax64())
            {
                if (!m_poGDS->m_bHasWarnedDisableAggressiveBandCaching)
                {
                    CPLDebug("GTiff",
                             "Disable aggressive band caching. "
                             "Cache not big enough. "
                             "At least " CPL_FRMT_GIB " bytes necessary",
                             nRequiredMem);
                    m_poGDS->m_bHasWarnedDisableAggressiveBandCaching = true;
                }
                m_poGDS->m_bLoadingOtherBands = true;
            }
        }
    }

    ++m_poGDS->m_nJPEGOverviewVisibilityCounter;
    const CPLErr eErr = GDALPamRasterBand::IRasterIO(
        eRWFlag, nXOff, nYOff, nXSize, nYSize, pData, nBufXSize, nBufYSize,
        eBufType, nPixelSpace, nLineSpace, psExtraArg);
    --m_poGDS->m_nJPEGOverviewVisibilityCounter;

    m_poGDS->m_bLoadingOtherBands = false;

    if (pBufferedData)
    {
        VSIFree(pBufferedData);
        VSI_TIFFSetCachedRanges(TIFFClientdata(m_poGDS->m_hTIFF), 0, nullptr,
                                nullptr, nullptr);
    }

    return eErr;
}

/************************************************************************/
/*                       IGetDataCoverageStatus()                       */
/************************************************************************/

int GTiffRasterBand::IGetDataCoverageStatus(int nXOff, int nYOff, int nXSize,
                                            int nYSize, int nMaskFlagStop,
                                            double *pdfDataPct)
{
    if (eAccess == GA_Update)
        m_poGDS->FlushCache(false);

    const int iXBlockStart = nXOff / nBlockXSize;
    const int iXBlockEnd = (nXOff + nXSize - 1) / nBlockXSize;
    const int iYBlockStart = nYOff / nBlockYSize;
    const int iYBlockEnd = (nYOff + nYSize - 1) / nBlockYSize;
    int nStatus = 0;
    VSILFILE *fp = VSI_TIFFGetVSILFile(TIFFClientdata(m_poGDS->m_hTIFF));
    GIntBig nPixelsData = 0;
    // We need to compute this here as it might not have been computed
    // previously (which sucks...)
    nBlocksPerRow = DIV_ROUND_UP(nRasterXSize, nBlockXSize);
    for (int iY = iYBlockStart; iY <= iYBlockEnd; ++iY)
    {
        for (int iX = iXBlockStart; iX <= iXBlockEnd; ++iX)
        {
            const int nBlockIdBand0 = iX + iY * nBlocksPerRow;
            int nBlockId = nBlockIdBand0;
            if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
                nBlockId =
                    nBlockIdBand0 + (nBand - 1) * m_poGDS->m_nBlocksPerBand;
            vsi_l_offset nOffset = 0;
            vsi_l_offset nLength = 0;
            bool bHasData = false;
            if (!m_poGDS->IsBlockAvailable(nBlockId, &nOffset, &nLength))
            {
                nStatus |= GDAL_DATA_COVERAGE_STATUS_EMPTY;
            }
            else
            {
                if (m_poGDS->m_nCompression == COMPRESSION_NONE &&
                    m_poGDS->eAccess == GA_ReadOnly &&
                    ((!m_bNoDataSet && !m_bNoDataSetAsInt64 &&
                      !m_bNoDataSetAsUInt64) ||
                     (m_bNoDataSet && m_dfNoDataValue == 0.0) ||
                     (m_bNoDataSetAsInt64 && m_nNoDataValueInt64 == 0) ||
                     (m_bNoDataSetAsUInt64 && m_nNoDataValueUInt64 == 0)))
                {
                    VSIRangeStatus eStatus =
                        VSIFGetRangeStatusL(fp, nOffset, nLength);
                    if (eStatus == VSI_RANGE_STATUS_HOLE)
                    {
                        nStatus |= GDAL_DATA_COVERAGE_STATUS_EMPTY;
                    }
                    else
                    {
                        bHasData = true;
                    }
                }
                else
                {
                    bHasData = true;
                }
            }
            if (bHasData)
            {
                const int nXBlockRight =
                    (iX * nBlockXSize > INT_MAX - nBlockXSize)
                        ? INT_MAX
                        : (iX + 1) * nBlockXSize;
                const int nYBlockBottom =
                    (iY * nBlockYSize > INT_MAX - nBlockYSize)
                        ? INT_MAX
                        : (iY + 1) * nBlockYSize;

                nPixelsData += (static_cast<GIntBig>(
                                    std::min(nXBlockRight, nXOff + nXSize)) -
                                std::max(iX * nBlockXSize, nXOff)) *
                               (std::min(nYBlockBottom, nYOff + nYSize) -
                                std::max(iY * nBlockYSize, nYOff));
                nStatus |= GDAL_DATA_COVERAGE_STATUS_DATA;
            }
            if (nMaskFlagStop != 0 && (nMaskFlagStop & nStatus) != 0)
            {
                if (pdfDataPct)
                    *pdfDataPct = -1.0;
                return nStatus;
            }
        }
    }
    if (pdfDataPct)
        *pdfDataPct =
            100.0 * nPixelsData / (static_cast<GIntBig>(nXSize) * nYSize);
    return nStatus;
}

/************************************************************************/
/*                             ReadStrile()                             */
/************************************************************************/

bool GTiffDataset::ReadStrile(int nBlockId, void *pOutputBuffer,
                              GPtrDiff_t nBlockReqSize)
{
#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    // Optimization by which we can save some libtiff buffer copy
    std::pair<vsi_l_offset, vsi_l_offset> oPair;
    if (
#if TIFFLIB_VERSION <= 20220520 && !defined(INTERNAL_LIBTIFF)
        // There's a bug, up to libtiff 4.4.0, in TIFFReadFromUserBuffer()
        // which clears the TIFF_CODERSETUP flag of tif->tif_flags, which
        // causes the codec SetupDecode method to be called for each strile,
        // whereas it should normally be called only for the first decoded one.
        // For JPEG, that causes TIFFjpeg_read_header() to be called. Most
        // of the time, that works. But for some files, at some point, the
        // libjpeg machinery is not in the appropriate state for that.
        m_nCompression != COMPRESSION_JPEG &&
#endif
        m_oCacheStrileToOffsetByteCount.tryGet(nBlockId, oPair))
    {
        // For the mask, use the parent TIFF handle to get cached ranges
        auto th = TIFFClientdata(m_poImageryDS && m_bMaskInterleavedWithImagery
                                     ? m_poImageryDS->m_hTIFF
                                     : m_hTIFF);
        void *pInputBuffer = VSI_TIFFGetCachedRange(
            th, oPair.first, static_cast<size_t>(oPair.second));
        if (pInputBuffer &&
            TIFFReadFromUserBuffer(m_hTIFF, nBlockId, pInputBuffer,
                                   static_cast<size_t>(oPair.second),
                                   pOutputBuffer, nBlockReqSize))
        {
            return true;
        }
    }
#endif

    // For debugging
    if (m_poBaseDS)
        m_poBaseDS->m_bHasUsedReadEncodedAPI = true;
    else
        m_bHasUsedReadEncodedAPI = true;

#if 0
    // Can be useful to test TIFFReadFromUserBuffer() for local files
    VSILFILE* fpTIF = VSI_TIFFGetVSILFile(TIFFClientdata( m_hTIFF ));
    std::vector<GByte> tmp(TIFFGetStrileByteCount(m_hTIFF, nBlockId));
    VSIFSeekL(fpTIF, TIFFGetStrileOffset(m_hTIFF, nBlockId), SEEK_SET);
    VSIFReadL(&tmp[0], 1, TIFFGetStrileByteCount(m_hTIFF, nBlockId), fpTIF);
    if( !TIFFReadFromUserBuffer( m_hTIFF, nBlockId,
                                &tmp[0], tmp.size(),
                                pOutputBuffer, nBlockReqSize ) )
    {
        return false;
    }
#else
    // Set to 1 to allow GTiffErrorHandler to implement limitation on error
    // messages
    gnThreadLocalLibtiffError = 1;
    if (TIFFIsTiled(m_hTIFF))
    {
        if (TIFFReadEncodedTile(m_hTIFF, nBlockId, pOutputBuffer,
                                nBlockReqSize) == -1 &&
            !m_bIgnoreReadErrors)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "TIFFReadEncodedTile() failed.");
            gnThreadLocalLibtiffError = 0;
            return false;
        }
    }
    else
    {
        if (TIFFReadEncodedStrip(m_hTIFF, nBlockId, pOutputBuffer,
                                 nBlockReqSize) == -1 &&
            !m_bIgnoreReadErrors)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "TIFFReadEncodedStrip() failed.");
            gnThreadLocalLibtiffError = 0;
            return false;
        }
    }
    gnThreadLocalLibtiffError = 0;
#endif
    return true;
}

/************************************************************************/
/*                             IReadBlock()                             */
/************************************************************************/

CPLErr GTiffRasterBand::IReadBlock(int nBlockXOff, int nBlockYOff, void *pImage)

{
    m_poGDS->Crystalize();

    GPtrDiff_t nBlockBufSize = 0;
    if (TIFFIsTiled(m_poGDS->m_hTIFF))
    {
        nBlockBufSize = static_cast<GPtrDiff_t>(TIFFTileSize(m_poGDS->m_hTIFF));
    }
    else
    {
        CPLAssert(nBlockXOff == 0);
        nBlockBufSize =
            static_cast<GPtrDiff_t>(TIFFStripSize(m_poGDS->m_hTIFF));
    }

    CPLAssert(nBlocksPerRow != 0);
    const int nBlockIdBand0 = nBlockXOff + nBlockYOff * nBlocksPerRow;
    int nBlockId = nBlockIdBand0;
    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
        nBlockId = nBlockIdBand0 + (nBand - 1) * m_poGDS->m_nBlocksPerBand;

    /* -------------------------------------------------------------------- */
    /*      The bottom most partial tiles and strips are sometimes only     */
    /*      partially encoded.  This code reduces the requested data so     */
    /*      an error won't be reported in this case. (#1179)                */
    /* -------------------------------------------------------------------- */
    auto nBlockReqSize = nBlockBufSize;

    if (nBlockYOff * nBlockYSize > nRasterYSize - nBlockYSize)
    {
        nBlockReqSize =
            (nBlockBufSize / nBlockYSize) *
            (nBlockYSize -
             static_cast<int>(
                 (static_cast<GIntBig>(nBlockYOff + 1) * nBlockYSize) %
                 nRasterYSize));
    }

    /* -------------------------------------------------------------------- */
    /*      Handle the case of a strip or tile that doesn't exist yet.      */
    /*      Just set to zeros and return.                                   */
    /* -------------------------------------------------------------------- */
    vsi_l_offset nOffset = 0;
    bool bErrOccurred = false;
    if (nBlockId != m_poGDS->m_nLoadedBlock &&
        !m_poGDS->IsBlockAvailable(nBlockId, &nOffset, nullptr, &bErrOccurred))
    {
        NullBlock(pImage);
        if (bErrOccurred)
            return CE_Failure;
        return CE_None;
    }

    if (m_poGDS->m_bStreamingIn &&
        !(m_poGDS->nBands > 1 &&
          m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG &&
          nBlockId == m_poGDS->m_nLoadedBlock))
    {
        if (nOffset < VSIFTellL(m_poGDS->m_fpL))
        {
            ReportError(CE_Failure, CPLE_NotSupported,
                        "Trying to load block %d at offset " CPL_FRMT_GUIB
                        " whereas current pos is " CPL_FRMT_GUIB
                        " (backward read not supported)",
                        nBlockId, static_cast<GUIntBig>(nOffset),
                        static_cast<GUIntBig>(VSIFTellL(m_poGDS->m_fpL)));
            return CE_Failure;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Handle simple case (separate, onesampleperpixel)                */
    /* -------------------------------------------------------------------- */
    CPLErr eErr = CE_None;
    if (m_poGDS->nBands == 1 ||
        m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
    {
        if (nBlockReqSize < nBlockBufSize)
            memset(pImage, 0, nBlockBufSize);

        if (!m_poGDS->ReadStrile(nBlockId, pImage, nBlockReqSize))
        {
            memset(pImage, 0, nBlockBufSize);
            return CE_Failure;
        }
    }
    else
    {
        /* --------------------------------------------------------------------
         */
        /*      Load desired block */
        /* --------------------------------------------------------------------
         */
        eErr = m_poGDS->LoadBlockBuf(nBlockId);
        if (eErr != CE_None)
        {
            memset(pImage, 0,
                   static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize *
                       GDALGetDataTypeSizeBytes(eDataType));
            return eErr;
        }

        bool bDoCopyWords = true;
        if (nBand == 1 && !m_poGDS->m_bLoadingOtherBands &&
            eAccess == GA_ReadOnly &&
            (m_poGDS->nBands == 3 || m_poGDS->nBands == 4) &&
            ((eDataType == GDT_Byte && m_poGDS->m_nBitsPerSample == 8) ||
             (eDataType == GDT_Int16 && m_poGDS->m_nBitsPerSample == 16) ||
             (eDataType == GDT_UInt16 && m_poGDS->m_nBitsPerSample == 16)) &&
            static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize *
                    GDALGetDataTypeSizeBytes(eDataType) <
                GDALGetCacheMax64() / m_poGDS->nBands)
        {
            bDoCopyWords = false;
            void *ppDestBuffers[4];
            GDALRasterBlock *apoLockedBlocks[4] = {nullptr, nullptr, nullptr,
                                                   nullptr};
            for (int iBand = 1; iBand <= m_poGDS->nBands; ++iBand)
            {
                if (iBand == nBand)
                {
                    ppDestBuffers[iBand - 1] = pImage;
                }
                else
                {
                    GDALRasterBlock *poBlock =
                        m_poGDS->GetRasterBand(iBand)->GetLockedBlockRef(
                            nBlockXOff, nBlockYOff, true);
                    if (poBlock == nullptr)
                    {
                        bDoCopyWords = true;
                        break;
                    }
                    ppDestBuffers[iBand - 1] = poBlock->GetDataRef();
                    apoLockedBlocks[iBand - 1] = poBlock;
                }
            }
            if (!bDoCopyWords)
            {
                GDALDeinterleave(m_poGDS->m_pabyBlockBuf, eDataType,
                                 m_poGDS->nBands, ppDestBuffers, eDataType,
                                 static_cast<size_t>(nBlockXSize) *
                                     nBlockYSize);
            }
            for (int iBand = 1; iBand <= m_poGDS->nBands; ++iBand)
            {
                if (apoLockedBlocks[iBand - 1])
                {
                    apoLockedBlocks[iBand - 1]->DropLock();
                }
            }
        }

        if (bDoCopyWords)
        {
            const int nWordBytes = m_poGDS->m_nBitsPerSample / 8;
            GByte *pabyImage =
                m_poGDS->m_pabyBlockBuf + (nBand - 1) * nWordBytes;

            GDALCopyWords64(pabyImage, eDataType, m_poGDS->nBands * nWordBytes,
                            pImage, eDataType, nWordBytes,
                            static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize);

            eErr = FillCacheForOtherBands(nBlockXOff, nBlockYOff);
        }
    }

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    CacheMaskForBlock(nBlockXOff, nBlockYOff);
#endif
    return eErr;
}

/************************************************************************/
/*                           CacheMaskForBlock()                       */
/************************************************************************/

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
void GTiffRasterBand::CacheMaskForBlock(int nBlockXOff, int nBlockYOff)

{
    // Preload mask data if layout compatible and we have cached ranges
    if (m_poGDS->m_bMaskInterleavedWithImagery &&
        m_poGDS->GetRasterBand(1)->GetMaskBand() && m_poGDS->m_poMaskDS &&
        VSI_TIFFHasCachedRanges(TIFFClientdata(m_poGDS->m_hTIFF)) &&
        m_poGDS->m_poMaskDS->m_oCacheStrileToOffsetByteCount.contains(
            nBlockXOff + nBlockYOff * nBlocksPerRow))
    {
        GDALRasterBlock *poBlock =
            m_poGDS->m_poMaskDS->GetRasterBand(1)->GetLockedBlockRef(
                nBlockXOff, nBlockYOff);
        if (poBlock)
            poBlock->DropLock();
    }
}
#endif

/************************************************************************/
/*                       FillCacheForOtherBands()                       */
/************************************************************************/

CPLErr GTiffRasterBand::FillCacheForOtherBands(int nBlockXOff, int nBlockYOff)

{
    /* -------------------------------------------------------------------- */
    /*      In the fairly common case of pixel interleaved 8bit data        */
    /*      that is multi-band, lets push the rest of the data into the     */
    /*      block cache too, to avoid (hopefully) having to redecode it.    */
    /*                                                                      */
    /*      Our following logic actually depends on the fact that the       */
    /*      this block is already loaded, so subsequent calls will end      */
    /*      up back in this method and pull from the loaded block.          */
    /*                                                                      */
    /*      Be careful not entering this portion of code from               */
    /*      the other bands, otherwise we'll get very deep nested calls     */
    /*      and O(nBands^2) performance !                                   */
    /*                                                                      */
    /*      If there are many bands and the block cache size is not big     */
    /*      enough to accommodate the size of all the blocks, don't enter   */
    /* -------------------------------------------------------------------- */
    CPLErr eErr = CE_None;
    if (m_poGDS->nBands != 1 &&
        m_poGDS->nBands <
            128 &&  // avoid caching for datasets with too many bands
        !m_poGDS->m_bLoadingOtherBands &&
        static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize *
                GDALGetDataTypeSizeBytes(eDataType) <
            GDALGetCacheMax64() / m_poGDS->nBands)
    {
        m_poGDS->m_bLoadingOtherBands = true;

        for (int iOtherBand = 1; iOtherBand <= m_poGDS->nBands; ++iOtherBand)
        {
            if (iOtherBand == nBand)
                continue;

            GDALRasterBlock *poBlock =
                m_poGDS->GetRasterBand(iOtherBand)
                    ->GetLockedBlockRef(nBlockXOff, nBlockYOff);
            if (poBlock == nullptr)
            {
                eErr = CE_Failure;
                break;
            }
            poBlock->DropLock();
        }

        m_poGDS->m_bLoadingOtherBands = false;
    }

    return eErr;
}

/************************************************************************/
/*                            IWriteBlock()                             */
/************************************************************************/

CPLErr GTiffRasterBand::IWriteBlock(int nBlockXOff, int nBlockYOff,
                                    void *pImage)

{
    m_poGDS->Crystalize();

    if (m_poGDS->m_bDebugDontWriteBlocks)
        return CE_None;

    if (m_poGDS->m_bWriteError)
    {
        // Report as an error if a previously loaded block couldn't be written
        // correctly.
        return CE_Failure;
    }

    CPLAssert(nBlocksPerRow != 0);

    /* -------------------------------------------------------------------- */
    /*      Handle case of "separate" images                                */
    /* -------------------------------------------------------------------- */
    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE ||
        m_poGDS->nBands == 1)
    {
        const int nBlockId = nBlockXOff + nBlockYOff * nBlocksPerRow +
                             (nBand - 1) * m_poGDS->m_nBlocksPerBand;

        const CPLErr eErr =
            m_poGDS->WriteEncodedTileOrStrip(nBlockId, pImage, true);

        return eErr;
    }

    /* -------------------------------------------------------------------- */
    /*      Handle case of pixel interleaved (PLANARCONFIG_CONTIG) images.  */
    /* -------------------------------------------------------------------- */
    const int nBlockId = nBlockXOff + nBlockYOff * nBlocksPerRow;
    // Why 10 ? Somewhat arbitrary
    constexpr int MAX_BANDS_FOR_DIRTY_CHECK = 10;
    GDALRasterBlock *apoBlocks[MAX_BANDS_FOR_DIRTY_CHECK] = {};
    const int nBands = m_poGDS->nBands;
    bool bAllBlocksDirty = false;

    /* -------------------------------------------------------------------- */
    /*     If all blocks are cached and dirty then we do not need to reload */
    /*     the tile/strip from disk                                         */
    /* -------------------------------------------------------------------- */
    if (nBands <= MAX_BANDS_FOR_DIRTY_CHECK)
    {
        bAllBlocksDirty = true;
        for (int iBand = 0; iBand < nBands; ++iBand)
        {
            if (iBand + 1 != nBand)
            {
                apoBlocks[iBand] =
                    cpl::down_cast<GTiffRasterBand *>(
                        m_poGDS->GetRasterBand(iBand + 1))
                        ->TryGetLockedBlockRef(nBlockXOff, nBlockYOff);

                if (apoBlocks[iBand] == nullptr)
                {
                    bAllBlocksDirty = false;
                }
                else if (!apoBlocks[iBand]->GetDirty())
                {
                    apoBlocks[iBand]->DropLock();
                    apoBlocks[iBand] = nullptr;
                    bAllBlocksDirty = false;
                }
            }
            else
                apoBlocks[iBand] = nullptr;
        }
#if DEBUG_VERBOSE
        if (bAllBlocksDirty)
            CPLDebug("GTIFF", "Saved reloading block %d", nBlockId);
        else
            CPLDebug("GTIFF", "Must reload block %d", nBlockId);
#endif
    }

    {
        const CPLErr eErr = m_poGDS->LoadBlockBuf(nBlockId, !bAllBlocksDirty);
        if (eErr != CE_None)
        {
            if (nBands <= MAX_BANDS_FOR_DIRTY_CHECK)
            {
                for (int iBand = 0; iBand < nBands; ++iBand)
                {
                    if (apoBlocks[iBand] != nullptr)
                        apoBlocks[iBand]->DropLock();
                }
            }
            return eErr;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      On write of pixel interleaved data, we might as well flush      */
    /*      out any other bands that are dirty in our cache.  This is       */
    /*      especially helpful when writing compressed blocks.              */
    /* -------------------------------------------------------------------- */
    const int nWordBytes = m_poGDS->m_nBitsPerSample / 8;

    for (int iBand = 0; iBand < nBands; ++iBand)
    {
        const GByte *pabyThisImage = nullptr;
        GDALRasterBlock *poBlock = nullptr;

        if (iBand + 1 == nBand)
        {
            pabyThisImage = static_cast<GByte *>(pImage);
        }
        else
        {
            if (nBands <= MAX_BANDS_FOR_DIRTY_CHECK)
                poBlock = apoBlocks[iBand];
            else
                poBlock = cpl::down_cast<GTiffRasterBand *>(
                              m_poGDS->GetRasterBand(iBand + 1))
                              ->TryGetLockedBlockRef(nBlockXOff, nBlockYOff);

            if (poBlock == nullptr)
                continue;

            if (!poBlock->GetDirty())
            {
                poBlock->DropLock();
                continue;
            }

            pabyThisImage = static_cast<GByte *>(poBlock->GetDataRef());
        }

        GByte *pabyOut = m_poGDS->m_pabyBlockBuf + iBand * nWordBytes;

        GDALCopyWords64(pabyThisImage, eDataType, nWordBytes, pabyOut,
                        eDataType, nWordBytes * nBands,
                        static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize);

        if (poBlock != nullptr)
        {
            poBlock->MarkClean();
            poBlock->DropLock();
        }
    }

    if (bAllBlocksDirty)
    {
        // We can synchronously write the block now.
        const CPLErr eErr = m_poGDS->WriteEncodedTileOrStrip(
            nBlockId, m_poGDS->m_pabyBlockBuf, true);
        m_poGDS->m_bLoadedBlockDirty = false;
        return eErr;
    }

    m_poGDS->m_bLoadedBlockDirty = true;

    return CE_None;
}

/************************************************************************/
/*                           SetDescription()                           */
/************************************************************************/

void GTiffRasterBand::SetDescription(const char *pszDescription)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (pszDescription == nullptr)
        pszDescription = "";

    if (m_osDescription != pszDescription)
        m_poGDS->m_bMetadataChanged = true;

    m_osDescription = pszDescription;
}

/************************************************************************/
/*                           GetDescription()                           */
/************************************************************************/

const char *GTiffRasterBand::GetDescription() const
{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    return m_osDescription;
}

/************************************************************************/
/*                             GetOffset()                              */
/************************************************************************/

double GTiffRasterBand::GetOffset(int *pbSuccess)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (pbSuccess)
        *pbSuccess = m_bHaveOffsetScale;
    return m_dfOffset;
}

/************************************************************************/
/*                             SetOffset()                              */
/************************************************************************/

CPLErr GTiffRasterBand::SetOffset(double dfNewValue)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (!m_bHaveOffsetScale || dfNewValue != m_dfOffset)
        m_poGDS->m_bMetadataChanged = true;

    m_bHaveOffsetScale = true;
    m_dfOffset = dfNewValue;
    return CE_None;
}

/************************************************************************/
/*                              GetScale()                              */
/************************************************************************/

double GTiffRasterBand::GetScale(int *pbSuccess)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (pbSuccess)
        *pbSuccess = m_bHaveOffsetScale;
    return m_dfScale;
}

/************************************************************************/
/*                              SetScale()                              */
/************************************************************************/

CPLErr GTiffRasterBand::SetScale(double dfNewValue)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (!m_bHaveOffsetScale || dfNewValue != m_dfScale)
        m_poGDS->m_bMetadataChanged = true;

    m_bHaveOffsetScale = true;
    m_dfScale = dfNewValue;
    return CE_None;
}

/************************************************************************/
/*                            GetUnitType()                             */
/************************************************************************/

const char *GTiffRasterBand::GetUnitType()

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();
    if (m_osUnitType.empty())
    {
        m_poGDS->LookForProjection();
        if (m_poGDS->m_pszVertUnit)
            return m_poGDS->m_pszVertUnit;
    }

    return m_osUnitType.c_str();
}

/************************************************************************/
/*                           SetUnitType()                              */
/************************************************************************/

CPLErr GTiffRasterBand::SetUnitType(const char *pszNewValue)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    CPLString osNewValue(pszNewValue ? pszNewValue : "");
    if (osNewValue.compare(m_osUnitType) != 0)
        m_poGDS->m_bMetadataChanged = true;

    m_osUnitType = osNewValue;
    return CE_None;
}

/************************************************************************/
/*                      GetMetadataDomainList()                         */
/************************************************************************/

char **GTiffRasterBand::GetMetadataDomainList()
{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    return CSLDuplicate(m_oGTiffMDMD.GetDomainList());
}

/************************************************************************/
/*                            GetMetadata()                             */
/************************************************************************/

char **GTiffRasterBand::GetMetadata(const char *pszDomain)

{
    if (pszDomain == nullptr || !EQUAL(pszDomain, "IMAGE_STRUCTURE"))
    {
        m_poGDS->LoadGeoreferencingAndPamIfNeeded();
    }

    return m_oGTiffMDMD.GetMetadata(pszDomain);
}

/************************************************************************/
/*                            SetMetadata()                             */
/************************************************************************/

CPLErr GTiffRasterBand::SetMetadata(char **papszMD, const char *pszDomain)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (m_poGDS->m_bStreamingOut && m_poGDS->m_bCrystalized)
    {
        ReportError(CE_Failure, CPLE_NotSupported,
                    "Cannot modify metadata at that point in a streamed "
                    "output file");
        return CE_Failure;
    }

    CPLErr eErr = CE_None;
    if (eAccess == GA_Update)
    {
        if (pszDomain == nullptr || !EQUAL(pszDomain, "_temporary_"))
        {
            if (papszMD != nullptr || GetMetadata(pszDomain) != nullptr)
            {
                m_poGDS->m_bMetadataChanged = true;
                // Cancel any existing metadata from PAM file.
                if (GDALPamRasterBand::GetMetadata(pszDomain) != nullptr)
                    GDALPamRasterBand::SetMetadata(nullptr, pszDomain);
            }
        }
    }
    else
    {
        CPLDebug(
            "GTIFF",
            "GTiffRasterBand::SetMetadata() goes to PAM instead of TIFF tags");
        eErr = GDALPamRasterBand::SetMetadata(papszMD, pszDomain);
    }

    if (eErr == CE_None)
    {
        eErr = m_oGTiffMDMD.SetMetadata(papszMD, pszDomain);
    }
    return eErr;
}

/************************************************************************/
/*                          GetMetadataItem()                           */
/************************************************************************/

const char *GTiffRasterBand::GetMetadataItem(const char *pszName,
                                             const char *pszDomain)

{
    if (pszDomain == nullptr || !EQUAL(pszDomain, "IMAGE_STRUCTURE"))
    {
        m_poGDS->LoadGeoreferencingAndPamIfNeeded();
    }

    if (pszName != nullptr && pszDomain != nullptr && EQUAL(pszDomain, "TIFF"))
    {
        int nBlockXOff = 0;
        int nBlockYOff = 0;

        if (EQUAL(pszName, "JPEGTABLES"))
        {
            uint32_t nJPEGTableSize = 0;
            void *pJPEGTable = nullptr;
            if (TIFFGetField(m_poGDS->m_hTIFF, TIFFTAG_JPEGTABLES,
                             &nJPEGTableSize, &pJPEGTable) != 1 ||
                pJPEGTable == nullptr || nJPEGTableSize > INT_MAX)
            {
                return nullptr;
            }
            char *const pszHex = CPLBinaryToHex(
                nJPEGTableSize, static_cast<const GByte *>(pJPEGTable));
            const char *pszReturn = CPLSPrintf("%s", pszHex);
            CPLFree(pszHex);

            return pszReturn;
        }

        if (EQUAL(pszName, "IFD_OFFSET"))
        {
            return CPLSPrintf(CPL_FRMT_GUIB,
                              static_cast<GUIntBig>(m_poGDS->m_nDirOffset));
        }

        if (sscanf(pszName, "BLOCK_OFFSET_%d_%d", &nBlockXOff, &nBlockYOff) ==
            2)
        {
            nBlocksPerRow =
                DIV_ROUND_UP(m_poGDS->nRasterXSize, m_poGDS->m_nBlockXSize);
            nBlocksPerColumn =
                DIV_ROUND_UP(m_poGDS->nRasterYSize, m_poGDS->m_nBlockYSize);
            if (nBlockXOff < 0 || nBlockXOff >= nBlocksPerRow ||
                nBlockYOff < 0 || nBlockYOff >= nBlocksPerColumn)
                return nullptr;

            int nBlockId = nBlockYOff * nBlocksPerRow + nBlockXOff;
            if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
            {
                nBlockId += (nBand - 1) * m_poGDS->m_nBlocksPerBand;
            }

            vsi_l_offset nOffset = 0;
            if (!m_poGDS->IsBlockAvailable(nBlockId, &nOffset))
            {
                return nullptr;
            }

            return CPLSPrintf(CPL_FRMT_GUIB, static_cast<GUIntBig>(nOffset));
        }

        if (sscanf(pszName, "BLOCK_SIZE_%d_%d", &nBlockXOff, &nBlockYOff) == 2)
        {
            nBlocksPerRow =
                DIV_ROUND_UP(m_poGDS->nRasterXSize, m_poGDS->m_nBlockXSize);
            nBlocksPerColumn =
                DIV_ROUND_UP(m_poGDS->nRasterYSize, m_poGDS->m_nBlockYSize);
            if (nBlockXOff < 0 || nBlockXOff >= nBlocksPerRow ||
                nBlockYOff < 0 || nBlockYOff >= nBlocksPerColumn)
                return nullptr;

            int nBlockId = nBlockYOff * nBlocksPerRow + nBlockXOff;
            if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
            {
                nBlockId += (nBand - 1) * m_poGDS->m_nBlocksPerBand;
            }

            vsi_l_offset nByteCount = 0;
            if (!m_poGDS->IsBlockAvailable(nBlockId, nullptr, &nByteCount))
            {
                return nullptr;
            }

            return CPLSPrintf(CPL_FRMT_GUIB, static_cast<GUIntBig>(nByteCount));
        }
    }
    return m_oGTiffMDMD.GetMetadataItem(pszName, pszDomain);
}

/************************************************************************/
/*                          SetMetadataItem()                           */
/************************************************************************/

CPLErr GTiffRasterBand::SetMetadataItem(const char *pszName,
                                        const char *pszValue,
                                        const char *pszDomain)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (m_poGDS->m_bStreamingOut && m_poGDS->m_bCrystalized)
    {
        ReportError(CE_Failure, CPLE_NotSupported,
                    "Cannot modify metadata at that point in a streamed "
                    "output file");
        return CE_Failure;
    }

    CPLErr eErr = CE_None;
    if (eAccess == GA_Update)
    {
        if (pszDomain == nullptr || !EQUAL(pszDomain, "_temporary_"))
        {
            m_poGDS->m_bMetadataChanged = true;
            // Cancel any existing metadata from PAM file.
            if (GDALPamRasterBand::GetMetadataItem(pszName, pszDomain) !=
                nullptr)
                GDALPamRasterBand::SetMetadataItem(pszName, nullptr, pszDomain);
        }
    }
    else
    {
        CPLDebug("GTIFF", "GTiffRasterBand::SetMetadataItem() goes to PAM "
                          "instead of TIFF tags");
        eErr = GDALPamRasterBand::SetMetadataItem(pszName, pszValue, pszDomain);
    }

    if (eErr == CE_None)
    {
        eErr = m_oGTiffMDMD.SetMetadataItem(pszName, pszValue, pszDomain);
    }
    return eErr;
}

/************************************************************************/
/*                       GetColorInterpretation()                       */
/************************************************************************/

GDALColorInterp GTiffRasterBand::GetColorInterpretation()

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    return m_eBandInterp;
}

/************************************************************************/
/*                         GTiffGetAlphaValue()                         */
/************************************************************************/

uint16_t GTiffGetAlphaValue(const char *pszValue, uint16_t nDefault)
{
    if (pszValue == nullptr)
        return nDefault;
    if (EQUAL(pszValue, "YES"))
        return DEFAULT_ALPHA_TYPE;
    if (EQUAL(pszValue, "PREMULTIPLIED"))
        return EXTRASAMPLE_ASSOCALPHA;
    if (EQUAL(pszValue, "NON-PREMULTIPLIED"))
        return EXTRASAMPLE_UNASSALPHA;
    if (EQUAL(pszValue, "NO") || EQUAL(pszValue, "UNSPECIFIED"))
        return EXTRASAMPLE_UNSPECIFIED;

    return nDefault;
}

/************************************************************************/
/*                       SetColorInterpretation()                       */
/************************************************************************/

CPLErr GTiffRasterBand::SetColorInterpretation(GDALColorInterp eInterp)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (eInterp == m_eBandInterp)
        return CE_None;

    m_eBandInterp = eInterp;

    if (eAccess != GA_Update)
    {
        CPLDebug("GTIFF",
                 "ColorInterpretation %s for band %d goes to PAM "
                 "instead of TIFF tag",
                 GDALGetColorInterpretationName(eInterp), nBand);
        return GDALPamRasterBand::SetColorInterpretation(eInterp);
    }

    m_poGDS->m_bNeedsRewrite = true;
    m_poGDS->m_bMetadataChanged = true;

    // Try to autoset TIFFTAG_PHOTOMETRIC = PHOTOMETRIC_RGB if possible.
    if (m_poGDS->nBands >= 3 && m_poGDS->m_nCompression != COMPRESSION_JPEG &&
        m_poGDS->m_nPhotometric != PHOTOMETRIC_RGB &&
        CSLFetchNameValue(m_poGDS->m_papszCreationOptions, "PHOTOMETRIC") ==
            nullptr &&
        ((nBand == 1 && eInterp == GCI_RedBand) ||
         (nBand == 2 && eInterp == GCI_GreenBand) ||
         (nBand == 3 && eInterp == GCI_BlueBand)))
    {
        if (m_poGDS->GetRasterBand(1)->GetColorInterpretation() ==
                GCI_RedBand &&
            m_poGDS->GetRasterBand(2)->GetColorInterpretation() ==
                GCI_GreenBand &&
            m_poGDS->GetRasterBand(3)->GetColorInterpretation() == GCI_BlueBand)
        {
            m_poGDS->m_nPhotometric = PHOTOMETRIC_RGB;
            TIFFSetField(m_poGDS->m_hTIFF, TIFFTAG_PHOTOMETRIC,
                         m_poGDS->m_nPhotometric);

            // We need to update the number of extra samples.
            uint16_t *v = nullptr;
            uint16_t count = 0;
            const uint16_t nNewExtraSamplesCount =
                static_cast<uint16_t>(m_poGDS->nBands - 3);
            if (m_poGDS->nBands >= 4 &&
                TIFFGetField(m_poGDS->m_hTIFF, TIFFTAG_EXTRASAMPLES, &count,
                             &v) &&
                count > nNewExtraSamplesCount)
            {
                uint16_t *const pasNewExtraSamples = static_cast<uint16_t *>(
                    CPLMalloc(nNewExtraSamplesCount * sizeof(uint16_t)));
                memcpy(pasNewExtraSamples, v + count - nNewExtraSamplesCount,
                       nNewExtraSamplesCount * sizeof(uint16_t));

                TIFFSetField(m_poGDS->m_hTIFF, TIFFTAG_EXTRASAMPLES,
                             nNewExtraSamplesCount, pasNewExtraSamples);

                CPLFree(pasNewExtraSamples);
            }
        }
        return CE_None;
    }

    // On the contrary, cancel the above if needed
    if (m_poGDS->m_nCompression != COMPRESSION_JPEG &&
        m_poGDS->m_nPhotometric == PHOTOMETRIC_RGB &&
        CSLFetchNameValue(m_poGDS->m_papszCreationOptions, "PHOTOMETRIC") ==
            nullptr &&
        ((nBand == 1 && eInterp != GCI_RedBand) ||
         (nBand == 2 && eInterp != GCI_GreenBand) ||
         (nBand == 3 && eInterp != GCI_BlueBand)))
    {
        m_poGDS->m_nPhotometric = PHOTOMETRIC_MINISBLACK;
        TIFFSetField(m_poGDS->m_hTIFF, TIFFTAG_PHOTOMETRIC,
                     m_poGDS->m_nPhotometric);

        // We need to update the number of extra samples.
        uint16_t *v = nullptr;
        uint16_t count = 0;
        const uint16_t nNewExtraSamplesCount =
            static_cast<uint16_t>(m_poGDS->nBands - 1);
        if (m_poGDS->nBands >= 2)
        {
            TIFFGetField(m_poGDS->m_hTIFF, TIFFTAG_EXTRASAMPLES, &count, &v);
            if (nNewExtraSamplesCount > count)
            {
                uint16_t *const pasNewExtraSamples = static_cast<uint16_t *>(
                    CPLMalloc(nNewExtraSamplesCount * sizeof(uint16_t)));
                for (int i = 0;
                     i < static_cast<int>(nNewExtraSamplesCount - count); ++i)
                    pasNewExtraSamples[i] = EXTRASAMPLE_UNSPECIFIED;
                if (count > 0)
                {
                    memcpy(pasNewExtraSamples + nNewExtraSamplesCount - count,
                           v, count * sizeof(uint16_t));
                }

                TIFFSetField(m_poGDS->m_hTIFF, TIFFTAG_EXTRASAMPLES,
                             nNewExtraSamplesCount, pasNewExtraSamples);

                CPLFree(pasNewExtraSamples);
            }
        }
    }

    // Mark alpha band / undefined in extrasamples.
    if (eInterp == GCI_AlphaBand || eInterp == GCI_Undefined)
    {
        uint16_t *v = nullptr;
        uint16_t count = 0;
        if (TIFFGetField(m_poGDS->m_hTIFF, TIFFTAG_EXTRASAMPLES, &count, &v))
        {
            const int nBaseSamples = m_poGDS->m_nSamplesPerPixel - count;

            if (eInterp == GCI_AlphaBand)
            {
                for (int i = 1; i <= m_poGDS->nBands; ++i)
                {
                    if (i != nBand &&
                        m_poGDS->GetRasterBand(i)->GetColorInterpretation() ==
                            GCI_AlphaBand)
                    {
                        if (i == nBaseSamples + 1 &&
                            CSLFetchNameValue(m_poGDS->m_papszCreationOptions,
                                              "ALPHA") != nullptr)
                        {
                            ReportError(
                                CE_Warning, CPLE_AppDefined,
                                "Band %d was already identified as alpha band, "
                                "and band %d is now marked as alpha too. "
                                "Presumably ALPHA creation option is not "
                                "needed",
                                i, nBand);
                        }
                        else
                        {
                            ReportError(
                                CE_Warning, CPLE_AppDefined,
                                "Band %d was already identified as alpha band, "
                                "and band %d is now marked as alpha too",
                                i, nBand);
                        }
                    }
                }
            }

            if (nBand > nBaseSamples && nBand - nBaseSamples - 1 < count)
            {
                // We need to allocate a new array as (current) libtiff
                // versions will not like that we reuse the array we got from
                // TIFFGetField().

                uint16_t *pasNewExtraSamples = static_cast<uint16_t *>(
                    CPLMalloc(count * sizeof(uint16_t)));
                memcpy(pasNewExtraSamples, v, count * sizeof(uint16_t));
                if (eInterp == GCI_AlphaBand)
                {
                    pasNewExtraSamples[nBand - nBaseSamples - 1] =
                        GTiffGetAlphaValue(
                            CPLGetConfigOption("GTIFF_ALPHA", nullptr),
                            DEFAULT_ALPHA_TYPE);
                }
                else
                {
                    pasNewExtraSamples[nBand - nBaseSamples - 1] =
                        EXTRASAMPLE_UNSPECIFIED;
                }

                TIFFSetField(m_poGDS->m_hTIFF, TIFFTAG_EXTRASAMPLES, count,
                             pasNewExtraSamples);

                CPLFree(pasNewExtraSamples);

                return CE_None;
            }
        }
    }

    if (m_poGDS->m_nPhotometric != PHOTOMETRIC_MINISBLACK &&
        CSLFetchNameValue(m_poGDS->m_papszCreationOptions, "PHOTOMETRIC") ==
            nullptr)
    {
        m_poGDS->m_nPhotometric = PHOTOMETRIC_MINISBLACK;
        TIFFSetField(m_poGDS->m_hTIFF, TIFFTAG_PHOTOMETRIC,
                     m_poGDS->m_nPhotometric);
    }

    return CE_None;
}

/************************************************************************/
/*                           GetColorTable()                            */
/************************************************************************/

GDALColorTable *GTiffRasterBand::GetColorTable()

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (nBand == 1)
        return m_poGDS->m_poColorTable;

    return nullptr;
}

/************************************************************************/
/*                           SetColorTable()                            */
/************************************************************************/

CPLErr GTiffRasterBand::SetColorTable(GDALColorTable *poCT)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    /* -------------------------------------------------------------------- */
    /*      Check if this is even a candidate for applying a PCT.           */
    /* -------------------------------------------------------------------- */
    if (eAccess == GA_Update)
    {
        if (nBand != 1)
        {
            ReportError(CE_Failure, CPLE_NotSupported,
                        "SetColorTable() can only be called on band 1.");
            return CE_Failure;
        }

        if (m_poGDS->m_nSamplesPerPixel != 1 &&
            m_poGDS->m_nSamplesPerPixel != 2)
        {
            ReportError(CE_Failure, CPLE_NotSupported,
                        "SetColorTable() not supported for multi-sample TIFF "
                        "files.");
            return CE_Failure;
        }

        if (eDataType != GDT_Byte && eDataType != GDT_UInt16)
        {
            ReportError(
                CE_Failure, CPLE_NotSupported,
                "SetColorTable() only supported for Byte or UInt16 bands "
                "in TIFF format.");
            return CE_Failure;
        }

        // Clear any existing PAM color table
        if (GDALPamRasterBand::GetColorTable() != nullptr)
        {
            GDALPamRasterBand::SetColorTable(nullptr);
            GDALPamRasterBand::SetColorInterpretation(GCI_Undefined);
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Is this really a request to clear the color table?              */
    /* -------------------------------------------------------------------- */
    if (poCT == nullptr || poCT->GetColorEntryCount() == 0)
    {
        if (eAccess == GA_Update)
        {
            TIFFSetField(m_poGDS->m_hTIFF, TIFFTAG_PHOTOMETRIC,
                         PHOTOMETRIC_MINISBLACK);

            TIFFUnsetField(m_poGDS->m_hTIFF, TIFFTAG_COLORMAP);
        }

        if (m_poGDS->m_poColorTable)
        {
            delete m_poGDS->m_poColorTable;
            m_poGDS->m_poColorTable = nullptr;
        }

        return CE_None;
    }

    /* -------------------------------------------------------------------- */
    /*      Write out the colortable, and update the configuration.         */
    /* -------------------------------------------------------------------- */
    CPLErr eErr = CE_None;
    if (eAccess == GA_Update)
    {
        int nColors = 65536;

        if (eDataType == GDT_Byte)
            nColors = 256;

        unsigned short *panTRed = static_cast<unsigned short *>(
            CPLMalloc(sizeof(unsigned short) * nColors));
        unsigned short *panTGreen = static_cast<unsigned short *>(
            CPLMalloc(sizeof(unsigned short) * nColors));
        unsigned short *panTBlue = static_cast<unsigned short *>(
            CPLMalloc(sizeof(unsigned short) * nColors));

        for (int iColor = 0; iColor < nColors; ++iColor)
        {
            if (iColor < poCT->GetColorEntryCount())
            {
                GDALColorEntry sRGB;
                poCT->GetColorEntryAsRGB(iColor, &sRGB);

                panTRed[iColor] = static_cast<unsigned short>(257 * sRGB.c1);
                panTGreen[iColor] = static_cast<unsigned short>(257 * sRGB.c2);
                panTBlue[iColor] = static_cast<unsigned short>(257 * sRGB.c3);
            }
            else
            {
                panTRed[iColor] = 0;
                panTGreen[iColor] = 0;
                panTBlue[iColor] = 0;
            }
        }

        TIFFSetField(m_poGDS->m_hTIFF, TIFFTAG_PHOTOMETRIC,
                     PHOTOMETRIC_PALETTE);
        TIFFSetField(m_poGDS->m_hTIFF, TIFFTAG_COLORMAP, panTRed, panTGreen,
                     panTBlue);

        CPLFree(panTRed);
        CPLFree(panTGreen);
        CPLFree(panTBlue);

        // libtiff 3.X needs setting this in all cases (creation or update)
        // whereas libtiff 4.X would just need it if there
        // was no color table before.
        m_poGDS->m_bNeedsRewrite = true;
    }
    else
    {
        eErr = GDALPamRasterBand::SetColorTable(poCT);
    }

    if (m_poGDS->m_poColorTable)
        delete m_poGDS->m_poColorTable;

    m_poGDS->m_poColorTable = poCT->Clone();
    m_eBandInterp = GCI_PaletteIndex;

    return eErr;
}

/************************************************************************/
/*                           GetNoDataValue()                           */
/************************************************************************/

double GTiffRasterBand::GetNoDataValue(int *pbSuccess)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    int bSuccess = FALSE;
    double dfNoDataValue = GDALPamRasterBand::GetNoDataValue(&bSuccess);
    if (bSuccess)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return dfNoDataValue;
    }

    if (m_bNoDataSet)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return m_dfNoDataValue;
    }

    if (m_poGDS->m_bNoDataSet)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return m_poGDS->m_dfNoDataValue;
    }

    if (m_bNoDataSetAsInt64)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return GDALGetNoDataValueCastToDouble(m_nNoDataValueInt64);
    }

    if (m_poGDS->m_bNoDataSetAsInt64)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return GDALGetNoDataValueCastToDouble(m_poGDS->m_nNoDataValueInt64);
    }

    if (m_bNoDataSetAsUInt64)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return GDALGetNoDataValueCastToDouble(m_nNoDataValueUInt64);
    }

    if (m_poGDS->m_bNoDataSetAsUInt64)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return GDALGetNoDataValueCastToDouble(m_poGDS->m_nNoDataValueUInt64);
    }

    if (pbSuccess)
        *pbSuccess = FALSE;
    return dfNoDataValue;
}

/************************************************************************/
/*                       GetNoDataValueAsInt64()                        */
/************************************************************************/

int64_t GTiffRasterBand::GetNoDataValueAsInt64(int *pbSuccess)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (eDataType == GDT_UInt64)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "GetNoDataValueAsUInt64() should be called instead");
        if (pbSuccess)
            *pbSuccess = FALSE;
        return GDAL_PAM_DEFAULT_NODATA_VALUE_INT64;
    }
    if (eDataType != GDT_Int64)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "GetNoDataValue() should be called instead");
        if (pbSuccess)
            *pbSuccess = FALSE;
        return GDAL_PAM_DEFAULT_NODATA_VALUE_INT64;
    }

    int bSuccess = FALSE;
    const auto nNoDataValue =
        GDALPamRasterBand::GetNoDataValueAsInt64(&bSuccess);
    if (bSuccess)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return nNoDataValue;
    }

    if (m_bNoDataSetAsInt64)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return m_nNoDataValueInt64;
    }

    if (m_poGDS->m_bNoDataSetAsInt64)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return m_poGDS->m_nNoDataValueInt64;
    }

    if (pbSuccess)
        *pbSuccess = FALSE;
    return nNoDataValue;
}

/************************************************************************/
/*                      GetNoDataValueAsUInt64()                        */
/************************************************************************/

uint64_t GTiffRasterBand::GetNoDataValueAsUInt64(int *pbSuccess)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (eDataType == GDT_Int64)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "GetNoDataValueAsInt64() should be called instead");
        if (pbSuccess)
            *pbSuccess = FALSE;
        return GDAL_PAM_DEFAULT_NODATA_VALUE_UINT64;
    }
    if (eDataType != GDT_UInt64)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "GetNoDataValue() should be called instead");
        if (pbSuccess)
            *pbSuccess = FALSE;
        return GDAL_PAM_DEFAULT_NODATA_VALUE_UINT64;
    }

    int bSuccess = FALSE;
    const auto nNoDataValue =
        GDALPamRasterBand::GetNoDataValueAsUInt64(&bSuccess);
    if (bSuccess)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return nNoDataValue;
    }

    if (m_bNoDataSetAsUInt64)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return m_nNoDataValueUInt64;
    }

    if (m_poGDS->m_bNoDataSetAsUInt64)
    {
        if (pbSuccess)
            *pbSuccess = TRUE;

        return m_poGDS->m_nNoDataValueUInt64;
    }

    if (pbSuccess)
        *pbSuccess = FALSE;
    return nNoDataValue;
}

/************************************************************************/
/*                           SetNoDataValue()                           */
/************************************************************************/

CPLErr GTiffRasterBand::SetNoDataValue(double dfNoData)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (m_poGDS->m_bNoDataSet &&
        (m_poGDS->m_dfNoDataValue == dfNoData ||
         (std::isnan(m_poGDS->m_dfNoDataValue) && std::isnan(dfNoData))))
    {
        ResetNoDataValues(false);

        m_bNoDataSet = true;
        m_dfNoDataValue = dfNoData;

        return CE_None;
    }

    if (m_poGDS->nBands > 1 && m_poGDS->m_eProfile == GTiffProfile::GDALGEOTIFF)
    {
        int bOtherBandHasNoData = FALSE;
        const int nOtherBand = nBand > 1 ? 1 : 2;
        double dfOtherNoData = m_poGDS->GetRasterBand(nOtherBand)
                                   ->GetNoDataValue(&bOtherBandHasNoData);
        if (bOtherBandHasNoData && dfOtherNoData != dfNoData)
        {
            ReportError(
                CE_Warning, CPLE_AppDefined,
                "Setting nodata to %.18g on band %d, but band %d has nodata "
                "at %.18g. The TIFFTAG_GDAL_NODATA only support one value "
                "per dataset. This value of %.18g will be used for all bands "
                "on re-opening",
                dfNoData, nBand, nOtherBand, dfOtherNoData, dfNoData);
        }
    }

    if (m_poGDS->m_bStreamingOut && m_poGDS->m_bCrystalized)
    {
        ReportError(
            CE_Failure, CPLE_NotSupported,
            "Cannot modify nodata at that point in a streamed output file");
        return CE_Failure;
    }

    CPLErr eErr = CE_None;
    if (eAccess == GA_Update)
    {
        m_poGDS->m_bNoDataChanged = true;
        int bSuccess = FALSE;
        CPL_IGNORE_RET_VAL(GDALPamRasterBand::GetNoDataValue(&bSuccess));
        if (bSuccess)
        {
            // Cancel any existing nodata from PAM file.
            eErr = GDALPamRasterBand::DeleteNoDataValue();
        }
    }
    else
    {
        CPLDebug("GTIFF", "SetNoDataValue() goes to PAM instead of TIFF tags");
        eErr = GDALPamRasterBand::SetNoDataValue(dfNoData);
    }

    if (eErr == CE_None)
    {
        ResetNoDataValues(true);

        m_poGDS->m_bNoDataSet = true;
        m_poGDS->m_dfNoDataValue = dfNoData;

        m_bNoDataSet = true;
        m_dfNoDataValue = dfNoData;
    }

    return eErr;
}

/************************************************************************/
/*                       SetNoDataValueAsInt64()                        */
/************************************************************************/

CPLErr GTiffRasterBand::SetNoDataValueAsInt64(int64_t nNoData)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (m_poGDS->m_bNoDataSetAsInt64 && m_poGDS->m_nNoDataValueInt64 == nNoData)
    {
        ResetNoDataValues(false);

        m_bNoDataSetAsInt64 = true;
        m_nNoDataValueInt64 = nNoData;

        return CE_None;
    }

    if (m_poGDS->nBands > 1 && m_poGDS->m_eProfile == GTiffProfile::GDALGEOTIFF)
    {
        int bOtherBandHasNoData = FALSE;
        const int nOtherBand = nBand > 1 ? 1 : 2;
        const auto nOtherNoData =
            m_poGDS->GetRasterBand(nOtherBand)
                ->GetNoDataValueAsInt64(&bOtherBandHasNoData);
        if (bOtherBandHasNoData && nOtherNoData != nNoData)
        {
            ReportError(CE_Warning, CPLE_AppDefined,
                        "Setting nodata to " CPL_FRMT_GIB
                        " on band %d, but band %d has nodata "
                        "at " CPL_FRMT_GIB
                        ". The TIFFTAG_GDAL_NODATA only support one value "
                        "per dataset. This value of " CPL_FRMT_GIB
                        " will be used for all bands "
                        "on re-opening",
                        static_cast<GIntBig>(nNoData), nBand, nOtherBand,
                        static_cast<GIntBig>(nOtherNoData),
                        static_cast<GIntBig>(nNoData));
        }
    }

    if (m_poGDS->m_bStreamingOut && m_poGDS->m_bCrystalized)
    {
        ReportError(
            CE_Failure, CPLE_NotSupported,
            "Cannot modify nodata at that point in a streamed output file");
        return CE_Failure;
    }

    CPLErr eErr = CE_None;
    if (eAccess == GA_Update)
    {
        m_poGDS->m_bNoDataChanged = true;
        int bSuccess = FALSE;
        CPL_IGNORE_RET_VAL(GDALPamRasterBand::GetNoDataValueAsInt64(&bSuccess));
        if (bSuccess)
        {
            // Cancel any existing nodata from PAM file.
            eErr = GDALPamRasterBand::DeleteNoDataValue();
        }
    }
    else
    {
        CPLDebug("GTIFF", "SetNoDataValue() goes to PAM instead of TIFF tags");
        eErr = GDALPamRasterBand::SetNoDataValueAsInt64(nNoData);
    }

    if (eErr == CE_None)
    {
        ResetNoDataValues(true);

        m_poGDS->m_bNoDataSetAsInt64 = true;
        m_poGDS->m_nNoDataValueInt64 = nNoData;
    }

    return eErr;
}

/************************************************************************/
/*                      SetNoDataValueAsUInt64()                        */
/************************************************************************/

CPLErr GTiffRasterBand::SetNoDataValueAsUInt64(uint64_t nNoData)

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (m_poGDS->m_bNoDataSetAsUInt64 &&
        m_poGDS->m_nNoDataValueUInt64 == nNoData)
    {
        ResetNoDataValues(false);

        m_bNoDataSetAsUInt64 = true;
        m_nNoDataValueUInt64 = nNoData;

        return CE_None;
    }

    if (m_poGDS->nBands > 1 && m_poGDS->m_eProfile == GTiffProfile::GDALGEOTIFF)
    {
        int bOtherBandHasNoData = FALSE;
        const int nOtherBand = nBand > 1 ? 1 : 2;
        const auto nOtherNoData =
            m_poGDS->GetRasterBand(nOtherBand)
                ->GetNoDataValueAsUInt64(&bOtherBandHasNoData);
        if (bOtherBandHasNoData && nOtherNoData != nNoData)
        {
            ReportError(CE_Warning, CPLE_AppDefined,
                        "Setting nodata to " CPL_FRMT_GUIB
                        " on band %d, but band %d has nodata "
                        "at " CPL_FRMT_GUIB
                        ". The TIFFTAG_GDAL_NODATA only support one value "
                        "per dataset. This value of " CPL_FRMT_GUIB
                        " will be used for all bands "
                        "on re-opening",
                        static_cast<GUIntBig>(nNoData), nBand, nOtherBand,
                        static_cast<GUIntBig>(nOtherNoData),
                        static_cast<GUIntBig>(nNoData));
        }
    }

    if (m_poGDS->m_bStreamingOut && m_poGDS->m_bCrystalized)
    {
        ReportError(
            CE_Failure, CPLE_NotSupported,
            "Cannot modify nodata at that point in a streamed output file");
        return CE_Failure;
    }

    CPLErr eErr = CE_None;
    if (eAccess == GA_Update)
    {
        m_poGDS->m_bNoDataChanged = true;
        int bSuccess = FALSE;
        CPL_IGNORE_RET_VAL(
            GDALPamRasterBand::GetNoDataValueAsUInt64(&bSuccess));
        if (bSuccess)
        {
            // Cancel any existing nodata from PAM file.
            eErr = GDALPamRasterBand::DeleteNoDataValue();
        }
    }
    else
    {
        CPLDebug("GTIFF", "SetNoDataValue() goes to PAM instead of TIFF tags");
        eErr = GDALPamRasterBand::SetNoDataValueAsUInt64(nNoData);
    }

    if (eErr == CE_None)
    {
        ResetNoDataValues(true);

        m_poGDS->m_bNoDataSetAsUInt64 = true;
        m_poGDS->m_nNoDataValueUInt64 = nNoData;

        m_bNoDataSetAsUInt64 = true;
        m_nNoDataValueUInt64 = nNoData;
    }

    return eErr;
}

/************************************************************************/
/*                        ResetNoDataValues()                           */
/************************************************************************/

void GTiffRasterBand::ResetNoDataValues(bool bResetDatasetToo)
{
    if (bResetDatasetToo)
    {
        m_poGDS->m_bNoDataSet = false;
        m_poGDS->m_dfNoDataValue = DEFAULT_NODATA_VALUE;
    }

    m_bNoDataSet = false;
    m_dfNoDataValue = DEFAULT_NODATA_VALUE;

    if (bResetDatasetToo)
    {
        m_poGDS->m_bNoDataSetAsInt64 = false;
        m_poGDS->m_nNoDataValueInt64 = GDAL_PAM_DEFAULT_NODATA_VALUE_INT64;
    }

    m_bNoDataSetAsInt64 = false;
    m_nNoDataValueInt64 = GDAL_PAM_DEFAULT_NODATA_VALUE_INT64;

    if (bResetDatasetToo)
    {
        m_poGDS->m_bNoDataSetAsUInt64 = false;
        m_poGDS->m_nNoDataValueUInt64 = GDAL_PAM_DEFAULT_NODATA_VALUE_UINT64;
    }

    m_bNoDataSetAsUInt64 = false;
    m_nNoDataValueUInt64 = GDAL_PAM_DEFAULT_NODATA_VALUE_UINT64;
}

/************************************************************************/
/*                        DeleteNoDataValue()                           */
/************************************************************************/

CPLErr GTiffRasterBand::DeleteNoDataValue()

{
    m_poGDS->LoadGeoreferencingAndPamIfNeeded();

    if (m_poGDS->m_bStreamingOut && m_poGDS->m_bCrystalized)
    {
        ReportError(
            CE_Failure, CPLE_NotSupported,
            "Cannot modify nodata at that point in a streamed output file");
        return CE_Failure;
    }

    if (eAccess == GA_Update)
    {
        if (m_poGDS->m_bNoDataSet)
            m_poGDS->m_bNoDataChanged = true;
    }
    else
    {
        CPLDebug("GTIFF",
                 "DeleteNoDataValue() goes to PAM instead of TIFF tags");
    }

    CPLErr eErr = GDALPamRasterBand::DeleteNoDataValue();
    if (eErr == CE_None)
    {
        ResetNoDataValues(true);
    }

    return eErr;
}

/************************************************************************/
/*                             NullBlock()                              */
/*                                                                      */
/*      Set the block data to the null value if it is set, or zero      */
/*      if there is no null data value.                                 */
/************************************************************************/

void GTiffRasterBand::NullBlock(void *pData)

{
    const GPtrDiff_t nWords =
        static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize;
    const int nChunkSize = std::max(1, GDALGetDataTypeSizeBytes(eDataType));

    int l_bNoDataSet = FALSE;
    if (eDataType == GDT_Int64)
    {
        const auto nVal = GetNoDataValueAsInt64(&l_bNoDataSet);
        if (!l_bNoDataSet)
        {
            memset(pData, 0, nWords * nChunkSize);
        }
        else
        {
            GDALCopyWords64(&nVal, GDT_Int64, 0, pData, eDataType, nChunkSize,
                            nWords);
        }
    }
    else if (eDataType == GDT_UInt64)
    {
        const auto nVal = GetNoDataValueAsUInt64(&l_bNoDataSet);
        if (!l_bNoDataSet)
        {
            memset(pData, 0, nWords * nChunkSize);
        }
        else
        {
            GDALCopyWords64(&nVal, GDT_UInt64, 0, pData, eDataType, nChunkSize,
                            nWords);
        }
    }
    else
    {
        double dfNoData = GetNoDataValue(&l_bNoDataSet);
        if (!l_bNoDataSet)
        {
#ifdef ESRI_BUILD
            if (m_poGDS->m_nBitsPerSample >= 2)
                memset(pData, 0, nWords * nChunkSize);
            else
                memset(pData, 1, nWords * nChunkSize);
#else
            memset(pData, 0, nWords * nChunkSize);
#endif
        }
        else
        {
            // Hack for Signed Int8 case. As the data type is GDT_Byte
            // (unsigned), we have to convert a negative nodata value in the
            // range [-128,-1] in [128, 255]
            if (m_poGDS->m_nBitsPerSample == 8 &&
                m_poGDS->m_nSampleFormat == SAMPLEFORMAT_INT && dfNoData < 0 &&
                dfNoData >= -128 && static_cast<int>(dfNoData) == dfNoData)
            {
                dfNoData = 256 + dfNoData;
            }

            // Will convert nodata value to the right type and copy efficiently.
            GDALCopyWords64(&dfNoData, GDT_Float64, 0, pData, eDataType,
                            nChunkSize, nWords);
        }
    }
}

/************************************************************************/
/*                          GetOverviewCount()                          */
/************************************************************************/

int GTiffRasterBand::GetOverviewCount()

{
    if (!m_poGDS->AreOverviewsEnabled())
        return 0;

    m_poGDS->ScanDirectories();

    if (m_poGDS->m_nOverviewCount > 0)
    {
        return m_poGDS->m_nOverviewCount;
    }

    const int nOverviewCount = GDALRasterBand::GetOverviewCount();
    if (nOverviewCount > 0)
        return nOverviewCount;

    // Implicit JPEG overviews are normally hidden, except when doing
    // IRasterIO() operations.
    if (m_poGDS->m_nJPEGOverviewVisibilityCounter)
        return m_poGDS->GetJPEGOverviewCount();

    return 0;
}

/************************************************************************/
/*                            GetOverview()                             */
/************************************************************************/

GDALRasterBand *GTiffRasterBand::GetOverview(int i)

{
    m_poGDS->ScanDirectories();

    if (m_poGDS->m_nOverviewCount > 0)
    {
        // Do we have internal overviews?
        if (i < 0 || i >= m_poGDS->m_nOverviewCount)
            return nullptr;

        return m_poGDS->m_papoOverviewDS[i]->GetRasterBand(nBand);
    }

    GDALRasterBand *const poOvrBand = GDALRasterBand::GetOverview(i);
    if (poOvrBand != nullptr)
        return poOvrBand;

    // For consistency with GetOverviewCount(), we should also test
    // m_nJPEGOverviewVisibilityCounter, but it is also convenient to be able
    // to query them for testing purposes.
    if (i >= 0 && i < m_poGDS->GetJPEGOverviewCount())
        return m_poGDS->m_papoJPEGOverviewDS[i]->GetRasterBand(nBand);

    return nullptr;
}

/************************************************************************/
/*                           GetMaskFlags()                             */
/************************************************************************/

int GTiffRasterBand::GetMaskFlags()
{
    m_poGDS->ScanDirectories();

    if (m_poGDS->m_poExternalMaskDS != nullptr)
    {
        return GMF_PER_DATASET;
    }

    if (m_poGDS->m_poMaskDS != nullptr)
    {
        if (m_poGDS->m_poMaskDS->GetRasterCount() == 1)
        {
            return GMF_PER_DATASET;
        }

        return 0;
    }

    if (m_poGDS->m_bIsOverview)
    {
        return m_poGDS->m_poBaseDS->GetRasterBand(nBand)->GetMaskFlags();
    }

    return GDALPamRasterBand::GetMaskFlags();
}

/************************************************************************/
/*                            GetMaskBand()                             */
/************************************************************************/

GDALRasterBand *GTiffRasterBand::GetMaskBand()
{
    m_poGDS->ScanDirectories();

    if (m_poGDS->m_poExternalMaskDS != nullptr)
    {
        return m_poGDS->m_poExternalMaskDS->GetRasterBand(1);
    }

    if (m_poGDS->m_poMaskDS != nullptr)
    {
        if (m_poGDS->m_poMaskDS->GetRasterCount() == 1)
            return m_poGDS->m_poMaskDS->GetRasterBand(1);

        return m_poGDS->m_poMaskDS->GetRasterBand(nBand);
    }

    if (m_poGDS->m_bIsOverview)
    {
        GDALRasterBand *poBaseMask =
            m_poGDS->m_poBaseDS->GetRasterBand(nBand)->GetMaskBand();
        if (poBaseMask)
        {
            const int nOverviews = poBaseMask->GetOverviewCount();
            for (int i = 0; i < nOverviews; i++)
            {
                GDALRasterBand *poOvr = poBaseMask->GetOverview(i);
                if (poOvr && poOvr->GetXSize() == GetXSize() &&
                    poOvr->GetYSize() == GetYSize())
                {
                    return poOvr;
                }
            }
        }
    }

    return GDALPamRasterBand::GetMaskBand();
}

/************************************************************************/
/* ==================================================================== */
/*                             GTiffSplitBand                           */
/* ==================================================================== */
/************************************************************************/

class GTiffSplitBand final : public GTiffRasterBand
{
    friend class GTiffDataset;

  public:
    GTiffSplitBand(GTiffDataset *, int);
    virtual ~GTiffSplitBand()
    {
    }

    bool IsBaseGTiffClass() const override
    {
        return false;
    }

    virtual int IGetDataCoverageStatus(int nXOff, int nYOff, int nXSize,
                                       int nYSize, int nMaskFlagStop,
                                       double *pdfDataPct) override;

    virtual CPLErr IReadBlock(int, int, void *) override;
    virtual CPLErr IWriteBlock(int, int, void *) override;
};

/************************************************************************/
/*                           GTiffSplitBand()                           */
/************************************************************************/

GTiffSplitBand::GTiffSplitBand(GTiffDataset *poDSIn, int nBandIn)
    : GTiffRasterBand(poDSIn, nBandIn)
{
    nBlockXSize = poDS->GetRasterXSize();
    nBlockYSize = 1;
}

/************************************************************************/
/*                       IGetDataCoverageStatus()                       */
/************************************************************************/

int GTiffSplitBand::IGetDataCoverageStatus(int, int, int, int, int, double *)
{
    return GDAL_DATA_COVERAGE_STATUS_UNIMPLEMENTED |
           GDAL_DATA_COVERAGE_STATUS_DATA;
}

/************************************************************************/
/*                             IReadBlock()                             */
/************************************************************************/

CPLErr GTiffSplitBand::IReadBlock(int /* nBlockXOff */, int nBlockYOff,
                                  void *pImage)

{
    m_poGDS->Crystalize();

    // Optimization when reading the same line in a contig multi-band TIFF.
    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG &&
        m_poGDS->nBands > 1 && m_poGDS->m_nLoadedBlock == nBlockYOff)
    {
        goto extract_band_data;
    }

    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG && m_poGDS->nBands > 1)
    {
        if (m_poGDS->m_pabyBlockBuf == nullptr)
        {
            m_poGDS->m_pabyBlockBuf = static_cast<GByte *>(
                VSI_MALLOC_VERBOSE(TIFFScanlineSize(m_poGDS->m_hTIFF)));
            if (m_poGDS->m_pabyBlockBuf == nullptr)
            {
                return CE_Failure;
            }
        }
    }
    else
    {
        CPLAssert(TIFFScanlineSize(m_poGDS->m_hTIFF) == nBlockXSize);
    }

    /* -------------------------------------------------------------------- */
    /*      Read through to target scanline.                                */
    /* -------------------------------------------------------------------- */
    if (m_poGDS->m_nLoadedBlock >= nBlockYOff)
        m_poGDS->m_nLoadedBlock = -1;

    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE &&
        m_poGDS->nBands > 1)
    {
        // If we change of band, we must start reading the
        // new strip from its beginning.
        if (m_poGDS->m_nLastBandRead != nBand)
            m_poGDS->m_nLoadedBlock = -1;
        m_poGDS->m_nLastBandRead = nBand;
    }

    while (m_poGDS->m_nLoadedBlock < nBlockYOff)
    {
        ++m_poGDS->m_nLoadedBlock;
        if (TIFFReadScanline(m_poGDS->m_hTIFF,
                             m_poGDS->m_pabyBlockBuf ? m_poGDS->m_pabyBlockBuf
                                                     : pImage,
                             m_poGDS->m_nLoadedBlock,
                             (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
                                 ? static_cast<uint16_t>(nBand - 1)
                                 : 0) == -1 &&
            !m_poGDS->m_bIgnoreReadErrors)
        {
            ReportError(CE_Failure, CPLE_AppDefined,
                        "TIFFReadScanline() failed.");
            m_poGDS->m_nLoadedBlock = -1;
            return CE_Failure;
        }
    }

extract_band_data:
    /* -------------------------------------------------------------------- */
    /*      Extract band data from contig buffer.                           */
    /* -------------------------------------------------------------------- */
    if (m_poGDS->m_pabyBlockBuf != nullptr)
    {
        for (int iPixel = 0, iSrcOffset = nBand - 1, iDstOffset = 0;
             iPixel < nBlockXSize;
             ++iPixel, iSrcOffset += m_poGDS->nBands, ++iDstOffset)
        {
            static_cast<GByte *>(pImage)[iDstOffset] =
                m_poGDS->m_pabyBlockBuf[iSrcOffset];
        }
    }

    return CE_None;
}

/************************************************************************/
/*                            IWriteBlock()                             */
/************************************************************************/

CPLErr GTiffSplitBand::IWriteBlock(int /* nBlockXOff */, int /* nBlockYOff */,
                                   void * /* pImage */)

{
    ReportError(CE_Failure, CPLE_AppDefined, "Split bands are read-only.");
    return CE_Failure;
}

/************************************************************************/
/* ==================================================================== */
/*                             GTiffRGBABand                            */
/* ==================================================================== */
/************************************************************************/

class GTiffRGBABand final : public GTiffRasterBand
{
    friend class GTiffDataset;

  public:
    GTiffRGBABand(GTiffDataset *, int);
    virtual ~GTiffRGBABand()
    {
    }

    bool IsBaseGTiffClass() const override
    {
        return false;
    }

    virtual int IGetDataCoverageStatus(int nXOff, int nYOff, int nXSize,
                                       int nYSize, int nMaskFlagStop,
                                       double *pdfDataPct) override;

    virtual CPLErr IReadBlock(int, int, void *) override;
    virtual CPLErr IWriteBlock(int, int, void *) override;

    virtual GDALColorInterp GetColorInterpretation() override;
};

/************************************************************************/
/*                           GTiffRGBABand()                            */
/************************************************************************/

GTiffRGBABand::GTiffRGBABand(GTiffDataset *poDSIn, int nBandIn)
    : GTiffRasterBand(poDSIn, nBandIn)
{
    eDataType = GDT_Byte;
}

/************************************************************************/
/*                       IGetDataCoverageStatus()                       */
/************************************************************************/

int GTiffRGBABand::IGetDataCoverageStatus(int, int, int, int, int, double *)
{
    return GDAL_DATA_COVERAGE_STATUS_UNIMPLEMENTED |
           GDAL_DATA_COVERAGE_STATUS_DATA;
}

/************************************************************************/
/*                            IWriteBlock()                             */
/************************************************************************/

CPLErr GTiffRGBABand::IWriteBlock(int, int, void *)

{
    ReportError(CE_Failure, CPLE_AppDefined,
                "RGBA interpreted raster bands are read-only.");
    return CE_Failure;
}

/************************************************************************/
/*                             IReadBlock()                             */
/************************************************************************/

CPLErr GTiffRGBABand::IReadBlock(int nBlockXOff, int nBlockYOff, void *pImage)

{
    m_poGDS->Crystalize();

    CPLAssert(nBlocksPerRow != 0);
    const auto nBlockBufSize =
        4 * static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize;
    const int nBlockId = nBlockXOff + nBlockYOff * nBlocksPerRow;

    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
    {
        for (int iBand = 0; iBand < m_poGDS->m_nSamplesPerPixel; iBand++)
        {
            int nBlockIdBand = nBlockId + iBand * m_poGDS->m_nBlocksPerBand;
            if (!m_poGDS->IsBlockAvailable(nBlockIdBand))
                return CE_Failure;
        }
    }
    else
    {
        if (!m_poGDS->IsBlockAvailable(nBlockId))
            return CE_Failure;
    }

    /* -------------------------------------------------------------------- */
    /*      Allocate a temporary buffer for this strip.                     */
    /* -------------------------------------------------------------------- */
    if (m_poGDS->m_pabyBlockBuf == nullptr)
    {
        m_poGDS->m_pabyBlockBuf = static_cast<GByte *>(
            VSI_MALLOC3_VERBOSE(4, nBlockXSize, nBlockYSize));
        if (m_poGDS->m_pabyBlockBuf == nullptr)
            return CE_Failure;
    }

    /* -------------------------------------------------------------------- */
    /*      Read the strip                                                  */
    /* -------------------------------------------------------------------- */
    CPLErr eErr = CE_None;

    if (m_poGDS->m_nLoadedBlock != nBlockId)
    {
        if (TIFFIsTiled(m_poGDS->m_hTIFF))
        {
#if TIFFLIB_VERSION > 20161119
            if (TIFFReadRGBATileExt(
                    m_poGDS->m_hTIFF, nBlockXOff * nBlockXSize,
                    nBlockYOff * nBlockYSize,
                    reinterpret_cast<uint32_t *>(m_poGDS->m_pabyBlockBuf),
                    !m_poGDS->m_bIgnoreReadErrors) == 0 &&
                !m_poGDS->m_bIgnoreReadErrors)
#else
            if (TIFFReadRGBATile(m_poGDS->m_hTIFF, nBlockXOff * nBlockXSize,
                                 nBlockYOff * nBlockYSize,
                                 reinterpret_cast<uint32_t *>(
                                     m_poGDS->m_pabyBlockBuf)) == 0 &&
                !m_poGDS->m_bIgnoreReadErrors)
#endif
            {
                // Once TIFFError() is properly hooked, this can go away.
                ReportError(CE_Failure, CPLE_AppDefined,
                            "TIFFReadRGBATile() failed.");

                memset(m_poGDS->m_pabyBlockBuf, 0, nBlockBufSize);

                eErr = CE_Failure;
            }
        }
        else
        {
#if TIFFLIB_VERSION > 20161119
            if (TIFFReadRGBAStripExt(
                    m_poGDS->m_hTIFF, nBlockId * nBlockYSize,
                    reinterpret_cast<uint32_t *>(m_poGDS->m_pabyBlockBuf),
                    !m_poGDS->m_bIgnoreReadErrors) == 0 &&
                !m_poGDS->m_bIgnoreReadErrors)
#else
            if (TIFFReadRGBAStrip(m_poGDS->m_hTIFF, nBlockId * nBlockYSize,
                                  reinterpret_cast<uint32_t *>(
                                      m_poGDS->m_pabyBlockBuf)) == 0 &&
                !m_poGDS->m_bIgnoreReadErrors)
#endif
            {
                // Once TIFFError() is properly hooked, this can go away.
                ReportError(CE_Failure, CPLE_AppDefined,
                            "TIFFReadRGBAStrip() failed.");

                memset(m_poGDS->m_pabyBlockBuf, 0, nBlockBufSize);

                eErr = CE_Failure;
            }
        }
    }

    m_poGDS->m_nLoadedBlock = eErr == CE_None ? nBlockId : -1;

    /* -------------------------------------------------------------------- */
    /*      Handle simple case of eight bit data, and pixel interleaving.   */
    /* -------------------------------------------------------------------- */
    int nThisBlockYSize = nBlockYSize;

    if (nBlockYOff * nBlockYSize > GetYSize() - nBlockYSize &&
        !TIFFIsTiled(m_poGDS->m_hTIFF))
        nThisBlockYSize = GetYSize() - nBlockYOff * nBlockYSize;

#ifdef CPL_LSB
    const int nBO = nBand - 1;
#else
    const int nBO = 4 - nBand;
#endif

    for (int iDestLine = 0; iDestLine < nThisBlockYSize; ++iDestLine)
    {
        const auto nSrcOffset =
            static_cast<GPtrDiff_t>(nThisBlockYSize - iDestLine - 1) *
            nBlockXSize * 4;

        GDALCopyWords(m_poGDS->m_pabyBlockBuf + nBO + nSrcOffset, GDT_Byte, 4,
                      static_cast<GByte *>(pImage) +
                          static_cast<GPtrDiff_t>(iDestLine) * nBlockXSize,
                      GDT_Byte, 1, nBlockXSize);
    }

    if (eErr == CE_None)
        eErr = FillCacheForOtherBands(nBlockXOff, nBlockYOff);

    return eErr;
}

/************************************************************************/
/*                       GetColorInterpretation()                       */
/************************************************************************/

GDALColorInterp GTiffRGBABand::GetColorInterpretation()

{
    if (nBand == 1)
        return GCI_RedBand;
    if (nBand == 2)
        return GCI_GreenBand;
    if (nBand == 3)
        return GCI_BlueBand;

    return GCI_AlphaBand;
}

/************************************************************************/
/* ==================================================================== */
/*                             GTiffOddBitsBand                         */
/* ==================================================================== */
/************************************************************************/

class GTiffOddBitsBand CPL_NON_FINAL : public GTiffRasterBand
{
    friend class GTiffDataset;

  public:
    GTiffOddBitsBand(GTiffDataset *, int);
    virtual ~GTiffOddBitsBand()
    {
    }

    bool IsBaseGTiffClass() const override
    {
        return false;
    }

    virtual CPLErr IReadBlock(int, int, void *) override;
    virtual CPLErr IWriteBlock(int, int, void *) override;
};

/************************************************************************/
/*                           GTiffOddBitsBand()                         */
/************************************************************************/

GTiffOddBitsBand::GTiffOddBitsBand(GTiffDataset *m_poGDSIn, int nBandIn)
    : GTiffRasterBand(m_poGDSIn, nBandIn)

{
    eDataType = GDT_Unknown;
    if ((m_poGDS->m_nBitsPerSample == 16 || m_poGDS->m_nBitsPerSample == 24) &&
        m_poGDS->m_nSampleFormat == SAMPLEFORMAT_IEEEFP)
        eDataType = GDT_Float32;
    // FIXME ? in autotest we currently open gcore/data/int24.tif
    // which is declared as signed, but we consider it as unsigned
    else if ((m_poGDS->m_nSampleFormat == SAMPLEFORMAT_UINT ||
              m_poGDS->m_nSampleFormat == SAMPLEFORMAT_INT) &&
             m_poGDS->m_nBitsPerSample < 8)
        eDataType = GDT_Byte;
    else if ((m_poGDS->m_nSampleFormat == SAMPLEFORMAT_UINT ||
              m_poGDS->m_nSampleFormat == SAMPLEFORMAT_INT) &&
             m_poGDS->m_nBitsPerSample > 8 && m_poGDS->m_nBitsPerSample < 16)
        eDataType = GDT_UInt16;
    else if ((m_poGDS->m_nSampleFormat == SAMPLEFORMAT_UINT ||
              m_poGDS->m_nSampleFormat == SAMPLEFORMAT_INT) &&
             m_poGDS->m_nBitsPerSample > 16 && m_poGDS->m_nBitsPerSample < 32)
        eDataType = GDT_UInt32;
}

/************************************************************************/
/*                            IWriteBlock()                             */
/************************************************************************/

CPLErr GTiffOddBitsBand::IWriteBlock(int nBlockXOff, int nBlockYOff,
                                     void *pImage)

{
    m_poGDS->Crystalize();

    if (m_poGDS->m_bWriteError)
    {
        // Report as an error if a previously loaded block couldn't be written
        // correctly.
        return CE_Failure;
    }

    if (eDataType == GDT_Float32 && m_poGDS->m_nBitsPerSample != 16)
    {
        ReportError(
            CE_Failure, CPLE_NotSupported,
            "Writing float data with nBitsPerSample = %d is unsupported",
            m_poGDS->m_nBitsPerSample);
        return CE_Failure;
    }

    /* -------------------------------------------------------------------- */
    /*      Load the block buffer.                                          */
    /* -------------------------------------------------------------------- */
    CPLAssert(nBlocksPerRow != 0);
    int nBlockId = nBlockXOff + nBlockYOff * nBlocksPerRow;

    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
        nBlockId += (nBand - 1) * m_poGDS->m_nBlocksPerBand;

    // Only read content from disk in the CONTIG case.
    {
        const CPLErr eErr = m_poGDS->LoadBlockBuf(
            nBlockId, m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG &&
                          m_poGDS->nBands > 1);
        if (eErr != CE_None)
            return eErr;
    }

    const GUInt32 nMaxVal = (1U << m_poGDS->m_nBitsPerSample) - 1;

    /* -------------------------------------------------------------------- */
    /*      Handle case of "separate" images or single band images where    */
    /*      no interleaving with other data is required.                    */
    /* -------------------------------------------------------------------- */
    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE ||
        m_poGDS->nBands == 1)
    {
        // TODO(schwehr): Create a CplNumBits8Aligned.
        // Bits per line rounds up to next byte boundary.
        GInt64 nBitsPerLine =
            static_cast<GInt64>(nBlockXSize) * m_poGDS->m_nBitsPerSample;
        if ((nBitsPerLine & 7) != 0)
            nBitsPerLine = (nBitsPerLine + 7) & (~7);

        GPtrDiff_t iPixel = 0;

        // Small optimization in 1 bit case.
        if (m_poGDS->m_nBitsPerSample == 1)
        {
            for (int iY = 0; iY < nBlockYSize; ++iY, iPixel += nBlockXSize)
            {
                GInt64 iBitOffset = iY * nBitsPerLine;

                const GByte *pabySrc =
                    static_cast<const GByte *>(pImage) + iPixel;
                auto iByteOffset = iBitOffset / 8;
                int iX = 0;  // Used after for.
                for (; iX + 7 < nBlockXSize; iX += 8, iByteOffset++)
                {
                    int nRes = (!(!pabySrc[iX + 0])) << 7;
                    nRes |= (!(!pabySrc[iX + 1])) << 6;
                    nRes |= (!(!pabySrc[iX + 2])) << 5;
                    nRes |= (!(!pabySrc[iX + 3])) << 4;
                    nRes |= (!(!pabySrc[iX + 4])) << 3;
                    nRes |= (!(!pabySrc[iX + 5])) << 2;
                    nRes |= (!(!pabySrc[iX + 6])) << 1;
                    nRes |= (!(!pabySrc[iX + 7])) << 0;
                    m_poGDS->m_pabyBlockBuf[iByteOffset] =
                        static_cast<GByte>(nRes);
                }
                iBitOffset = iByteOffset * 8;
                if (iX < nBlockXSize)
                {
                    int nRes = 0;
                    for (; iX < nBlockXSize; ++iX)
                    {
                        if (pabySrc[iX])
                            nRes |= (0x80 >> (iBitOffset & 7));
                        ++iBitOffset;
                    }
                    m_poGDS->m_pabyBlockBuf[iBitOffset >> 3] =
                        static_cast<GByte>(nRes);
                }
            }

            m_poGDS->m_bLoadedBlockDirty = true;

            return CE_None;
        }

        if (eDataType == GDT_Float32 && m_poGDS->m_nBitsPerSample == 16)
        {
            for (; iPixel < static_cast<GPtrDiff_t>(nBlockYSize) * nBlockXSize;
                 iPixel++)
            {
                GUInt32 nInWord = static_cast<GUInt32 *>(pImage)[iPixel];
                bool bClipWarn = m_poGDS->m_bClipWarn;
                GUInt16 nHalf = CPLFloatToHalf(nInWord, bClipWarn);
                m_poGDS->m_bClipWarn = bClipWarn;
                reinterpret_cast<GUInt16 *>(m_poGDS->m_pabyBlockBuf)[iPixel] =
                    nHalf;
            }

            m_poGDS->m_bLoadedBlockDirty = true;

            return CE_None;
        }

        // Initialize to zero as we set the buffer with binary or operations.
        if (m_poGDS->m_nBitsPerSample != 24)
            memset(m_poGDS->m_pabyBlockBuf, 0,
                   static_cast<size_t>((nBitsPerLine / 8) * nBlockYSize));

        for (int iY = 0; iY < nBlockYSize; ++iY)
        {
            GInt64 iBitOffset = iY * nBitsPerLine;

            if (m_poGDS->m_nBitsPerSample == 12)
            {
                for (int iX = 0; iX < nBlockXSize; ++iX)
                {
                    GUInt32 nInWord = static_cast<GUInt16 *>(pImage)[iPixel++];
                    if (nInWord > nMaxVal)
                    {
                        nInWord = nMaxVal;
                        if (!m_poGDS->m_bClipWarn)
                        {
                            m_poGDS->m_bClipWarn = true;
                            ReportError(
                                CE_Warning, CPLE_AppDefined,
                                "One or more pixels clipped to fit %d bit "
                                "domain.",
                                m_poGDS->m_nBitsPerSample);
                        }
                    }

                    if ((iBitOffset % 8) == 0)
                    {
                        m_poGDS->m_pabyBlockBuf[iBitOffset >> 3] =
                            static_cast<GByte>(nInWord >> 4);
                        // Let 4 lower bits to zero as they're going to be
                        // overridden by the next word.
                        m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 1] =
                            static_cast<GByte>((nInWord & 0xf) << 4);
                    }
                    else
                    {
                        // Must or to preserve the 4 upper bits written
                        // for the previous word.
                        m_poGDS->m_pabyBlockBuf[iBitOffset >> 3] |=
                            static_cast<GByte>(nInWord >> 8);
                        m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 1] =
                            static_cast<GByte>(nInWord & 0xff);
                    }

                    iBitOffset += m_poGDS->m_nBitsPerSample;
                }
                continue;
            }

            for (int iX = 0; iX < nBlockXSize; ++iX)
            {
                GUInt32 nInWord = 0;
                if (eDataType == GDT_Byte)
                {
                    nInWord = static_cast<GByte *>(pImage)[iPixel++];
                }
                else if (eDataType == GDT_UInt16)
                {
                    nInWord = static_cast<GUInt16 *>(pImage)[iPixel++];
                }
                else if (eDataType == GDT_UInt32)
                {
                    nInWord = static_cast<GUInt32 *>(pImage)[iPixel++];
                }
                else
                {
                    CPLAssert(false);
                }

                if (nInWord > nMaxVal)
                {
                    nInWord = nMaxVal;
                    if (!m_poGDS->m_bClipWarn)
                    {
                        m_poGDS->m_bClipWarn = true;
                        ReportError(
                            CE_Warning, CPLE_AppDefined,
                            "One or more pixels clipped to fit %d bit domain.",
                            m_poGDS->m_nBitsPerSample);
                    }
                }

                if (m_poGDS->m_nBitsPerSample == 24)
                {
/* -------------------------------------------------------------------- */
/*      Special case for 24bit data which is pre-byteswapped since      */
/*      the size falls on a byte boundary ... ugh (#2361).              */
/* -------------------------------------------------------------------- */
#ifdef CPL_MSB
                    m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 0] =
                        static_cast<GByte>(nInWord);
                    m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 1] =
                        static_cast<GByte>(nInWord >> 8);
                    m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 2] =
                        static_cast<GByte>(nInWord >> 16);
#else
                    m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 0] =
                        static_cast<GByte>(nInWord >> 16);
                    m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 1] =
                        static_cast<GByte>(nInWord >> 8);
                    m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 2] =
                        static_cast<GByte>(nInWord);
#endif
                    iBitOffset += 24;
                }
                else
                {
                    for (int iBit = 0; iBit < m_poGDS->m_nBitsPerSample; ++iBit)
                    {
                        if (nInWord &
                            (1 << (m_poGDS->m_nBitsPerSample - 1 - iBit)))
                            m_poGDS->m_pabyBlockBuf[iBitOffset >> 3] |=
                                (0x80 >> (iBitOffset & 7));
                        ++iBitOffset;
                    }
                }
            }
        }

        m_poGDS->m_bLoadedBlockDirty = true;

        return CE_None;
    }

    /* -------------------------------------------------------------------- */
    /*      Handle case of pixel interleaved (PLANARCONFIG_CONTIG) images.  */
    /* -------------------------------------------------------------------- */

    /* -------------------------------------------------------------------- */
    /*      On write of pixel interleaved data, we might as well flush      */
    /*      out any other bands that are dirty in our cache.  This is       */
    /*      especially helpful when writing compressed blocks.              */
    /* -------------------------------------------------------------------- */
    for (int iBand = 0; iBand < m_poGDS->nBands; ++iBand)
    {
        const GByte *pabyThisImage = nullptr;
        GDALRasterBlock *poBlock = nullptr;

        if (iBand + 1 == nBand)
        {
            pabyThisImage = static_cast<GByte *>(pImage);
        }
        else
        {
            poBlock = cpl::down_cast<GTiffOddBitsBand *>(
                          m_poGDS->GetRasterBand(iBand + 1))
                          ->TryGetLockedBlockRef(nBlockXOff, nBlockYOff);

            if (poBlock == nullptr)
                continue;

            if (!poBlock->GetDirty())
            {
                poBlock->DropLock();
                continue;
            }

            pabyThisImage = static_cast<GByte *>(poBlock->GetDataRef());
        }

        const int iPixelBitSkip = m_poGDS->m_nBitsPerSample * m_poGDS->nBands;
        const int iBandBitOffset = iBand * m_poGDS->m_nBitsPerSample;

        // Bits per line rounds up to next byte boundary.
        GInt64 nBitsPerLine = static_cast<GInt64>(nBlockXSize) * iPixelBitSkip;
        if ((nBitsPerLine & 7) != 0)
            nBitsPerLine = (nBitsPerLine + 7) & (~7);

        GPtrDiff_t iPixel = 0;

        if (eDataType == GDT_Float32 && m_poGDS->m_nBitsPerSample == 16)
        {
            for (; iPixel < static_cast<GPtrDiff_t>(nBlockYSize) * nBlockXSize;
                 iPixel++)
            {
                GUInt32 nInWord =
                    reinterpret_cast<const GUInt32 *>(pabyThisImage)[iPixel];
                bool bClipWarn = m_poGDS->m_bClipWarn;
                GUInt16 nHalf = CPLFloatToHalf(nInWord, bClipWarn);
                m_poGDS->m_bClipWarn = bClipWarn;
                reinterpret_cast<GUInt16 *>(
                    m_poGDS->m_pabyBlockBuf)[iPixel * m_poGDS->nBands + iBand] =
                    nHalf;
            }

            if (poBlock != nullptr)
            {
                poBlock->MarkClean();
                poBlock->DropLock();
            }
            continue;
        }

        for (int iY = 0; iY < nBlockYSize; ++iY)
        {
            GInt64 iBitOffset = iBandBitOffset + iY * nBitsPerLine;

            if (m_poGDS->m_nBitsPerSample == 12)
            {
                for (int iX = 0; iX < nBlockXSize; ++iX)
                {
                    GUInt32 nInWord = reinterpret_cast<const GUInt16 *>(
                        pabyThisImage)[iPixel++];
                    if (nInWord > nMaxVal)
                    {
                        nInWord = nMaxVal;
                        if (!m_poGDS->m_bClipWarn)
                        {
                            m_poGDS->m_bClipWarn = true;
                            ReportError(
                                CE_Warning, CPLE_AppDefined,
                                "One or more pixels clipped to fit %d bit "
                                "domain.",
                                m_poGDS->m_nBitsPerSample);
                        }
                    }

                    if ((iBitOffset % 8) == 0)
                    {
                        m_poGDS->m_pabyBlockBuf[iBitOffset >> 3] =
                            static_cast<GByte>(nInWord >> 4);
                        m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 1] =
                            static_cast<GByte>(
                                ((nInWord & 0xf) << 4) |
                                (m_poGDS
                                     ->m_pabyBlockBuf[(iBitOffset >> 3) + 1] &
                                 0xf));
                    }
                    else
                    {
                        m_poGDS->m_pabyBlockBuf[iBitOffset >> 3] =
                            static_cast<GByte>(
                                (m_poGDS->m_pabyBlockBuf[iBitOffset >> 3] &
                                 0xf0) |
                                (nInWord >> 8));
                        m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 1] =
                            static_cast<GByte>(nInWord & 0xff);
                    }

                    iBitOffset += iPixelBitSkip;
                }
                continue;
            }

            for (int iX = 0; iX < nBlockXSize; ++iX)
            {
                GUInt32 nInWord = 0;
                if (eDataType == GDT_Byte)
                {
                    nInWord =
                        static_cast<const GByte *>(pabyThisImage)[iPixel++];
                }
                else if (eDataType == GDT_UInt16)
                {
                    nInWord = reinterpret_cast<const GUInt16 *>(
                        pabyThisImage)[iPixel++];
                }
                else if (eDataType == GDT_UInt32)
                {
                    nInWord = reinterpret_cast<const GUInt32 *>(
                        pabyThisImage)[iPixel++];
                }
                else
                {
                    CPLAssert(false);
                }

                if (nInWord > nMaxVal)
                {
                    nInWord = nMaxVal;
                    if (!m_poGDS->m_bClipWarn)
                    {
                        m_poGDS->m_bClipWarn = true;
                        ReportError(
                            CE_Warning, CPLE_AppDefined,
                            "One or more pixels clipped to fit %d bit domain.",
                            m_poGDS->m_nBitsPerSample);
                    }
                }

                if (m_poGDS->m_nBitsPerSample == 24)
                {
/* -------------------------------------------------------------------- */
/*      Special case for 24bit data which is pre-byteswapped since      */
/*      the size falls on a byte boundary ... ugh (#2361).              */
/* -------------------------------------------------------------------- */
#ifdef CPL_MSB
                    m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 0] =
                        static_cast<GByte>(nInWord);
                    m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 1] =
                        static_cast<GByte>(nInWord >> 8);
                    m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 2] =
                        static_cast<GByte>(nInWord >> 16);
#else
                    m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 0] =
                        static_cast<GByte>(nInWord >> 16);
                    m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 1] =
                        static_cast<GByte>(nInWord >> 8);
                    m_poGDS->m_pabyBlockBuf[(iBitOffset >> 3) + 2] =
                        static_cast<GByte>(nInWord);
#endif
                    iBitOffset += 24;
                }
                else
                {
                    for (int iBit = 0; iBit < m_poGDS->m_nBitsPerSample; ++iBit)
                    {
                        // TODO(schwehr): Revisit this block.
                        if (nInWord &
                            (1 << (m_poGDS->m_nBitsPerSample - 1 - iBit)))
                        {
                            m_poGDS->m_pabyBlockBuf[iBitOffset >> 3] |=
                                (0x80 >> (iBitOffset & 7));
                        }
                        else
                        {
                            // We must explicitly unset the bit as we
                            // may update an existing block.
                            m_poGDS->m_pabyBlockBuf[iBitOffset >> 3] &=
                                ~(0x80 >> (iBitOffset & 7));
                        }

                        ++iBitOffset;
                    }
                }

                iBitOffset =
                    iBitOffset + iPixelBitSkip - m_poGDS->m_nBitsPerSample;
            }
        }

        if (poBlock != nullptr)
        {
            poBlock->MarkClean();
            poBlock->DropLock();
        }
    }

    m_poGDS->m_bLoadedBlockDirty = true;

    return CE_None;
}

/************************************************************************/
/*                             IReadBlock()                             */
/************************************************************************/

static void ExpandPacked8ToByte1(const GByte *const CPL_RESTRICT pabySrc,
                                 GByte *const CPL_RESTRICT pabyDest,
                                 GPtrDiff_t nBytes)
{
    for (decltype(nBytes) i = 0, j = 0; i < nBytes; i++, j += 8)
    {
        const GByte byVal = pabySrc[i];
        pabyDest[j + 0] = (byVal >> 7) & 0x1;
        pabyDest[j + 1] = (byVal >> 6) & 0x1;
        pabyDest[j + 2] = (byVal >> 5) & 0x1;
        pabyDest[j + 3] = (byVal >> 4) & 0x1;
        pabyDest[j + 4] = (byVal >> 3) & 0x1;
        pabyDest[j + 5] = (byVal >> 2) & 0x1;
        pabyDest[j + 6] = (byVal >> 1) & 0x1;
        pabyDest[j + 7] = (byVal >> 0) & 0x1;
    }
}

#if defined(__GNUC__) || defined(_MSC_VER)
// Signedness of char implementation dependent, so be explicit.
// Assumes 2-complement integer types and sign extension of right shifting
// GCC guarantees such:
// https://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html#Integers-implementation
static inline GByte ExtractBitAndConvertTo255(GByte byVal, int nBit)
{
    return static_cast<GByte>(static_cast<signed char>(byVal << (7 - nBit)) >>
                              7);
}
#else
// Portable way
static inline GByte ExtractBitAndConvertTo255(GByte byVal, int nBit)
{
    return (byVal & (1 << nBit)) ? 255 : 0;
}
#endif

static void ExpandPacked8ToByte255(const GByte *const CPL_RESTRICT pabySrc,
                                   GByte *const CPL_RESTRICT pabyDest,
                                   GPtrDiff_t nBytes)
{
    for (decltype(nBytes) i = 0, j = 0; i < nBytes; i++, j += 8)
    {
        const GByte byVal = pabySrc[i];
        pabyDest[j + 0] = ExtractBitAndConvertTo255(byVal, 7);
        pabyDest[j + 1] = ExtractBitAndConvertTo255(byVal, 6);
        pabyDest[j + 2] = ExtractBitAndConvertTo255(byVal, 5);
        pabyDest[j + 3] = ExtractBitAndConvertTo255(byVal, 4);
        pabyDest[j + 4] = ExtractBitAndConvertTo255(byVal, 3);
        pabyDest[j + 5] = ExtractBitAndConvertTo255(byVal, 2);
        pabyDest[j + 6] = ExtractBitAndConvertTo255(byVal, 1);
        pabyDest[j + 7] = ExtractBitAndConvertTo255(byVal, 0);
    }
}

CPLErr GTiffOddBitsBand::IReadBlock(int nBlockXOff, int nBlockYOff,
                                    void *pImage)

{
    m_poGDS->Crystalize();

    CPLAssert(nBlocksPerRow != 0);
    int nBlockId = nBlockXOff + nBlockYOff * nBlocksPerRow;

    if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
        nBlockId += (nBand - 1) * m_poGDS->m_nBlocksPerBand;

    /* -------------------------------------------------------------------- */
    /*      Handle the case of a strip in a writable file that doesn't      */
    /*      exist yet, but that we want to read.  Just set to zeros and     */
    /*      return.                                                         */
    /* -------------------------------------------------------------------- */
    if (nBlockId != m_poGDS->m_nLoadedBlock)
    {
        bool bErrOccurred = false;
        if (!m_poGDS->IsBlockAvailable(nBlockId, nullptr, nullptr,
                                       &bErrOccurred))
        {
            NullBlock(pImage);
            if (bErrOccurred)
                return CE_Failure;
            return CE_None;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Load the block buffer.                                          */
    /* -------------------------------------------------------------------- */
    {
        const CPLErr eErr = m_poGDS->LoadBlockBuf(nBlockId);
        if (eErr != CE_None)
            return eErr;
    }

    if (m_poGDS->m_nBitsPerSample == 1 &&
        (m_poGDS->nBands == 1 ||
         m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE))
    {
        /* --------------------------------------------------------------------
         */
        /*      Translate 1bit data to eight bit. */
        /* --------------------------------------------------------------------
         */
        GPtrDiff_t iDstOffset = 0;
        const GByte *const CPL_RESTRICT m_pabyBlockBuf =
            m_poGDS->m_pabyBlockBuf;
        GByte *CPL_RESTRICT pabyDest = static_cast<GByte *>(pImage);

        for (int iLine = 0; iLine < nBlockYSize; ++iLine)
        {
            GPtrDiff_t iSrcOffsetByte =
                static_cast<GPtrDiff_t>((nBlockXSize + 7) >> 3) * iLine;

            if (!m_poGDS->m_bPromoteTo8Bits)
            {
                ExpandPacked8ToByte1(m_pabyBlockBuf + iSrcOffsetByte,
                                     pabyDest + iDstOffset, nBlockXSize / 8);
            }
            else
            {
                ExpandPacked8ToByte255(m_pabyBlockBuf + iSrcOffsetByte,
                                       pabyDest + iDstOffset, nBlockXSize / 8);
            }
            GPtrDiff_t iSrcOffsetBit = (iSrcOffsetByte + nBlockXSize / 8) * 8;
            iDstOffset += nBlockXSize & ~0x7;
            const GByte bSetVal = m_poGDS->m_bPromoteTo8Bits ? 255 : 1;
            for (int iPixel = nBlockXSize & ~0x7; iPixel < nBlockXSize;
                 ++iPixel, ++iSrcOffsetBit)
            {
                if (m_pabyBlockBuf[iSrcOffsetBit >> 3] &
                    (0x80 >> (iSrcOffsetBit & 0x7)))
                    static_cast<GByte *>(pImage)[iDstOffset++] = bSetVal;
                else
                    static_cast<GByte *>(pImage)[iDstOffset++] = 0;
            }
        }
    }
    /* -------------------------------------------------------------------- */
    /*      Handle the case of 16- and 24-bit floating point data as per    */
    /*      TIFF Technical Note 3.                                          */
    /* -------------------------------------------------------------------- */
    else if (eDataType == GDT_Float32)
    {
        const int nWordBytes = m_poGDS->m_nBitsPerSample / 8;
        const GByte *pabyImage =
            m_poGDS->m_pabyBlockBuf +
            ((m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
                 ? 0
                 : (nBand - 1) * nWordBytes);
        const int iSkipBytes =
            (m_poGDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
                ? nWordBytes
                : m_poGDS->nBands * nWordBytes;

        const auto nBlockPixels =
            static_cast<GPtrDiff_t>(nBlockXSize) * nBlockYSize;
        if (m_poGDS->m_nBitsPerSample == 16)
        {
            for (GPtrDiff_t i = 0; i < nBlockPixels; ++i)
            {
                static_cast<GUInt32 *>(pImage)[i] = CPLHalfToFloat(
                    *reinterpret_cast<const GUInt16 *>(pabyImage));
                pabyImage += iSkipBytes;
            }
        }
        else if (m_poGDS->m_nBitsPerSample == 24)
        {
            for (GPtrDiff_t i = 0; i < nBlockPixels; ++i)
            {
#ifdef CPL_MSB
                static_cast<GUInt32 *>(pImage)[i] = CPLTripleToFloat(
                    (static_cast<GUInt32>(*(pabyImage + 0)) << 16) |
                    (static_cast<GUInt32>(*(pabyImage + 1)) << 8) |
                    static_cast<GUInt32>(*(pabyImage + 2)));
#else
                static_cast<GUInt32 *>(pImage)[i] = CPLTripleToFloat(
                    (static_cast<GUInt32>(*(pabyImage + 2)) << 16) |
                    (static_cast<GUInt32>(*(pabyImage + 1)) << 8) |
                    static_cast<GUInt32>(*pabyImage));
#endif
                pabyImage += iSkipBytes;
            }
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Special case for moving 12bit data somewhat more efficiently.   */
    /* -------------------------------------------------------------------- */
    else if (m_poGDS->m_nBitsPerSample == 12)
    {
        int iPixelBitSkip = 0;
        int iBandBitOffset = 0;

        if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG)
        {
            iPixelBitSkip = m_poGDS->nBands * m_poGDS->m_nBitsPerSample;
            iBandBitOffset = (nBand - 1) * m_poGDS->m_nBitsPerSample;
        }
        else
        {
            iPixelBitSkip = m_poGDS->m_nBitsPerSample;
        }

        // Bits per line rounds up to next byte boundary.
        GPtrDiff_t nBitsPerLine =
            static_cast<GPtrDiff_t>(nBlockXSize) * iPixelBitSkip;
        if ((nBitsPerLine & 7) != 0)
            nBitsPerLine = (nBitsPerLine + 7) & (~7);

        int iPixel = 0;
        for (int iY = 0; iY < nBlockYSize; ++iY)
        {
            GPtrDiff_t iBitOffset = iBandBitOffset + iY * nBitsPerLine;

            for (int iX = 0; iX < nBlockXSize; ++iX)
            {
                const auto iByte = iBitOffset >> 3;

                if ((iBitOffset & 0x7) == 0)
                {
                    // Starting on byte boundary.

                    static_cast<GUInt16 *>(pImage)[iPixel++] =
                        (m_poGDS->m_pabyBlockBuf[iByte] << 4) |
                        (m_poGDS->m_pabyBlockBuf[iByte + 1] >> 4);
                }
                else
                {
                    // Starting off byte boundary.

                    static_cast<GUInt16 *>(pImage)[iPixel++] =
                        ((m_poGDS->m_pabyBlockBuf[iByte] & 0xf) << 8) |
                        (m_poGDS->m_pabyBlockBuf[iByte + 1]);
                }
                iBitOffset += iPixelBitSkip;
            }
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Special case for 24bit data which is pre-byteswapped since      */
    /*      the size falls on a byte boundary ... ugh (#2361).              */
    /* -------------------------------------------------------------------- */
    else if (m_poGDS->m_nBitsPerSample == 24)
    {
        int iPixelByteSkip = 0;
        int iBandByteOffset = 0;

        if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG)
        {
            iPixelByteSkip = (m_poGDS->nBands * m_poGDS->m_nBitsPerSample) / 8;
            iBandByteOffset = ((nBand - 1) * m_poGDS->m_nBitsPerSample) / 8;
        }
        else
        {
            iPixelByteSkip = m_poGDS->m_nBitsPerSample / 8;
        }

        const GPtrDiff_t nBytesPerLine =
            static_cast<GPtrDiff_t>(nBlockXSize) * iPixelByteSkip;

        GPtrDiff_t iPixel = 0;
        for (int iY = 0; iY < nBlockYSize; ++iY)
        {
            GByte *pabyImage =
                m_poGDS->m_pabyBlockBuf + iBandByteOffset + iY * nBytesPerLine;

            for (int iX = 0; iX < nBlockXSize; ++iX)
            {
#ifdef CPL_MSB
                static_cast<GUInt32 *>(pImage)[iPixel++] =
                    (static_cast<GUInt32>(*(pabyImage + 2)) << 16) |
                    (static_cast<GUInt32>(*(pabyImage + 1)) << 8) |
                    static_cast<GUInt32>(*(pabyImage + 0));
#else
                static_cast<GUInt32 *>(pImage)[iPixel++] =
                    (static_cast<GUInt32>(*(pabyImage + 0)) << 16) |
                    (static_cast<GUInt32>(*(pabyImage + 1)) << 8) |
                    static_cast<GUInt32>(*(pabyImage + 2));
#endif
                pabyImage += iPixelByteSkip;
            }
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Handle 1-32 bit integer data.                                   */
    /* -------------------------------------------------------------------- */
    else
    {
        unsigned iPixelBitSkip = 0;
        unsigned iBandBitOffset = 0;

        if (m_poGDS->m_nPlanarConfig == PLANARCONFIG_CONTIG)
        {
            iPixelBitSkip = m_poGDS->nBands * m_poGDS->m_nBitsPerSample;
            iBandBitOffset = (nBand - 1) * m_poGDS->m_nBitsPerSample;
        }
        else
        {
            iPixelBitSkip = m_poGDS->m_nBitsPerSample;
        }

        // Bits per line rounds up to next byte boundary.
        GUIntBig nBitsPerLine =
            static_cast<GUIntBig>(nBlockXSize) * iPixelBitSkip;
        if ((nBitsPerLine & 7) != 0)
            nBitsPerLine = (nBitsPerLine + 7) & (~7);

        const GByte *const m_pabyBlockBuf = m_poGDS->m_pabyBlockBuf;
        const unsigned nBitsPerSample = m_poGDS->m_nBitsPerSample;
        GPtrDiff_t iPixel = 0;

        if (nBitsPerSample == 1 && eDataType == GDT_Byte)
        {
            for (unsigned iY = 0; iY < static_cast<unsigned>(nBlockYSize); ++iY)
            {
                GUIntBig iBitOffset = iBandBitOffset + iY * nBitsPerLine;

                for (unsigned iX = 0; iX < static_cast<unsigned>(nBlockXSize);
                     ++iX)
                {
                    if (m_pabyBlockBuf[iBitOffset >> 3] &
                        (0x80 >> (iBitOffset & 7)))
                        static_cast<GByte *>(pImage)[iPixel] = 1;
                    else
                        static_cast<GByte *>(pImage)[iPixel] = 0;
                    iBitOffset += iPixelBitSkip;
                    iPixel++;
                }
            }
        }
        else
        {
            for (unsigned iY = 0; iY < static_cast<unsigned>(nBlockYSize); ++iY)
            {
                GUIntBig iBitOffset = iBandBitOffset + iY * nBitsPerLine;

                for (unsigned iX = 0; iX < static_cast<unsigned>(nBlockXSize);
                     ++iX)
                {
                    unsigned nOutWord = 0;

                    for (unsigned iBit = 0; iBit < nBitsPerSample; ++iBit)
                    {
                        if (m_pabyBlockBuf[iBitOffset >> 3] &
                            (0x80 >> (iBitOffset & 7)))
                            nOutWord |= (1 << (nBitsPerSample - 1 - iBit));
                        ++iBitOffset;
                    }

                    iBitOffset = iBitOffset + iPixelBitSkip - nBitsPerSample;

                    if (eDataType == GDT_Byte)
                    {
                        static_cast<GByte *>(pImage)[iPixel++] =
                            static_cast<GByte>(nOutWord);
                    }
                    else if (eDataType == GDT_UInt16)
                    {
                        static_cast<GUInt16 *>(pImage)[iPixel++] =
                            static_cast<GUInt16>(nOutWord);
                    }
                    else if (eDataType == GDT_UInt32)
                    {
                        static_cast<GUInt32 *>(pImage)[iPixel++] = nOutWord;
                    }
                    else
                    {
                        CPLAssert(false);
                    }
                }
            }
        }
    }

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    CacheMaskForBlock(nBlockXOff, nBlockYOff);
#endif

    return CE_None;
}

/************************************************************************/
/* ==================================================================== */
/*                             GTiffBitmapBand                          */
/* ==================================================================== */
/************************************************************************/

class GTiffBitmapBand : public GTiffOddBitsBand
{
    friend class GTiffDataset;

    GDALColorTable *m_poColorTable = nullptr;

    CPL_DISALLOW_COPY_ASSIGN(GTiffBitmapBand)

  public:
    GTiffBitmapBand(GTiffDataset *, int);
    virtual ~GTiffBitmapBand();

    virtual GDALColorInterp GetColorInterpretation() override;
    virtual GDALColorTable *GetColorTable() override;
};

/************************************************************************/
/*                           GTiffBitmapBand()                          */
/************************************************************************/

GTiffBitmapBand::GTiffBitmapBand(GTiffDataset *poDSIn, int nBandIn)
    : GTiffOddBitsBand(poDSIn, nBandIn)

{
    eDataType = GDT_Byte;

    if (poDSIn->m_poColorTable != nullptr)
    {
        m_poColorTable = poDSIn->m_poColorTable->Clone();
    }
    else
    {
#ifdef ESRI_BUILD
        m_poColorTable = nullptr;
#else
        const GDALColorEntry oWhite = {255, 255, 255, 255};
        const GDALColorEntry oBlack = {0, 0, 0, 255};

        m_poColorTable = new GDALColorTable();

        if (poDSIn->m_nPhotometric == PHOTOMETRIC_MINISWHITE)
        {
            m_poColorTable->SetColorEntry(0, &oWhite);
            m_poColorTable->SetColorEntry(1, &oBlack);
        }
        else
        {
            m_poColorTable->SetColorEntry(0, &oBlack);
            m_poColorTable->SetColorEntry(1, &oWhite);
        }
#endif  // not defined ESRI_BUILD.
    }
}

/************************************************************************/
/*                          ~GTiffBitmapBand()                          */
/************************************************************************/

GTiffBitmapBand::~GTiffBitmapBand()

{
    delete m_poColorTable;
}

/************************************************************************/
/*                       GetColorInterpretation()                       */
/************************************************************************/

GDALColorInterp GTiffBitmapBand::GetColorInterpretation()

{
    if (m_poGDS->m_bPromoteTo8Bits)
        return GCI_Undefined;

    return GCI_PaletteIndex;
}

/************************************************************************/
/*                           GetColorTable()                            */
/************************************************************************/

GDALColorTable *GTiffBitmapBand::GetColorTable()

{
    if (m_poGDS->m_bPromoteTo8Bits)
        return nullptr;

    return m_poColorTable;
}

/************************************************************************/
/* ==================================================================== */
/*                          GTiffSplitBitmapBand                        */
/* ==================================================================== */
/************************************************************************/

class GTiffSplitBitmapBand final : public GTiffBitmapBand
{
    friend class GTiffDataset;
    int m_nLastLineValid = -1;

  public:
    GTiffSplitBitmapBand(GTiffDataset *, int);
    virtual ~GTiffSplitBitmapBand();

    virtual int IGetDataCoverageStatus(int nXOff, int nYOff, int nXSize,
                                       int nYSize, int nMaskFlagStop,
                                       double *pdfDataPct) override;

    virtual CPLErr IReadBlock(int, int, void *) override;
    virtual CPLErr IWriteBlock(int, int, void *) override;
};

/************************************************************************/
/*                       GTiffSplitBitmapBand()                         */
/************************************************************************/

GTiffSplitBitmapBand::GTiffSplitBitmapBand(GTiffDataset *poDSIn, int nBandIn)
    : GTiffBitmapBand(poDSIn, nBandIn)

{
    nBlockXSize = poDS->GetRasterXSize();
    nBlockYSize = 1;
}

/************************************************************************/
/*                      ~GTiffSplitBitmapBand()                         */
/************************************************************************/

GTiffSplitBitmapBand::~GTiffSplitBitmapBand()
{
}

/************************************************************************/
/*                       IGetDataCoverageStatus()                       */
/************************************************************************/

int GTiffSplitBitmapBand::IGetDataCoverageStatus(int, int, int, int, int,
                                                 double *)
{
    return GDAL_DATA_COVERAGE_STATUS_UNIMPLEMENTED |
           GDAL_DATA_COVERAGE_STATUS_DATA;
}

/************************************************************************/
/*                             IReadBlock()                             */
/************************************************************************/

CPLErr GTiffSplitBitmapBand::IReadBlock(int /* nBlockXOff */, int nBlockYOff,
                                        void *pImage)

{
    m_poGDS->Crystalize();

    if (m_nLastLineValid >= 0 && nBlockYOff > m_nLastLineValid)
        return CE_Failure;

    if (m_poGDS->m_pabyBlockBuf == nullptr)
    {
        m_poGDS->m_pabyBlockBuf = static_cast<GByte *>(
            VSI_MALLOC_VERBOSE(TIFFScanlineSize(m_poGDS->m_hTIFF)));
        if (m_poGDS->m_pabyBlockBuf == nullptr)
        {
            return CE_Failure;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Read through to target scanline.                                */
    /* -------------------------------------------------------------------- */
    if (m_poGDS->m_nLoadedBlock >= nBlockYOff)
        m_poGDS->m_nLoadedBlock = -1;

    // Set to 1 to allow GTiffErrorHandler to implement limitation on error
    // messages
    gnThreadLocalLibtiffError = 1;
    while (m_poGDS->m_nLoadedBlock < nBlockYOff)
    {
        ++m_poGDS->m_nLoadedBlock;

        std::vector<CPLErrorHandlerAccumulatorStruct> aoErrors;
        CPLInstallErrorHandlerAccumulator(aoErrors);
        int nRet = TIFFReadScanline(m_poGDS->m_hTIFF, m_poGDS->m_pabyBlockBuf,
                                    m_poGDS->m_nLoadedBlock, 0);
        CPLUninstallErrorHandlerAccumulator();

        for (size_t iError = 0; iError < aoErrors.size(); ++iError)
        {
            ReportError(aoErrors[iError].type, aoErrors[iError].no, "%s",
                        aoErrors[iError].msg.c_str());
            // FAX decoding only handles EOF condition as a warning, so
            // catch it so as to turn on error when attempting to read
            // following lines, to avoid performance issues.
            if (!m_poGDS->m_bIgnoreReadErrors &&
                aoErrors[iError].msg.find("Premature EOF") != std::string::npos)
            {
                m_nLastLineValid = nBlockYOff;
                nRet = -1;
            }
        }

        if (nRet == -1 && !m_poGDS->m_bIgnoreReadErrors)
        {
            ReportError(CE_Failure, CPLE_AppDefined,
                        "TIFFReadScanline() failed.");
            m_poGDS->m_nLoadedBlock = -1;
            gnThreadLocalLibtiffError = 0;
            return CE_Failure;
        }
    }
    gnThreadLocalLibtiffError = 0;

    /* -------------------------------------------------------------------- */
    /*      Translate 1bit data to eight bit.                               */
    /* -------------------------------------------------------------------- */
    int iSrcOffset = 0;
    int iDstOffset = 0;

    for (int iPixel = 0; iPixel < nBlockXSize; ++iPixel, ++iSrcOffset)
    {
        if (m_poGDS->m_pabyBlockBuf[iSrcOffset >> 3] &
            (0x80 >> (iSrcOffset & 0x7)))
            static_cast<GByte *>(pImage)[iDstOffset++] = 1;
        else
            static_cast<GByte *>(pImage)[iDstOffset++] = 0;
    }

    return CE_None;
}

/************************************************************************/
/*                            IWriteBlock()                             */
/************************************************************************/

CPLErr GTiffSplitBitmapBand::IWriteBlock(int /* nBlockXOff */,
                                         int /* nBlockYOff */,
                                         void * /* pImage */)

{
    ReportError(CE_Failure, CPLE_AppDefined,
                "Split bitmap bands are read-only.");
    return CE_Failure;
}

/************************************************************************/
/* ==================================================================== */
/*                            GTiffDataset                              */
/* ==================================================================== */
/************************************************************************/

/************************************************************************/
/*                            GTiffDataset()                            */
/************************************************************************/

GTiffDataset::GTiffDataset()
    : m_bStreamingIn(false), m_bStreamingOut(false), m_bScanDeferred(true),
      m_bSingleIFDOpened(false), m_bLoadedBlockDirty(false),
      m_bWriteError(false), m_bLookedForProjection(false),
      m_bLookedForMDAreaOrPoint(false), m_bGeoTransformValid(false),
      m_bCrystalized(true), m_bGeoTIFFInfoChanged(false),
      m_bForceUnsetGTOrGCPs(false), m_bForceUnsetProjection(false),
      m_bNoDataChanged(false), m_bNoDataSet(false), m_bNoDataSetAsInt64(false),
      m_bNoDataSetAsUInt64(false), m_bMetadataChanged(false),
      m_bColorProfileMetadataChanged(false), m_bForceUnsetRPC(false),
      m_bNeedsRewrite(false), m_bLoadingOtherBands(false), m_bIsOverview(false),
      m_bWriteEmptyTiles(true), m_bFillEmptyTilesAtClosing(false),
      m_bTreatAsSplit(false), m_bTreatAsSplitBitmap(false), m_bClipWarn(false),
      m_bIMDRPCMetadataLoaded(false), m_bEXIFMetadataLoaded(false),
      m_bICCMetadataLoaded(false),
      m_bHasWarnedDisableAggressiveBandCaching(false),
      m_bDontReloadFirstBlock(false), m_bWebPLossless(false),
      m_bPromoteTo8Bits(false),
      m_bDebugDontWriteBlocks(
          CPLTestBool(CPLGetConfigOption("GTIFF_DONT_WRITE_BLOCKS", "NO"))),
      m_bIsFinalized(false),
      m_bIgnoreReadErrors(
          CPLTestBool(CPLGetConfigOption("GTIFF_IGNORE_READ_ERRORS", "NO"))),
      m_bDirectIO(CPLTestBool(CPLGetConfigOption("GTIFF_DIRECT_IO", "NO"))),
      m_bReadGeoTransform(false), m_bLoadPam(false),
      m_bHasGotSiblingFiles(false),
      m_bHasIdentifiedAuthorizedGeoreferencingSources(false),
      m_bLayoutIFDSBeforeData(false), m_bBlockOrderRowMajor(false),
      m_bLeaderSizeAsUInt4(false), m_bTrailerRepeatedLast4BytesRepeated(false),
      m_bMaskInterleavedWithImagery(false), m_bKnownIncompatibleEdition(false),
      m_bWriteKnownIncompatibleEdition(false), m_bHasUsedReadEncodedAPI(false),
      m_bWriteCOGLayout(false)
{
    // CPLDebug("GDAL", "sizeof(GTiffDataset) = %d bytes", static_cast<int>(
    //     sizeof(GTiffDataset)));

    const char *pszVirtualMemIO =
        CPLGetConfigOption("GTIFF_VIRTUAL_MEM_IO", "NO");
    if (EQUAL(pszVirtualMemIO, "IF_ENOUGH_RAM"))
        m_eVirtualMemIOUsage = VirtualMemIOEnum::IF_ENOUGH_RAM;
    else if (CPLTestBool(pszVirtualMemIO))
        m_eVirtualMemIOUsage = VirtualMemIOEnum::YES;

    m_oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
}

/************************************************************************/
/*                           ~GTiffDataset()                            */
/************************************************************************/

GTiffDataset::~GTiffDataset()

{
    Finalize();
    if (m_pszTmpFilename)
    {
        VSIUnlink(m_pszTmpFilename);
        CPLFree(m_pszTmpFilename);
    }
}

/************************************************************************/
/*                             Finalize()                               */
/************************************************************************/

int GTiffDataset::Finalize()
{
    if (m_bIsFinalized)
        return FALSE;

    bool bHasDroppedRef = false;

    Crystalize();

    if (m_bColorProfileMetadataChanged)
    {
        SaveICCProfile(this, nullptr, nullptr, 0);
        m_bColorProfileMetadataChanged = false;
    }

    /* -------------------------------------------------------------------- */
    /*      Handle forcing xml:ESRI data to be written to PAM.              */
    /* -------------------------------------------------------------------- */
    if (CPLTestBool(CPLGetConfigOption("ESRI_XML_PAM", "NO")))
    {
        char **papszESRIMD = GTiffDataset::GetMetadata("xml:ESRI");
        if (papszESRIMD)
        {
            GDALPamDataset::SetMetadata(papszESRIMD, "xml:ESRI");
        }
    }

    if (m_psVirtualMemIOMapping)
        CPLVirtualMemFree(m_psVirtualMemIOMapping);
    m_psVirtualMemIOMapping = nullptr;

    /* -------------------------------------------------------------------- */
    /*      Fill in missing blocks with empty data.                         */
    /* -------------------------------------------------------------------- */
    if (m_bFillEmptyTilesAtClosing)
    {
        /* --------------------------------------------------------------------
         */
        /*  Ensure any blocks write cached by GDAL gets pushed through libtiff.
         */
        /* --------------------------------------------------------------------
         */
        FlushCacheInternal(true, /* at closing */
                           false /* do not call FlushDirectory */);

        FillEmptyTiles();
        m_bFillEmptyTilesAtClosing = false;
    }

    /* -------------------------------------------------------------------- */
    /*      Force a complete flush, including either rewriting(moving)      */
    /*      of writing in place the current directory.                      */
    /* -------------------------------------------------------------------- */
    FlushCacheInternal(true /* at closing */, true);

    // Destroy compression queue
    if (m_poCompressQueue)
    {
        m_poCompressQueue->WaitCompletion();

        for (int i = 0; i < static_cast<int>(m_asCompressionJobs.size()); ++i)
        {
            CPLFree(m_asCompressionJobs[i].pabyBuffer);
            if (m_asCompressionJobs[i].pszTmpFilename)
            {
                VSIUnlink(m_asCompressionJobs[i].pszTmpFilename);
                CPLFree(m_asCompressionJobs[i].pszTmpFilename);
            }
        }
        CPLDestroyMutex(m_hCompressThreadPoolMutex);
        m_hCompressThreadPoolMutex = nullptr;
        m_poCompressQueue.reset();
    }

    /* -------------------------------------------------------------------- */
    /*      If there is still changed metadata, then presumably we want     */
    /*      to push it into PAM.                                            */
    /* -------------------------------------------------------------------- */
    if (m_bMetadataChanged)
    {
        PushMetadataToPam();
        m_bMetadataChanged = false;
        GDALPamDataset::FlushCache(false);
    }

    /* -------------------------------------------------------------------- */
    /*      Cleanup overviews.                                              */
    /* -------------------------------------------------------------------- */
    if (!m_poBaseDS)
    {
        // Nullify m_nOverviewCount before deleting overviews, otherwise
        // GTiffDataset::FlushDirectory() might try to access an overview
        // that is being deleted (#5580)
        const int nOldOverviewCount = m_nOverviewCount;
        m_nOverviewCount = 0;
        for (int i = 0; i < nOldOverviewCount; ++i)
        {
            delete m_papoOverviewDS[i];
            bHasDroppedRef = true;
        }

        for (int i = 0; i < m_nJPEGOverviewCountOri; ++i)
        {
            delete m_papoJPEGOverviewDS[i];
            bHasDroppedRef = true;
        }
        m_nJPEGOverviewCount = 0;
        m_nJPEGOverviewCountOri = 0;
        CPLFree(m_papoJPEGOverviewDS);
        m_papoJPEGOverviewDS = nullptr;
    }

    // If we are a mask dataset, we can have overviews, but we don't
    // own them. We can only free the array, not the overviews themselves.
    CPLFree(m_papoOverviewDS);
    m_papoOverviewDS = nullptr;

    // m_poMaskDS is owned by the main image and the overviews
    // so because of the latter case, we can delete it even if
    // we are not the base image.
    if (m_poMaskDS)
    {
        // Nullify m_nOverviewCount before deleting overviews, otherwise
        // GTiffDataset::FlushDirectory() might try to access it while being
        // deleted. (#5580)
        auto poMaskDS = m_poMaskDS;
        m_poMaskDS = nullptr;
        delete poMaskDS;
        bHasDroppedRef = true;
    }

    if (m_poColorTable != nullptr)
        delete m_poColorTable;
    m_poColorTable = nullptr;

    if (m_hTIFF)
    {
        XTIFFClose(m_hTIFF);
        m_hTIFF = nullptr;
    }

    if (!m_poBaseDS)
    {
        if (m_fpL != nullptr)
        {
            if (m_bWriteKnownIncompatibleEdition)
            {
                GByte abyHeader[4096];
                VSIFSeekL(m_fpL, 0, SEEK_SET);
                VSIFReadL(abyHeader, 1, sizeof(abyHeader), m_fpL);
                const char *szKeyToLook =
                    "KNOWN_INCOMPATIBLE_EDITION=NO\n ";  // trailing space
                                                         // intended
                for (size_t i = 0; i < sizeof(abyHeader) - strlen(szKeyToLook);
                     i++)
                {
                    if (memcmp(abyHeader + i, szKeyToLook,
                               strlen(szKeyToLook)) == 0)
                    {
                        const char *szNewKey =
                            "KNOWN_INCOMPATIBLE_EDITION=YES\n";
                        CPLAssert(strlen(szKeyToLook) == strlen(szNewKey));
                        memcpy(abyHeader + i, szNewKey, strlen(szNewKey));
                        VSIFSeekL(m_fpL, 0, SEEK_SET);
                        VSIFWriteL(abyHeader, 1, sizeof(abyHeader), m_fpL);
                        break;
                    }
                }
            }
            if (VSIFCloseL(m_fpL) != 0)
            {
                ReportError(CE_Failure, CPLE_FileIO, "I/O error");
            }
            m_fpL = nullptr;
        }
    }

    if (m_fpToWrite != nullptr)
    {
        if (VSIFCloseL(m_fpToWrite) != 0)
        {
            ReportError(CE_Failure, CPLE_FileIO, "I/O error");
        }
        m_fpToWrite = nullptr;
    }

    if (m_nGCPCount > 0)
    {
        GDALDeinitGCPs(m_nGCPCount, m_pasGCPList);
        CPLFree(m_pasGCPList);
        m_pasGCPList = nullptr;
        m_nGCPCount = 0;
    }

    CSLDestroy(m_papszCreationOptions);
    m_papszCreationOptions = nullptr;

    CPLFree(m_pabyTempWriteBuffer);
    m_pabyTempWriteBuffer = nullptr;

    m_bIMDRPCMetadataLoaded = false;
    CSLDestroy(m_papszMetadataFiles);
    m_papszMetadataFiles = nullptr;

    VSIFree(m_pTempBufferForCommonDirectIO);
    m_pTempBufferForCommonDirectIO = nullptr;

    CPLFree(m_panMaskOffsetLsb);
    m_panMaskOffsetLsb = nullptr;

    CPLFree(m_pszVertUnit);
    m_pszVertUnit = nullptr;

    CPLFree(m_pszFilename);
    m_pszFilename = nullptr;

    CPLFree(m_pszGeorefFilename);
    m_pszGeorefFilename = nullptr;

    m_bIsFinalized = true;

    return bHasDroppedRef;
}

/************************************************************************/
/*                        CloseDependentDatasets()                      */
/************************************************************************/

int GTiffDataset::CloseDependentDatasets()
{
    if (m_poBaseDS)
        return FALSE;

    int bHasDroppedRef = GDALPamDataset::CloseDependentDatasets();

    bHasDroppedRef |= Finalize();

    return bHasDroppedRef;
}

/************************************************************************/
/*                        GetJPEGOverviewCount()                        */
/************************************************************************/

int GTiffDataset::GetJPEGOverviewCount()
{
    if (m_nJPEGOverviewCount >= 0)
        return m_nJPEGOverviewCount;

    m_nJPEGOverviewCount = 0;
    if (m_poBaseDS || eAccess != GA_ReadOnly ||
        m_nCompression != COMPRESSION_JPEG ||
        (nRasterXSize < 256 && nRasterYSize < 256) ||
        !CPLTestBool(CPLGetConfigOption("GTIFF_IMPLICIT_JPEG_OVR", "YES")) ||
        GDALGetDriverByName("JPEG") == nullptr)
    {
        return 0;
    }
    const char *pszSourceColorSpace =
        m_oGTiffMDMD.GetMetadataItem("SOURCE_COLOR_SPACE", "IMAGE_STRUCTURE");
    if (pszSourceColorSpace != nullptr && EQUAL(pszSourceColorSpace, "CMYK"))
    {
        // We cannot handle implicit overviews on JPEG CMYK datasets converted
        // to RGBA This would imply doing the conversion in
        // GTiffJPEGOverviewBand.
        return 0;
    }

    // libjpeg-6b only supports 2, 4 and 8 scale denominators.
    // TODO: Later versions support more.
    for (signed char i = 2; i >= 0; i--)
    {
        if (nRasterXSize >= (256 << i) || nRasterYSize >= (256 << i))
        {
            m_nJPEGOverviewCount = i + 1;
            break;
        }
    }
    if (m_nJPEGOverviewCount == 0)
        return 0;

    // Get JPEG tables.
    uint32_t nJPEGTableSize = 0;
    void *pJPEGTable = nullptr;
    GByte abyFFD8[] = {0xFF, 0xD8};
    if (TIFFGetField(m_hTIFF, TIFFTAG_JPEGTABLES, &nJPEGTableSize, &pJPEGTable))
    {
        if (pJPEGTable == nullptr || nJPEGTableSize > INT_MAX ||
            static_cast<GByte *>(pJPEGTable)[nJPEGTableSize - 1] != 0xD9)
        {
            m_nJPEGOverviewCount = 0;
            return 0;
        }
        nJPEGTableSize--;  // Remove final 0xD9.
    }
    else
    {
        pJPEGTable = abyFFD8;
        nJPEGTableSize = 2;
    }

    m_papoJPEGOverviewDS = static_cast<GTiffJPEGOverviewDS **>(
        CPLMalloc(sizeof(GTiffJPEGOverviewDS *) * m_nJPEGOverviewCount));
    for (int i = 0; i < m_nJPEGOverviewCount; ++i)
    {
        m_papoJPEGOverviewDS[i] = new GTiffJPEGOverviewDS(
            this, i + 1, pJPEGTable, static_cast<int>(nJPEGTableSize));
    }

    m_nJPEGOverviewCountOri = m_nJPEGOverviewCount;

    return m_nJPEGOverviewCount;
}

/************************************************************************/
/*                           FillEmptyTiles()                           */
/************************************************************************/

void GTiffDataset::FillEmptyTiles()

{
    /* -------------------------------------------------------------------- */
    /*      How many blocks are there in this file?                         */
    /* -------------------------------------------------------------------- */
    const int nBlockCount = m_nPlanarConfig == PLANARCONFIG_SEPARATE
                                ? m_nBlocksPerBand * nBands
                                : m_nBlocksPerBand;

    /* -------------------------------------------------------------------- */
    /*      Fetch block maps.                                               */
    /* -------------------------------------------------------------------- */
    toff_t *panByteCounts = nullptr;

    if (TIFFIsTiled(m_hTIFF))
        TIFFGetField(m_hTIFF, TIFFTAG_TILEBYTECOUNTS, &panByteCounts);
    else
        TIFFGetField(m_hTIFF, TIFFTAG_STRIPBYTECOUNTS, &panByteCounts);

    if (panByteCounts == nullptr)
    {
        // Got here with libtiff 3.9.3 and tiff_write_8 test.
        ReportError(CE_Failure, CPLE_AppDefined,
                    "FillEmptyTiles() failed because panByteCounts == NULL");
        return;
    }

    /* -------------------------------------------------------------------- */
    /*      Prepare a blank data buffer to write for uninitialized blocks.  */
    /* -------------------------------------------------------------------- */
    const GPtrDiff_t nBlockBytes =
        TIFFIsTiled(m_hTIFF) ? static_cast<GPtrDiff_t>(TIFFTileSize(m_hTIFF))
                             : static_cast<GPtrDiff_t>(TIFFStripSize(m_hTIFF));

    GByte *pabyData = static_cast<GByte *>(VSI_CALLOC_VERBOSE(nBlockBytes, 1));
    if (pabyData == nullptr)
    {
        return;
    }

    // Force tiles completely filled with the nodata value to be written.
    m_bWriteEmptyTiles = true;

    /* -------------------------------------------------------------------- */
    /*      If set, fill data buffer with no data value.                    */
    /* -------------------------------------------------------------------- */
    if ((m_bNoDataSet && m_dfNoDataValue != 0.0) ||
        (m_bNoDataSetAsInt64 && m_nNoDataValueInt64 != 0) ||
        (m_bNoDataSetAsUInt64 && m_nNoDataValueUInt64 != 0))
    {
        const GDALDataType eDataType = GetRasterBand(1)->GetRasterDataType();
        const int nDataTypeSize = GDALGetDataTypeSizeBytes(eDataType);
        if (nDataTypeSize &&
            nDataTypeSize * 8 == static_cast<int>(m_nBitsPerSample))
        {
            if (m_bNoDataSetAsInt64)
            {
                GDALCopyWords64(&m_nNoDataValueInt64, GDT_Int64, 0, pabyData,
                                eDataType, nDataTypeSize,
                                nBlockBytes / nDataTypeSize);
            }
            else if (m_bNoDataSetAsUInt64)
            {
                GDALCopyWords64(&m_nNoDataValueUInt64, GDT_UInt64, 0, pabyData,
                                eDataType, nDataTypeSize,
                                nBlockBytes / nDataTypeSize);
            }
            else
            {
                double dfNoData = m_dfNoDataValue;

                // Hack for Signed Int8 case. As the data type is GDT_Byte
                // (unsigned), we have to convert a negative nodata value in the
                // range [-128,-1] in [128, 255]
                if (m_nBitsPerSample == 8 &&
                    m_nSampleFormat == SAMPLEFORMAT_INT && dfNoData < 0 &&
                    dfNoData >= -128 && static_cast<int>(dfNoData) == dfNoData)
                {
                    dfNoData = 256 + dfNoData;
                }

                GDALCopyWords64(&dfNoData, GDT_Float64, 0, pabyData, eDataType,
                                nDataTypeSize, nBlockBytes / nDataTypeSize);
            }
        }
        else if (nDataTypeSize)
        {
            // Handle non power-of-two depths.
            // Ideally make a packed buffer, but that is a bit tedious,
            // so use the normal I/O interfaces.

            CPLFree(pabyData);

            pabyData = static_cast<GByte *>(VSI_MALLOC3_VERBOSE(
                m_nBlockXSize, m_nBlockYSize, nDataTypeSize));
            if (pabyData == nullptr)
                return;
            if (m_bNoDataSetAsInt64)
            {
                GDALCopyWords64(&m_nNoDataValueInt64, GDT_Int64, 0, pabyData,
                                eDataType, nDataTypeSize,
                                static_cast<GPtrDiff_t>(m_nBlockXSize) *
                                    m_nBlockYSize);
            }
            else if (m_bNoDataSetAsUInt64)
            {
                GDALCopyWords64(&m_nNoDataValueUInt64, GDT_UInt64, 0, pabyData,
                                eDataType, nDataTypeSize,
                                static_cast<GPtrDiff_t>(m_nBlockXSize) *
                                    m_nBlockYSize);
            }
            else
            {
                GDALCopyWords64(&m_dfNoDataValue, GDT_Float64, 0, pabyData,
                                eDataType, nDataTypeSize,
                                static_cast<GPtrDiff_t>(m_nBlockXSize) *
                                    m_nBlockYSize);
            }
            const int nBlocksPerRow = DIV_ROUND_UP(nRasterXSize, m_nBlockYSize);
            for (int iBlock = 0; iBlock < nBlockCount; ++iBlock)
            {
                if (panByteCounts[iBlock] == 0)
                {
                    if (m_nPlanarConfig == PLANARCONFIG_SEPARATE || nBands == 1)
                    {
                        CPL_IGNORE_RET_VAL(
                            GetRasterBand(1 + iBlock / m_nBlocksPerBand)
                                ->WriteBlock(
                                    (iBlock % m_nBlocksPerBand) % nBlocksPerRow,
                                    (iBlock % m_nBlocksPerBand) / nBlocksPerRow,
                                    pabyData));
                    }
                    else
                    {
                        // In contig case, don't directly call WriteBlock(), as
                        // it could cause useless decompression-recompression.
                        const int nXOff =
                            (iBlock % nBlocksPerRow) * m_nBlockXSize;
                        const int nYOff =
                            (iBlock / nBlocksPerRow) * m_nBlockYSize;
                        const int nXSize =
                            (nXOff + m_nBlockXSize <= nRasterXSize)
                                ? m_nBlockXSize
                                : nRasterXSize - nXOff;
                        const int nYSize =
                            (nYOff + m_nBlockYSize <= nRasterYSize)
                                ? m_nBlockYSize
                                : nRasterYSize - nYOff;
                        for (int iBand = 1; iBand <= nBands; ++iBand)
                        {
                            CPL_IGNORE_RET_VAL(GetRasterBand(iBand)->RasterIO(
                                GF_Write, nXOff, nYOff, nXSize, nYSize,
                                pabyData, nXSize, nYSize, eDataType, 0, 0,
                                nullptr));
                        }
                    }
                }
            }
            CPLFree(pabyData);
            return;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      When we must fill with zeroes, try to create non-sparse file    */
    /*      w.r.t TIFF spec ... as a sparse file w.r.t filesystem, ie by    */
    /*      seeking to end of file instead of writing zero blocks.          */
    /* -------------------------------------------------------------------- */
    else if (m_nCompression == COMPRESSION_NONE && (m_nBitsPerSample % 8) == 0)
    {
        // Only use libtiff to write the first sparse block to ensure that it
        // will serialize offset and count arrays back to disk.
        int nCountBlocksToZero = 0;
        for (int iBlock = 0; iBlock < nBlockCount; ++iBlock)
        {
            if (panByteCounts[iBlock] == 0)
            {
                if (nCountBlocksToZero == 0)
                {
                    const bool bWriteEmptyTilesBak = m_bWriteEmptyTiles;
                    m_bWriteEmptyTiles = true;
                    const bool bOK = WriteEncodedTileOrStrip(iBlock, pabyData,
                                                             FALSE) == CE_None;
                    m_bWriteEmptyTiles = bWriteEmptyTilesBak;
                    if (!bOK)
                        break;
                }
                nCountBlocksToZero++;
            }
        }
        CPLFree(pabyData);

        --nCountBlocksToZero;

        // And then seek to end of file for other ones.
        if (nCountBlocksToZero > 0)
        {
            toff_t *panByteOffsets = nullptr;

            if (TIFFIsTiled(m_hTIFF))
                TIFFGetField(m_hTIFF, TIFFTAG_TILEOFFSETS, &panByteOffsets);
            else
                TIFFGetField(m_hTIFF, TIFFTAG_STRIPOFFSETS, &panByteOffsets);

            if (panByteOffsets == nullptr)
            {
                ReportError(
                    CE_Failure, CPLE_AppDefined,
                    "FillEmptyTiles() failed because panByteOffsets == NULL");
                return;
            }

            VSILFILE *fpTIF = VSI_TIFFGetVSILFile(TIFFClientdata(m_hTIFF));
            VSIFSeekL(fpTIF, 0, SEEK_END);
            const vsi_l_offset nOffset = VSIFTellL(fpTIF);

            vsi_l_offset iBlockToZero = 0;
            for (int iBlock = 0; iBlock < nBlockCount; ++iBlock)
            {
                if (panByteCounts[iBlock] == 0)
                {
                    panByteOffsets[iBlock] = static_cast<toff_t>(
                        nOffset + iBlockToZero * nBlockBytes);
                    panByteCounts[iBlock] = nBlockBytes;
                    iBlockToZero++;
                }
            }
            CPLAssert(iBlockToZero ==
                      static_cast<vsi_l_offset>(nCountBlocksToZero));

            if (VSIFTruncateL(fpTIF, nOffset + iBlockToZero * nBlockBytes) != 0)
            {
                ReportError(CE_Failure, CPLE_FileIO,
                            "Cannot initialize empty blocks");
            }
        }

        return;
    }

    /* -------------------------------------------------------------------- */
    /*      Check all blocks, writing out data for uninitialized blocks.    */
    /* -------------------------------------------------------------------- */

    GByte *pabyRaw = nullptr;
    vsi_l_offset nRawSize = 0;
    for (int iBlock = 0; iBlock < nBlockCount; ++iBlock)
    {
        if (panByteCounts[iBlock] == 0)
        {
            if (pabyRaw == nullptr)
            {
                if (WriteEncodedTileOrStrip(iBlock, pabyData, FALSE) != CE_None)
                    break;

                vsi_l_offset nOffset = 0;
                if (!IsBlockAvailable(iBlock, &nOffset, &nRawSize))
                    break;

                // When using compression, get back the compressed block
                // so we can use the raw API to write it faster.
                if (m_nCompression != COMPRESSION_NONE)
                {
                    pabyRaw = static_cast<GByte *>(
                        VSI_MALLOC_VERBOSE(static_cast<size_t>(nRawSize)));
                    if (pabyRaw)
                    {
                        VSILFILE *fp =
                            VSI_TIFFGetVSILFile(TIFFClientdata(m_hTIFF));
                        const vsi_l_offset nCurOffset = VSIFTellL(fp);
                        VSIFSeekL(fp, nOffset, SEEK_SET);
                        VSIFReadL(pabyRaw, 1, static_cast<size_t>(nRawSize),
                                  fp);
                        VSIFSeekL(fp, nCurOffset, SEEK_SET);
                    }
                }
            }
            else
            {
                WriteRawStripOrTile(iBlock, pabyRaw,
                                    static_cast<GPtrDiff_t>(nRawSize));
            }
        }
    }

    CPLFree(pabyData);
    VSIFree(pabyRaw);
}

/************************************************************************/
/*                         HasOnlyNoData()                              */
/************************************************************************/

bool GTiffDataset::HasOnlyNoData(const void *pBuffer, int nWidth, int nHeight,
                                 int nLineStride, int nComponents)
{
    if (m_nSampleFormat == SAMPLEFORMAT_COMPLEXINT ||
        m_nSampleFormat == SAMPLEFORMAT_COMPLEXIEEEFP)
        return false;
    if (m_bNoDataSetAsInt64 || m_bNoDataSetAsUInt64)
        return false;  // FIXME: over pessimistic
    return GDALBufferHasOnlyNoData(
        pBuffer, m_bNoDataSet ? m_dfNoDataValue : 0.0, nWidth, nHeight,
        nLineStride, nComponents, m_nBitsPerSample,
        m_nSampleFormat == SAMPLEFORMAT_UINT  ? GSF_UNSIGNED_INT
        : m_nSampleFormat == SAMPLEFORMAT_INT ? GSF_SIGNED_INT
                                              : GSF_FLOATING_POINT);
}

/************************************************************************/
/*                     IsFirstPixelEqualToNoData()                      */
/************************************************************************/

inline bool GTiffDataset::IsFirstPixelEqualToNoData(const void *pBuffer)
{
    const GDALDataType eDT = GetRasterBand(1)->GetRasterDataType();
    const double dfEffectiveNoData = (m_bNoDataSet) ? m_dfNoDataValue : 0.0;
    if (m_bNoDataSetAsInt64 || m_bNoDataSetAsUInt64)
        return true;  // FIXME: over pessimistic
    if (m_nBitsPerSample == 8 ||
        (m_nBitsPerSample < 8 && dfEffectiveNoData == 0))
    {
        if (m_nSampleFormat == SAMPLEFORMAT_INT)
        {
            return GDALIsValueInRange<signed char>(dfEffectiveNoData) &&
                   *(static_cast<const signed char *>(pBuffer)) ==
                       static_cast<signed char>(dfEffectiveNoData);
        }
        return GDALIsValueInRange<GByte>(dfEffectiveNoData) &&
               *(static_cast<const GByte *>(pBuffer)) ==
                   static_cast<GByte>(dfEffectiveNoData);
    }
    if (m_nBitsPerSample == 16 && eDT == GDT_UInt16)
    {
        return GDALIsValueInRange<GUInt16>(dfEffectiveNoData) &&
               *(static_cast<const GUInt16 *>(pBuffer)) ==
                   static_cast<GUInt16>(dfEffectiveNoData);
    }
    if (m_nBitsPerSample == 16 && eDT == GDT_Int16)
    {
        return GDALIsValueInRange<GInt16>(dfEffectiveNoData) &&
               *(static_cast<const GInt16 *>(pBuffer)) ==
                   static_cast<GInt16>(dfEffectiveNoData);
    }
    if (m_nBitsPerSample == 32 && eDT == GDT_UInt32)
    {
        return GDALIsValueInRange<GUInt32>(dfEffectiveNoData) &&
               *(static_cast<const GUInt32 *>(pBuffer)) ==
                   static_cast<GUInt32>(dfEffectiveNoData);
    }
    if (m_nBitsPerSample == 32 && eDT == GDT_Int32)
    {
        return GDALIsValueInRange<GInt32>(dfEffectiveNoData) &&
               *(static_cast<const GInt32 *>(pBuffer)) ==
                   static_cast<GInt32>(dfEffectiveNoData);
    }
    if (m_nBitsPerSample == 64 && eDT == GDT_UInt64)
    {
        return GDALIsValueInRange<std::uint64_t>(dfEffectiveNoData) &&
               *(static_cast<const std::uint64_t *>(pBuffer)) ==
                   static_cast<std::uint64_t>(dfEffectiveNoData);
    }
    if (m_nBitsPerSample == 64 && eDT == GDT_Int64)
    {
        return GDALIsValueInRange<std::int64_t>(dfEffectiveNoData) &&
               *(static_cast<const std::int64_t *>(pBuffer)) ==
                   static_cast<std::int64_t>(dfEffectiveNoData);
    }
    if (m_nBitsPerSample == 32 && eDT == GDT_Float32)
    {
        if (CPLIsNan(m_dfNoDataValue))
            return CPL_TO_BOOL(
                CPLIsNan(*(static_cast<const float *>(pBuffer))));
        return GDALIsValueInRange<float>(dfEffectiveNoData) &&
               *(static_cast<const float *>(pBuffer)) ==
                   static_cast<float>(dfEffectiveNoData);
    }
    if (m_nBitsPerSample == 64 && eDT == GDT_Float64)
    {
        if (CPLIsNan(dfEffectiveNoData))
            return CPL_TO_BOOL(
                CPLIsNan(*(static_cast<const double *>(pBuffer))));
        return *(static_cast<const double *>(pBuffer)) == dfEffectiveNoData;
    }
    return false;
}

/************************************************************************/
/*                        WriteEncodedTile()                            */
/************************************************************************/

bool GTiffDataset::WriteEncodedTile(uint32_t tile, GByte *pabyData,
                                    int bPreserveDataBuffer)
{
    int iRow = 0;
    int iColumn = 0;
    int nBlocksPerRow = 1;
    int nBlocksPerColumn = 1;

    /* -------------------------------------------------------------------- */
    /*      Don't write empty blocks in some cases.                         */
    /* -------------------------------------------------------------------- */
    if (!m_bWriteEmptyTiles && IsFirstPixelEqualToNoData(pabyData))
    {
        if (!IsBlockAvailable(tile))
        {
            const int nComponents =
                m_nPlanarConfig == PLANARCONFIG_CONTIG ? nBands : 1;
            nBlocksPerRow = DIV_ROUND_UP(nRasterXSize, m_nBlockXSize);
            nBlocksPerColumn = DIV_ROUND_UP(nRasterYSize, m_nBlockYSize);

            iColumn = (tile % m_nBlocksPerBand) % nBlocksPerRow;
            iRow = (tile % m_nBlocksPerBand) / nBlocksPerRow;

            const int nActualBlockWidth =
                (iColumn == nBlocksPerRow - 1)
                    ? nRasterXSize - iColumn * m_nBlockXSize
                    : m_nBlockXSize;
            const int nActualBlockHeight =
                (iRow == nBlocksPerColumn - 1)
                    ? nRasterYSize - iRow * m_nBlockYSize
                    : m_nBlockYSize;

            if (HasOnlyNoData(pabyData, nActualBlockWidth, nActualBlockHeight,
                              m_nBlockXSize, nComponents))
            {
                return true;
            }
        }
    }

    // Do we need to spread edge values right or down for a partial
    // JPEG encoded tile?  We do this to avoid edge artifacts.
    bool bNeedTileFill = false;
    if (m_nCompression == COMPRESSION_JPEG)
    {
        nBlocksPerRow = DIV_ROUND_UP(nRasterXSize, m_nBlockXSize);
        nBlocksPerColumn = DIV_ROUND_UP(nRasterYSize, m_nBlockYSize);

        iColumn = (tile % m_nBlocksPerBand) % nBlocksPerRow;
        iRow = (tile % m_nBlocksPerBand) / nBlocksPerRow;

        // Is this a partial right edge tile?
        if (iRow == nBlocksPerRow - 1 && nRasterXSize % m_nBlockXSize != 0)
            bNeedTileFill = true;

        // Is this a partial bottom edge tile?
        if (iColumn == nBlocksPerColumn - 1 &&
            nRasterYSize % m_nBlockYSize != 0)
            bNeedTileFill = true;
    }

    // If we need to fill out the tile, or if we want to prevent
    // TIFFWriteEncodedTile from altering the buffer as part of
    // byte swapping the data on write then we will need a temporary
    // working buffer.  If not, we can just do a direct write.
    const GPtrDiff_t cc = static_cast<GPtrDiff_t>(TIFFTileSize(m_hTIFF));

    if (bPreserveDataBuffer &&
        (TIFFIsByteSwapped(m_hTIFF) || bNeedTileFill || m_panMaskOffsetLsb))
    {
        if (m_pabyTempWriteBuffer == nullptr)
        {
            m_pabyTempWriteBuffer = CPLMalloc(cc);
        }
        memcpy(m_pabyTempWriteBuffer, pabyData, cc);

        pabyData = static_cast<GByte *>(m_pabyTempWriteBuffer);
    }

    // Perform tile fill if needed.
    // TODO: we should also handle the case of nBitsPerSample == 12
    // but this is more involved.
    if (bNeedTileFill && m_nBitsPerSample == 8)
    {
        const int nComponents =
            m_nPlanarConfig == PLANARCONFIG_CONTIG ? nBands : 1;

        CPLDebug("GTiff", "Filling out jpeg edge tile on write.");

        const int nRightPixelsToFill =
            iColumn == nBlocksPerRow - 1
                ? m_nBlockXSize * (iColumn + 1) - nRasterXSize
                : 0;
        const int nBottomPixelsToFill =
            iRow == nBlocksPerColumn - 1
                ? m_nBlockYSize * (iRow + 1) - nRasterYSize
                : 0;

        // Fill out to the right.
        const int iSrcX = m_nBlockXSize - nRightPixelsToFill - 1;

        for (int iX = iSrcX + 1; iX < m_nBlockXSize; ++iX)
        {
            for (int iY = 0; iY < m_nBlockYSize; ++iY)
            {
                memcpy(pabyData +
                           (static_cast<GPtrDiff_t>(m_nBlockXSize) * iY + iX) *
                               nComponents,
                       pabyData + (static_cast<GPtrDiff_t>(m_nBlockXSize) * iY +
                                   iSrcX) *
                                      nComponents,
                       nComponents);
            }
        }

        // Now fill out the bottom.
        const int iSrcY = m_nBlockYSize - nBottomPixelsToFill - 1;
        for (int iY = iSrcY + 1; iY < m_nBlockYSize; ++iY)
        {
            memcpy(pabyData + static_cast<GPtrDiff_t>(m_nBlockXSize) *
                                  nComponents * iY,
                   pabyData + static_cast<GPtrDiff_t>(m_nBlockXSize) *
                                  nComponents * iSrcY,
                   static_cast<GPtrDiff_t>(m_nBlockXSize) * nComponents);
        }
    }

    if (m_panMaskOffsetLsb)
    {
        const int iBand = m_nPlanarConfig == PLANARCONFIG_SEPARATE
                              ? static_cast<int>(tile) / m_nBlocksPerBand
                              : -1;
        DiscardLsb(pabyData, cc, iBand);
    }

    if (m_bStreamingOut)
    {
        if (tile != static_cast<uint32_t>(m_nLastWrittenBlockId + 1))
        {
            ReportError(CE_Failure, CPLE_NotSupported,
                        "Attempt to write block %d whereas %d was expected",
                        tile, m_nLastWrittenBlockId + 1);
            return false;
        }
        if (static_cast<GPtrDiff_t>(VSIFWriteL(pabyData, 1, cc, m_fpToWrite)) !=
            cc)
        {
            ReportError(CE_Failure, CPLE_FileIO,
                        "Could not write " CPL_FRMT_GUIB " bytes",
                        static_cast<GUIntBig>(cc));
            return false;
        }
        m_nLastWrittenBlockId = tile;
        return true;
    }

    /* -------------------------------------------------------------------- */
    /*      Should we do compression in a worker thread ?                   */
    /* -------------------------------------------------------------------- */
    if (SubmitCompressionJob(tile, pabyData, cc, m_nBlockYSize))
        return true;

        // libtiff 4.0.6 or older do not always properly report write errors.
#if TIFFLIB_VERSION <= 20150912
    const CPLErr eBefore = CPLGetLastErrorType();
#endif
    const bool bRet = TIFFWriteEncodedTile(m_hTIFF, tile, pabyData, cc) == cc;
#if TIFFLIB_VERSION <= 20150912
    if (eBefore == CE_None && CPLGetLastErrorType() == CE_Failure)
        return false;
#endif
    return bRet;
}

/************************************************************************/
/*                        WriteEncodedStrip()                           */
/************************************************************************/

bool GTiffDataset::WriteEncodedStrip(uint32_t strip, GByte *pabyData,
                                     int bPreserveDataBuffer)
{
    GPtrDiff_t cc = static_cast<GPtrDiff_t>(TIFFStripSize(m_hTIFF));
    const auto ccFull = cc;

    /* -------------------------------------------------------------------- */
    /*      If this is the last strip in the image, and is partial, then    */
    /*      we need to trim the number of scanlines written to the          */
    /*      amount of valid data we have. (#2748)                           */
    /* -------------------------------------------------------------------- */
    const int nStripWithinBand = strip % m_nBlocksPerBand;
    int nStripHeight = m_nRowsPerStrip;

    if (nStripWithinBand * nStripHeight > GetRasterYSize() - nStripHeight)
    {
        nStripHeight = GetRasterYSize() - nStripWithinBand * m_nRowsPerStrip;
        cc = (cc / m_nRowsPerStrip) * nStripHeight;
        CPLDebug("GTiff",
                 "Adjusted bytes to write from " CPL_FRMT_GUIB
                 " to " CPL_FRMT_GUIB ".",
                 static_cast<GUIntBig>(TIFFStripSize(m_hTIFF)),
                 static_cast<GUIntBig>(cc));
    }

    /* -------------------------------------------------------------------- */
    /*      Don't write empty blocks in some cases.                         */
    /* -------------------------------------------------------------------- */
    if (!m_bWriteEmptyTiles && IsFirstPixelEqualToNoData(pabyData))
    {
        if (!IsBlockAvailable(strip))
        {
            const int nComponents =
                m_nPlanarConfig == PLANARCONFIG_CONTIG ? nBands : 1;

            if (HasOnlyNoData(pabyData, m_nBlockXSize, nStripHeight,
                              m_nBlockXSize, nComponents))
            {
                return true;
            }
        }
    }

    /* -------------------------------------------------------------------- */
    /*      TIFFWriteEncodedStrip can alter the passed buffer if            */
    /*      byte-swapping is necessary so we use a temporary buffer         */
    /*      before calling it.                                              */
    /* -------------------------------------------------------------------- */
    if (bPreserveDataBuffer &&
        (TIFFIsByteSwapped(m_hTIFF) || m_panMaskOffsetLsb))
    {
        if (m_pabyTempWriteBuffer == nullptr)
        {
            m_pabyTempWriteBuffer = CPLMalloc(ccFull);
        }
        memcpy(m_pabyTempWriteBuffer, pabyData, cc);
        pabyData = static_cast<GByte *>(m_pabyTempWriteBuffer);
    }

    if (m_panMaskOffsetLsb)
    {
        int iBand = m_nPlanarConfig == PLANARCONFIG_SEPARATE
                        ? static_cast<int>(strip) / m_nBlocksPerBand
                        : -1;
        DiscardLsb(pabyData, cc, iBand);
    }

    if (m_bStreamingOut)
    {
        if (strip != static_cast<uint32_t>(m_nLastWrittenBlockId + 1))
        {
            ReportError(CE_Failure, CPLE_NotSupported,
                        "Attempt to write block %d whereas %d was expected",
                        strip, m_nLastWrittenBlockId + 1);
            return false;
        }
        if (static_cast<GPtrDiff_t>(VSIFWriteL(pabyData, 1, cc, m_fpToWrite)) !=
            cc)
        {
            ReportError(CE_Failure, CPLE_FileIO,
                        "Could not write " CPL_FRMT_GUIB " bytes",
                        static_cast<GUIntBig>(cc));
            return false;
        }
        m_nLastWrittenBlockId = strip;
        return true;
    }

    /* -------------------------------------------------------------------- */
    /*      Should we do compression in a worker thread ?                   */
    /* -------------------------------------------------------------------- */
    if (SubmitCompressionJob(strip, pabyData, cc, nStripHeight))
        return true;

        // libtiff 4.0.6 or older do not always properly report write errors.
#if TIFFLIB_VERSION <= 20150912
    CPLErr eBefore = CPLGetLastErrorType();
#endif
    bool bRet = TIFFWriteEncodedStrip(m_hTIFF, strip, pabyData, cc) == cc;
#if TIFFLIB_VERSION <= 20150912
    if (eBefore == CE_None && CPLGetLastErrorType() == CE_Failure)
        bRet = FALSE;
#endif
    return bRet;
}

/************************************************************************/
/*                        InitCompressionThreads()                      */
/************************************************************************/

void GTiffDataset::InitCompressionThreads(bool bUpdateMode,
                                          CSLConstList papszOptions)
{
    // Raster == tile, then no need for threads
    if (m_nBlockXSize == nRasterXSize && m_nBlockYSize == nRasterYSize)
        return;

    const char *pszValue = CSLFetchNameValue(papszOptions, "NUM_THREADS");
    if (pszValue == nullptr)
        pszValue = CPLGetConfigOption("GDAL_NUM_THREADS", nullptr);
    if (pszValue)
    {
        int nThreads =
            EQUAL(pszValue, "ALL_CPUS") ? CPLGetNumCPUs() : atoi(pszValue);
        if (nThreads > 1024)
            nThreads = 1024;  // to please Coverity
        if (nThreads > 1)
        {
            if ((bUpdateMode && m_nCompression != COMPRESSION_NONE)
#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
                || (nBands >= 1 && IsMultiThreadedReadCompatible())
#endif
            )
            {
                CPLDebug("GTiff",
                         "Using up to %d threads for compression/decompression",
                         nThreads);

                m_poThreadPool = GDALGetGlobalThreadPool(nThreads);
                if (bUpdateMode && m_poThreadPool)
                    m_poCompressQueue = m_poThreadPool->CreateJobQueue();

                if (m_poCompressQueue != nullptr)
                {
                    // Add a margin of an extra job w.r.t thread number
                    // so as to optimize compression time (enables the main
                    // thread to do boring I/O while all CPUs are working).
                    m_asCompressionJobs.resize(nThreads + 1);
                    memset(&m_asCompressionJobs[0], 0,
                           m_asCompressionJobs.size() *
                               sizeof(GTiffCompressionJob));
                    for (int i = 0;
                         i < static_cast<int>(m_asCompressionJobs.size()); ++i)
                    {
                        m_asCompressionJobs[i].pszTmpFilename =
                            CPLStrdup(CPLSPrintf("/vsimem/gtiff/thread/job/%p",
                                                 &m_asCompressionJobs[i]));
                        m_asCompressionJobs[i].nStripOrTile = -1;
                    }
                    m_hCompressThreadPoolMutex = CPLCreateMutex();
                    CPLReleaseMutex(m_hCompressThreadPoolMutex);

                    // This is kind of a hack, but basically using
                    // TIFFWriteRawStrip/Tile and then TIFFReadEncodedStrip/Tile
                    // does not work on a newly created file, because
                    // TIFF_MYBUFFER is not set in tif_flags
                    // (if using TIFFWriteEncodedStrip/Tile first,
                    // TIFFWriteBufferSetup() is automatically called).
                    // This should likely rather fixed in libtiff itself.
                    CPL_IGNORE_RET_VAL(
                        TIFFWriteBufferSetup(m_hTIFF, nullptr, -1));
                }
            }
        }
        else if (nThreads < 0 ||
                 (!EQUAL(pszValue, "0") && !EQUAL(pszValue, "1") &&
                  !EQUAL(pszValue, "ALL_CPUS")))
        {
            ReportError(CE_Warning, CPLE_AppDefined,
                        "Invalid value for NUM_THREADS: %s", pszValue);
        }
    }
}

/************************************************************************/
/*                       GetGTIFFKeysFlavor()                           */
/************************************************************************/

static GTIFFKeysFlavorEnum GetGTIFFKeysFlavor(CSLConstList papszOptions)
{
    const char *pszGeoTIFFKeysFlavor =
        CSLFetchNameValueDef(papszOptions, "GEOTIFF_KEYS_FLAVOR", "STANDARD");
    if (EQUAL(pszGeoTIFFKeysFlavor, "ESRI_PE"))
        return GEOTIFF_KEYS_ESRI_PE;
    return GEOTIFF_KEYS_STANDARD;
}

/************************************************************************/
/*                       GetGeoTIFFVersion()                            */
/************************************************************************/

static GeoTIFFVersionEnum GetGeoTIFFVersion(CSLConstList papszOptions)
{
    const char *pszVersion =
        CSLFetchNameValueDef(papszOptions, "GEOTIFF_VERSION", "AUTO");
    if (EQUAL(pszVersion, "1.0"))
        return GEOTIFF_VERSION_1_0;
    if (EQUAL(pszVersion, "1.1"))
        return GEOTIFF_VERSION_1_1;
    return GEOTIFF_VERSION_AUTO;
}

/************************************************************************/
/*                      InitCreationOrOpenOptions()                     */
/************************************************************************/

void GTiffDataset::InitCreationOrOpenOptions(bool bUpdateMode,
                                             CSLConstList papszOptions)
{
    InitCompressionThreads(bUpdateMode, papszOptions);

    m_eGeoTIFFKeysFlavor = GetGTIFFKeysFlavor(papszOptions);
    m_eGeoTIFFVersion = GetGeoTIFFVersion(papszOptions);
}

/************************************************************************/
/*                      ThreadCompressionFunc()                         */
/************************************************************************/

void GTiffDataset::ThreadCompressionFunc(void *pData)
{
    GTiffCompressionJob *psJob = static_cast<GTiffCompressionJob *>(pData);
    GTiffDataset *poDS = psJob->poDS;

    VSILFILE *fpTmp = VSIFOpenL(psJob->pszTmpFilename, "wb+");
    TIFF *hTIFFTmp = VSI_TIFFOpen(
        psJob->pszTmpFilename, psJob->bTIFFIsBigEndian ? "wb+" : "wl+", fpTmp);
    CPLAssert(hTIFFTmp != nullptr);
    TIFFSetField(hTIFFTmp, TIFFTAG_IMAGEWIDTH, poDS->m_nBlockXSize);
    TIFFSetField(hTIFFTmp, TIFFTAG_IMAGELENGTH, psJob->nHeight);
    TIFFSetField(hTIFFTmp, TIFFTAG_BITSPERSAMPLE, poDS->m_nBitsPerSample);
    TIFFSetField(hTIFFTmp, TIFFTAG_COMPRESSION, poDS->m_nCompression);
    TIFFSetField(hTIFFTmp, TIFFTAG_PHOTOMETRIC, poDS->m_nPhotometric);
    TIFFSetField(hTIFFTmp, TIFFTAG_SAMPLEFORMAT, poDS->m_nSampleFormat);
    TIFFSetField(hTIFFTmp, TIFFTAG_SAMPLESPERPIXEL, poDS->m_nSamplesPerPixel);
    TIFFSetField(hTIFFTmp, TIFFTAG_ROWSPERSTRIP, poDS->m_nBlockYSize);
    TIFFSetField(hTIFFTmp, TIFFTAG_PLANARCONFIG, poDS->m_nPlanarConfig);
    if (psJob->nPredictor != PREDICTOR_NONE)
        TIFFSetField(hTIFFTmp, TIFFTAG_PREDICTOR, psJob->nPredictor);
    if (poDS->m_nCompression == COMPRESSION_LERC)
    {
        TIFFSetField(hTIFFTmp, TIFFTAG_LERC_PARAMETERS, 2,
                     poDS->m_anLercAddCompressionAndVersion);
    }

    poDS->RestoreVolatileParameters(hTIFFTmp);

    bool bOK = TIFFWriteEncodedStrip(hTIFFTmp, 0, psJob->pabyBuffer,
                                     psJob->nBufferSize) == psJob->nBufferSize;

    toff_t nOffset = 0;
    if (bOK)
    {
        toff_t *panOffsets = nullptr;
        toff_t *panByteCounts = nullptr;
        TIFFGetField(hTIFFTmp, TIFFTAG_STRIPOFFSETS, &panOffsets);
        TIFFGetField(hTIFFTmp, TIFFTAG_STRIPBYTECOUNTS, &panByteCounts);

        nOffset = panOffsets[0];
        psJob->nCompressedBufferSize =
            static_cast<GPtrDiff_t>(panByteCounts[0]);
    }
    else
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Error when compressing strip/tile %d", psJob->nStripOrTile);
    }

    XTIFFClose(hTIFFTmp);
    if (VSIFCloseL(fpTmp) != 0)
    {
        if (bOK)
        {
            bOK = false;
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Error when compressing strip/tile %d",
                     psJob->nStripOrTile);
        }
    }

    if (bOK)
    {
        vsi_l_offset nFileSize = 0;
        GByte *pabyCompressedBuffer =
            VSIGetMemFileBuffer(psJob->pszTmpFilename, &nFileSize, FALSE);
        CPLAssert(static_cast<vsi_l_offset>(
                      nOffset + psJob->nCompressedBufferSize) <= nFileSize);
        psJob->pabyCompressedBuffer = pabyCompressedBuffer + nOffset;
    }
    else
    {
        psJob->pabyCompressedBuffer = nullptr;
        psJob->nCompressedBufferSize = 0;
    }

    auto mutex = poDS->m_poBaseDS ? poDS->m_poBaseDS->m_hCompressThreadPoolMutex
                                  : poDS->m_hCompressThreadPoolMutex;
    if (mutex)
    {
        CPLAcquireMutex(mutex, 1000.0);
        psJob->bReady = true;
        CPLReleaseMutex(mutex);
    }
}

/************************************************************************/
/*                        WriteRawStripOrTile()                         */
/************************************************************************/

void GTiffDataset::WriteRawStripOrTile(int nStripOrTile,
                                       GByte *pabyCompressedBuffer,
                                       GPtrDiff_t nCompressedBufferSize)
{
#ifdef DEBUG_VERBOSE
    CPLDebug("GTIFF", "Writing raw strip/tile %d, size " CPL_FRMT_GUIB,
             nStripOrTile, static_cast<GUIntBig>(nCompressedBufferSize));
#endif
    toff_t *panOffsets = nullptr;
    toff_t *panByteCounts = nullptr;
    bool bWriteAtEnd = true;
    bool bWriteLeader = m_bLeaderSizeAsUInt4;
    bool bWriteTrailer = m_bTrailerRepeatedLast4BytesRepeated;
    if (TIFFGetField(m_hTIFF,
                     TIFFIsTiled(m_hTIFF) ? TIFFTAG_TILEOFFSETS
                                          : TIFFTAG_STRIPOFFSETS,
                     &panOffsets) &&
        panOffsets != nullptr && panOffsets[nStripOrTile] != 0)
    {
        // Forces TIFFAppendStrip() to consider if the location of the
        // tile/strip can be reused or if the strile should be written at end of
        // file.
        TIFFSetWriteOffset(m_hTIFF, 0);

        if (m_bBlockOrderRowMajor)
        {
            if (TIFFGetField(m_hTIFF,
                             TIFFIsTiled(m_hTIFF) ? TIFFTAG_TILEBYTECOUNTS
                                                  : TIFFTAG_STRIPBYTECOUNTS,
                             &panByteCounts) &&
                panByteCounts != nullptr)
            {
                if (static_cast<GUIntBig>(nCompressedBufferSize) >
                    panByteCounts[nStripOrTile])
                {
                    GTiffDataset *poRootDS = m_poBaseDS ? m_poBaseDS : this;
                    if (!poRootDS->m_bKnownIncompatibleEdition &&
                        !poRootDS->m_bWriteKnownIncompatibleEdition)
                    {
                        ReportError(
                            CE_Warning, CPLE_AppDefined,
                            "A strile cannot be rewritten in place, which "
                            "invalidates the BLOCK_ORDER optimization.");
                        poRootDS->m_bKnownIncompatibleEdition = true;
                        poRootDS->m_bWriteKnownIncompatibleEdition = true;
                    }
                }
                // For mask interleaving, if the size is not exactly the same,
                // completely give up (we could potentially move the mask in
                // case the imagery is smaller)
                else if (m_poMaskDS && m_bMaskInterleavedWithImagery &&
                         static_cast<GUIntBig>(nCompressedBufferSize) !=
                             panByteCounts[nStripOrTile])
                {
                    GTiffDataset *poRootDS = m_poBaseDS ? m_poBaseDS : this;
                    if (!poRootDS->m_bKnownIncompatibleEdition &&
                        !poRootDS->m_bWriteKnownIncompatibleEdition)
                    {
                        ReportError(
                            CE_Warning, CPLE_AppDefined,
                            "A strile cannot be rewritten in place, which "
                            "invalidates the MASK_INTERLEAVED_WITH_IMAGERY "
                            "optimization.");
                        poRootDS->m_bKnownIncompatibleEdition = true;
                        poRootDS->m_bWriteKnownIncompatibleEdition = true;
                    }
                    bWriteLeader = false;
                    bWriteTrailer = false;
                    if (m_bLeaderSizeAsUInt4)
                    {
                        // If there was a valid leader, invalidat it
                        VSI_TIFFSeek(m_hTIFF, panOffsets[nStripOrTile] - 4,
                                     SEEK_SET);
                        uint32_t nOldSize;
                        VSIFReadL(&nOldSize, 1, 4,
                                  VSI_TIFFGetVSILFile(TIFFClientdata(m_hTIFF)));
                        CPL_LSBPTR32(&nOldSize);
                        if (nOldSize == panByteCounts[nStripOrTile])
                        {
                            uint32_t nInvalidatedSize = 0;
                            VSI_TIFFSeek(m_hTIFF, panOffsets[nStripOrTile] - 4,
                                         SEEK_SET);
                            VSI_TIFFWrite(m_hTIFF, &nInvalidatedSize,
                                          sizeof(nInvalidatedSize));
                        }
                    }
                }
                else
                {
                    bWriteAtEnd = false;
                }
            }
        }
    }
    if (bWriteLeader &&
        static_cast<GUIntBig>(nCompressedBufferSize) <= 0xFFFFFFFFU)
    {
        // cppcheck-suppress knownConditionTrueFalse
        if (bWriteAtEnd)
        {
            VSI_TIFFSeek(m_hTIFF, 0, SEEK_END);
        }
        else
        {
            // If we rewrite an existing strile in place with an existing
            // leader, check that the leader is valid, before rewriting it. And
            // if it is not valid, then do not write the trailer, as we could
            // corrupt other data.
            VSI_TIFFSeek(m_hTIFF, panOffsets[nStripOrTile] - 4, SEEK_SET);
            uint32_t nOldSize;
            VSIFReadL(&nOldSize, 1, 4,
                      VSI_TIFFGetVSILFile(TIFFClientdata(m_hTIFF)));
            CPL_LSBPTR32(&nOldSize);
            bWriteLeader =
                panByteCounts && nOldSize == panByteCounts[nStripOrTile];
            bWriteTrailer = bWriteLeader;
            VSI_TIFFSeek(m_hTIFF, panOffsets[nStripOrTile] - 4, SEEK_SET);
        }
        // cppcheck-suppress knownConditionTrueFalse
        if (bWriteLeader)
        {
            uint32_t nSize = static_cast<uint32_t>(nCompressedBufferSize);
            CPL_LSBPTR32(&nSize);
            if (!VSI_TIFFWrite(m_hTIFF, &nSize, sizeof(nSize)))
                m_bWriteError = true;
        }
    }
    tmsize_t written;
    if (TIFFIsTiled(m_hTIFF))
        written = TIFFWriteRawTile(m_hTIFF, nStripOrTile, pabyCompressedBuffer,
                                   nCompressedBufferSize);
    else
        written = TIFFWriteRawStrip(m_hTIFF, nStripOrTile, pabyCompressedBuffer,
                                    nCompressedBufferSize);
    if (written != nCompressedBufferSize)
        m_bWriteError = true;
    if (bWriteTrailer &&
        static_cast<GUIntBig>(nCompressedBufferSize) <= 0xFFFFFFFFU)
    {
        GByte abyLastBytes[4] = {};
        if (nCompressedBufferSize >= 4)
            memcpy(abyLastBytes,
                   pabyCompressedBuffer + nCompressedBufferSize - 4, 4);
        else
            memcpy(abyLastBytes, pabyCompressedBuffer, nCompressedBufferSize);
        if (!VSI_TIFFWrite(m_hTIFF, abyLastBytes, 4))
            m_bWriteError = true;
    }
}

/************************************************************************/
/*                        WaitCompletionForJobIdx()                     */
/************************************************************************/

void GTiffDataset::WaitCompletionForJobIdx(int i)
{
    auto poQueue = m_poBaseDS ? m_poBaseDS->m_poCompressQueue.get()
                              : m_poCompressQueue.get();
    auto &oQueue = m_poBaseDS ? m_poBaseDS->m_asQueueJobIdx : m_asQueueJobIdx;
    auto &asJobs =
        m_poBaseDS ? m_poBaseDS->m_asCompressionJobs : m_asCompressionJobs;
    auto mutex = m_poBaseDS ? m_poBaseDS->m_hCompressThreadPoolMutex
                            : m_hCompressThreadPoolMutex;

    CPLAssert(i >= 0 && static_cast<size_t>(i) < asJobs.size());
    CPLAssert(asJobs[i].nStripOrTile >= 0);
    CPLAssert(!oQueue.empty());

    bool bHasWarned = false;
    while (true)
    {
        CPLAcquireMutex(mutex, 1000.0);
        const bool bReady = asJobs[i].bReady;
        CPLReleaseMutex(mutex);
        if (!bReady)
        {
            if (!bHasWarned)
            {
                CPLDebug("GTIFF",
                         "Waiting for worker job to finish handling block %d",
                         asJobs[i].nStripOrTile);
                bHasWarned = true;
            }
            poQueue->GetPool()->WaitEvent();
        }
        else
        {
            break;
        }
    }

    if (asJobs[i].nCompressedBufferSize)
    {
        asJobs[i].poDS->WriteRawStripOrTile(asJobs[i].nStripOrTile,
                                            asJobs[i].pabyCompressedBuffer,
                                            asJobs[i].nCompressedBufferSize);
    }
    asJobs[i].pabyCompressedBuffer = nullptr;
    asJobs[i].nBufferSize = 0;
    asJobs[i].bReady = false;
    asJobs[i].nStripOrTile = -1;
    oQueue.pop();
}

/************************************************************************/
/*                        WaitCompletionForBlock()                      */
/************************************************************************/

void GTiffDataset::WaitCompletionForBlock(int nBlockId)
{
    auto poQueue = m_poBaseDS ? m_poBaseDS->m_poCompressQueue.get()
                              : m_poCompressQueue.get();
    auto &oQueue = m_poBaseDS ? m_poBaseDS->m_asQueueJobIdx : m_asQueueJobIdx;
    auto &asJobs =
        m_poBaseDS ? m_poBaseDS->m_asCompressionJobs : m_asCompressionJobs;

    if (poQueue != nullptr && !oQueue.empty())
    {
        for (int i = 0; i < static_cast<int>(asJobs.size()); ++i)
        {
            if (asJobs[i].poDS == this && asJobs[i].nStripOrTile == nBlockId)
            {
                while (!oQueue.empty() &&
                       !(asJobs[oQueue.front()].poDS == this &&
                         asJobs[oQueue.front()].nStripOrTile == nBlockId))
                {
                    WaitCompletionForJobIdx(oQueue.front());
                }
                CPLAssert(!oQueue.empty() &&
                          asJobs[oQueue.front()].poDS == this &&
                          asJobs[oQueue.front()].nStripOrTile == nBlockId);
                WaitCompletionForJobIdx(oQueue.front());
            }
        }
    }
}

/************************************************************************/
/*                      SubmitCompressionJob()                          */
/************************************************************************/

bool GTiffDataset::SubmitCompressionJob(int nStripOrTile, GByte *pabyData,
                                        GPtrDiff_t cc, int nHeight)
{
    /* -------------------------------------------------------------------- */
    /*      Should we do compression in a worker thread ?                   */
    /* -------------------------------------------------------------------- */
    auto poQueue = m_poBaseDS ? m_poBaseDS->m_poCompressQueue.get()
                              : m_poCompressQueue.get();

    if (poQueue && m_nCompression == COMPRESSION_NONE)
    {
        // We don't do multi-threaded compression for uncompressed...
        // but we must wait for other related compression tasks (e.g mask)
        // to be completed
        poQueue->WaitCompletion();

        // Flush remaining data
        auto &oQueue =
            m_poBaseDS ? m_poBaseDS->m_asQueueJobIdx : m_asQueueJobIdx;
        while (!oQueue.empty())
        {
            WaitCompletionForJobIdx(oQueue.front());
        }
    }

    if (poQueue == nullptr || !(m_nCompression == COMPRESSION_ADOBE_DEFLATE ||
                                m_nCompression == COMPRESSION_LZW ||
                                m_nCompression == COMPRESSION_PACKBITS ||
                                m_nCompression == COMPRESSION_LZMA ||
                                m_nCompression == COMPRESSION_ZSTD ||
                                m_nCompression == COMPRESSION_LERC ||
                                m_nCompression == COMPRESSION_JXL ||
                                m_nCompression == COMPRESSION_WEBP ||
                                m_nCompression == COMPRESSION_JPEG))
    {
        if (m_bBlockOrderRowMajor || m_bLeaderSizeAsUInt4 ||
            m_bTrailerRepeatedLast4BytesRepeated)
        {
            GTiffCompressionJob sJob;
            memset(&sJob, 0, sizeof(sJob));
            sJob.poDS = this;
            sJob.pszTmpFilename =
                CPLStrdup(CPLSPrintf("/vsimem/gtiff/%p", this));
            sJob.bTIFFIsBigEndian = CPL_TO_BOOL(TIFFIsBigEndian(m_hTIFF));
            sJob.pabyBuffer =
                static_cast<GByte *>(CPLRealloc(sJob.pabyBuffer, cc));
            memcpy(sJob.pabyBuffer, pabyData, cc);
            sJob.nBufferSize = cc;
            sJob.nHeight = nHeight;
            sJob.nStripOrTile = nStripOrTile;
            sJob.nPredictor = PREDICTOR_NONE;
            if (GTIFFSupportsPredictor(m_nCompression))
            {
                TIFFGetField(m_hTIFF, TIFFTAG_PREDICTOR, &sJob.nPredictor);
            }

            ThreadCompressionFunc(&sJob);

            if (sJob.nCompressedBufferSize)
            {
                sJob.poDS->WriteRawStripOrTile(sJob.nStripOrTile,
                                               sJob.pabyCompressedBuffer,
                                               sJob.nCompressedBufferSize);
            }

            CPLFree(sJob.pabyBuffer);
            VSIUnlink(sJob.pszTmpFilename);
            CPLFree(sJob.pszTmpFilename);
            return sJob.nCompressedBufferSize > 0 && !m_bWriteError;
        }

        return false;
    }

    auto &oQueue = m_poBaseDS ? m_poBaseDS->m_asQueueJobIdx : m_asQueueJobIdx;
    auto &asJobs =
        m_poBaseDS ? m_poBaseDS->m_asCompressionJobs : m_asCompressionJobs;

    int nNextCompressionJobAvail = -1;

    if (oQueue.size() == asJobs.size())
    {
        CPLAssert(!oQueue.empty());
        nNextCompressionJobAvail = oQueue.front();
        WaitCompletionForJobIdx(nNextCompressionJobAvail);
    }
    else
    {
        const int nJobs = static_cast<int>(asJobs.size());
        for (int i = 0; i < nJobs; ++i)
        {
            if (asJobs[i].nBufferSize == 0)
            {
                nNextCompressionJobAvail = i;
                break;
            }
        }
    }
    CPLAssert(nNextCompressionJobAvail >= 0);

    GTiffCompressionJob *psJob = &asJobs[nNextCompressionJobAvail];
    psJob->poDS = this;
    psJob->bTIFFIsBigEndian = CPL_TO_BOOL(TIFFIsBigEndian(m_hTIFF));
    psJob->pabyBuffer = static_cast<GByte *>(CPLRealloc(psJob->pabyBuffer, cc));
    memcpy(psJob->pabyBuffer, pabyData, cc);
    psJob->nBufferSize = cc;
    psJob->nHeight = nHeight;
    psJob->nStripOrTile = nStripOrTile;
    psJob->nPredictor = PREDICTOR_NONE;
    if (GTIFFSupportsPredictor(m_nCompression))
    {
        TIFFGetField(m_hTIFF, TIFFTAG_PREDICTOR, &psJob->nPredictor);
    }

    poQueue->SubmitJob(ThreadCompressionFunc, psJob);
    oQueue.push(nNextCompressionJobAvail);

    return true;
}

/************************************************************************/
/*                          DiscardLsb()                                */
/************************************************************************/

template <class T> bool MustNotDiscardLsb(T value, bool bHasNoData, T nodata)
{
    return bHasNoData && value == nodata;
}

template <>
bool MustNotDiscardLsb<float>(float value, bool bHasNoData, float nodata)
{
    return (bHasNoData && value == nodata) || !std::isfinite(value);
}

template <>
bool MustNotDiscardLsb<double>(double value, bool bHasNoData, double nodata)
{
    return (bHasNoData && value == nodata) || !std::isfinite(value);
}

template <class T> T AdjustValue(T value, uint64_t nRoundUpBitTest);

template <class T> T AdjustValueInt(T value, uint64_t nRoundUpBitTest)
{
    if (value >=
        static_cast<T>(std::numeric_limits<T>::max() - (nRoundUpBitTest << 1)))
        return static_cast<T>(value - (nRoundUpBitTest << 1));
    return static_cast<T>(value + (nRoundUpBitTest << 1));
}

template <> int8_t AdjustValue<int8_t>(int8_t value, uint64_t nRoundUpBitTest)
{
    return AdjustValueInt(value, nRoundUpBitTest);
}

template <>
uint8_t AdjustValue<uint8_t>(uint8_t value, uint64_t nRoundUpBitTest)
{
    return AdjustValueInt(value, nRoundUpBitTest);
}

template <>
int16_t AdjustValue<int16_t>(int16_t value, uint64_t nRoundUpBitTest)
{
    return AdjustValueInt(value, nRoundUpBitTest);
}

template <>
uint16_t AdjustValue<uint16_t>(uint16_t value, uint64_t nRoundUpBitTest)
{
    return AdjustValueInt(value, nRoundUpBitTest);
}

template <>
int32_t AdjustValue<int32_t>(int32_t value, uint64_t nRoundUpBitTest)
{
    return AdjustValueInt(value, nRoundUpBitTest);
}

template <>
uint32_t AdjustValue<uint32_t>(uint32_t value, uint64_t nRoundUpBitTest)
{
    return AdjustValueInt(value, nRoundUpBitTest);
}

template <>
int64_t AdjustValue<int64_t>(int64_t value, uint64_t nRoundUpBitTest)
{
    return AdjustValueInt(value, nRoundUpBitTest);
}

template <>
uint64_t AdjustValue<uint64_t>(uint64_t value, uint64_t nRoundUpBitTest)
{
    return AdjustValueInt(value, nRoundUpBitTest);
}

template <> float AdjustValue<float>(float value, uint64_t)
{
    return std::nextafter(value, std::numeric_limits<float>::max());
}

template <> double AdjustValue<double>(double value, uint64_t)
{
    return std::nextafter(value, std::numeric_limits<double>::max());
}

template <class Teffective, class T>
T RoundValueDiscardLsb(const void *ptr, uint64_t nMask,
                       uint64_t nRoundUpBitTest);

template <class T>
T RoundValueDiscardLsbUnsigned(const void *ptr, uint64_t nMask,
                               uint64_t nRoundUpBitTest)
{
    if ((*reinterpret_cast<const T *>(ptr) & nMask) >
        static_cast<uint64_t>(std::numeric_limits<T>::max()) -
            (nRoundUpBitTest << 1U))
    {
        return static_cast<T>(std::numeric_limits<T>::max() & nMask);
    }
    const uint64_t newval =
        (*reinterpret_cast<const T *>(ptr) & nMask) + (nRoundUpBitTest << 1U);
    return static_cast<T>(newval);
}

template <class T>
T RoundValueDiscardLsbSigned(const void *ptr, uint64_t nMask,
                             uint64_t nRoundUpBitTest)
{
    T oldval = *reinterpret_cast<const T *>(ptr);
    if (oldval < 0)
    {
        return static_cast<T>(oldval & nMask);
    }
    const uint64_t newval =
        (*reinterpret_cast<const T *>(ptr) & nMask) + (nRoundUpBitTest << 1U);
    if (newval > static_cast<uint64_t>(std::numeric_limits<T>::max()))
        return static_cast<T>(std::numeric_limits<T>::max() & nMask);
    return static_cast<T>(newval);
}

template <>
uint16_t RoundValueDiscardLsb<uint16_t, uint16_t>(const void *ptr,
                                                  uint64_t nMask,
                                                  uint64_t nRoundUpBitTest)
{
    return RoundValueDiscardLsbUnsigned<uint16_t>(ptr, nMask, nRoundUpBitTest);
}

template <>
uint32_t RoundValueDiscardLsb<uint32_t, uint32_t>(const void *ptr,
                                                  uint64_t nMask,
                                                  uint64_t nRoundUpBitTest)
{
    return RoundValueDiscardLsbUnsigned<uint32_t>(ptr, nMask, nRoundUpBitTest);
}

template <>
uint64_t RoundValueDiscardLsb<uint64_t, uint64_t>(const void *ptr,
                                                  uint64_t nMask,
                                                  uint64_t nRoundUpBitTest)
{
    return RoundValueDiscardLsbUnsigned<uint64_t>(ptr, nMask, nRoundUpBitTest);
}
template <>
int8_t RoundValueDiscardLsb<int8_t, int8_t>(const void *ptr, uint64_t nMask,
                                            uint64_t nRoundUpBitTest)
{
    return RoundValueDiscardLsbSigned<int8_t>(ptr, nMask, nRoundUpBitTest);
}

template <>
int16_t RoundValueDiscardLsb<int16_t, int16_t>(const void *ptr, uint64_t nMask,
                                               uint64_t nRoundUpBitTest)
{
    return RoundValueDiscardLsbSigned<int16_t>(ptr, nMask, nRoundUpBitTest);
}

template <>
int32_t RoundValueDiscardLsb<int32_t, int32_t>(const void *ptr, uint64_t nMask,
                                               uint64_t nRoundUpBitTest)
{
    return RoundValueDiscardLsbSigned<int32_t>(ptr, nMask, nRoundUpBitTest);
}

template <>
int64_t RoundValueDiscardLsb<int64_t, int64_t>(const void *ptr, uint64_t nMask,
                                               uint64_t nRoundUpBitTest)
{
    return RoundValueDiscardLsbSigned<int64_t>(ptr, nMask, nRoundUpBitTest);
}

template <>
uint32_t RoundValueDiscardLsb<float, uint32_t>(const void *ptr, uint64_t nMask,
                                               uint64_t nRoundUpBitTest)
{
    return RoundValueDiscardLsbUnsigned<uint32_t>(ptr, nMask, nRoundUpBitTest);
}

template <>
uint64_t RoundValueDiscardLsb<double, uint64_t>(const void *ptr, uint64_t nMask,
                                                uint64_t nRoundUpBitTest)
{
    return RoundValueDiscardLsbUnsigned<uint64_t>(ptr, nMask, nRoundUpBitTest);
}

template <class Teffective, class T>
static void DiscardLsbT(GByte *pabyBuffer, size_t nBytes, int iBand, int nBands,
                        uint16_t nPlanarConfig,
                        const GTiffDataset::MaskOffset *panMaskOffsetLsb,
                        bool bHasNoData, Teffective nNoDataValue)
{
    static_assert(sizeof(Teffective) == sizeof(T),
                  "sizeof(Teffective) == sizeof(T)");
    if (nPlanarConfig == PLANARCONFIG_SEPARATE)
    {
        const auto nMask = panMaskOffsetLsb[iBand].nMask;
        const auto nRoundUpBitTest = panMaskOffsetLsb[iBand].nRoundUpBitTest;
        for (size_t i = 0; i < nBytes / sizeof(T); ++i)
        {
            if (MustNotDiscardLsb(reinterpret_cast<Teffective *>(pabyBuffer)[i],
                                  bHasNoData, nNoDataValue))
            {
                continue;
            }

            if (reinterpret_cast<T *>(pabyBuffer)[i] & nRoundUpBitTest)
            {
                reinterpret_cast<T *>(pabyBuffer)[i] =
                    RoundValueDiscardLsb<Teffective, T>(
                        &(reinterpret_cast<T *>(pabyBuffer)[i]), nMask,
                        nRoundUpBitTest);
            }
            else
            {
                reinterpret_cast<T *>(pabyBuffer)[i] = static_cast<T>(
                    reinterpret_cast<T *>(pabyBuffer)[i] & nMask);
            }

            // Make sure that by discarding LSB we don't end up to a value
            // that is no the nodata value
            if (MustNotDiscardLsb(reinterpret_cast<Teffective *>(pabyBuffer)[i],
                                  bHasNoData, nNoDataValue))
            {
                reinterpret_cast<Teffective *>(pabyBuffer)[i] =
                    AdjustValue(nNoDataValue, nRoundUpBitTest);
            }
        }
    }
    else
    {
        for (size_t i = 0; i < nBytes / sizeof(T); i += nBands)
        {
            for (int j = 0; j < nBands; ++j)
            {
                if (MustNotDiscardLsb(
                        reinterpret_cast<Teffective *>(pabyBuffer)[i + j],
                        bHasNoData, nNoDataValue))
                {
                    continue;
                }

                if (reinterpret_cast<T *>(pabyBuffer)[i + j] &
                    panMaskOffsetLsb[j].nRoundUpBitTest)
                {
                    reinterpret_cast<T *>(pabyBuffer)[i + j] =
                        RoundValueDiscardLsb<Teffective, T>(
                            &(reinterpret_cast<T *>(pabyBuffer)[i + j]),
                            panMaskOffsetLsb[j].nMask,
                            panMaskOffsetLsb[j].nRoundUpBitTest);
                }
                else
                {
                    reinterpret_cast<T *>(pabyBuffer)[i + j] = static_cast<T>(
                        (reinterpret_cast<T *>(pabyBuffer)[i + j] &
                         panMaskOffsetLsb[j].nMask));
                }

                // Make sure that by discarding LSB we don't end up to a value
                // that is no the nodata value
                if (MustNotDiscardLsb(
                        reinterpret_cast<Teffective *>(pabyBuffer)[i + j],
                        bHasNoData, nNoDataValue))
                {
                    reinterpret_cast<Teffective *>(pabyBuffer)[i + j] =
                        AdjustValue(nNoDataValue,
                                    panMaskOffsetLsb[j].nRoundUpBitTest);
                }
            }
        }
    }
}

static void DiscardLsb(GByte *pabyBuffer, GPtrDiff_t nBytes, int iBand,
                       int nBands, uint16_t nSampleFormat,
                       uint16_t nBitsPerSample, uint16_t nPlanarConfig,
                       const GTiffDataset::MaskOffset *panMaskOffsetLsb,
                       bool bHasNoData, double dfNoDataValue)
{
    if (nBitsPerSample == 8 && nSampleFormat == SAMPLEFORMAT_UINT)
    {
        uint8_t nNoDataValue = 0;
        if (bHasNoData &&
            dfNoDataValue >= std::numeric_limits<uint8_t>::min() &&
            dfNoDataValue <= std::numeric_limits<uint8_t>::max() &&
            dfNoDataValue == static_cast<uint8_t>(dfNoDataValue))
        {
            nNoDataValue = static_cast<uint8_t>(dfNoDataValue);
        }
        else
        {
            bHasNoData = false;
        }
        if (nPlanarConfig == PLANARCONFIG_SEPARATE)
        {
            const auto nMask =
                static_cast<unsigned>(panMaskOffsetLsb[iBand].nMask);
            const auto nRoundUpBitTest =
                static_cast<unsigned>(panMaskOffsetLsb[iBand].nRoundUpBitTest);
            for (decltype(nBytes) i = 0; i < nBytes; ++i)
            {
                if (bHasNoData && pabyBuffer[i] == nNoDataValue)
                    continue;

                // Keep 255 in case it is alpha.
                if (pabyBuffer[i] != 255)
                {
                    if (pabyBuffer[i] & nRoundUpBitTest)
                        pabyBuffer[i] = static_cast<GByte>(
                            std::min(255U, (pabyBuffer[i] & nMask) +
                                               (nRoundUpBitTest << 1U)));
                    else
                        pabyBuffer[i] =
                            static_cast<GByte>(pabyBuffer[i] & nMask);

                    // Make sure that by discarding LSB we don't end up to a
                    // value that is no the nodata value
                    if (bHasNoData && pabyBuffer[i] == nNoDataValue)
                        pabyBuffer[i] =
                            AdjustValue(nNoDataValue, nRoundUpBitTest);
                }
            }
        }
        else
        {
            for (decltype(nBytes) i = 0; i < nBytes; i += nBands)
            {
                for (int j = 0; j < nBands; ++j)
                {
                    if (bHasNoData && pabyBuffer[i + j] == nNoDataValue)
                        continue;

                    // Keep 255 in case it is alpha.
                    if (pabyBuffer[i + j] != 255)
                    {
                        if (pabyBuffer[i + j] &
                            panMaskOffsetLsb[j].nRoundUpBitTest)
                        {
                            pabyBuffer[i + j] = static_cast<GByte>(std::min(
                                255U,
                                (pabyBuffer[i + j] &
                                 static_cast<unsigned>(
                                     panMaskOffsetLsb[j].nMask)) +
                                    (static_cast<unsigned>(
                                         panMaskOffsetLsb[j].nRoundUpBitTest)
                                     << 1U)));
                        }
                        else
                        {
                            pabyBuffer[i + j] = static_cast<GByte>(
                                pabyBuffer[i + j] & panMaskOffsetLsb[j].nMask);
                        }

                        // Make sure that by discarding LSB we don't end up to a
                        // value that is no the nodata value
                        if (bHasNoData && pabyBuffer[i + j] == nNoDataValue)
                            pabyBuffer[i + j] = AdjustValue(
                                nNoDataValue,
                                panMaskOffsetLsb[j].nRoundUpBitTest);
                    }
                }
            }
        }
    }
    else if (nBitsPerSample == 8 && nSampleFormat == SAMPLEFORMAT_INT)
    {
        int8_t nNoDataValue = 0;
        if (bHasNoData && dfNoDataValue >= std::numeric_limits<int8_t>::min() &&
            dfNoDataValue <= std::numeric_limits<int8_t>::max() &&
            dfNoDataValue == static_cast<int8_t>(dfNoDataValue))
        {
            nNoDataValue = static_cast<int8_t>(dfNoDataValue);
        }
        else
        {
            bHasNoData = false;
        }
        DiscardLsbT<int8_t, int8_t>(pabyBuffer, nBytes, iBand, nBands,
                                    nPlanarConfig, panMaskOffsetLsb, bHasNoData,
                                    nNoDataValue);
    }
    else if (nBitsPerSample == 16 && nSampleFormat == SAMPLEFORMAT_INT)
    {
        int16_t nNoDataValue = 0;
        if (bHasNoData &&
            dfNoDataValue >= std::numeric_limits<int16_t>::min() &&
            dfNoDataValue <= std::numeric_limits<int16_t>::max() &&
            dfNoDataValue == static_cast<int16_t>(dfNoDataValue))
        {
            nNoDataValue = static_cast<int16_t>(dfNoDataValue);
        }
        else
        {
            bHasNoData = false;
        }
        DiscardLsbT<int16_t, int16_t>(pabyBuffer, nBytes, iBand, nBands,
                                      nPlanarConfig, panMaskOffsetLsb,
                                      bHasNoData, nNoDataValue);
    }
    else if (nBitsPerSample == 16 && nSampleFormat == SAMPLEFORMAT_UINT)
    {
        uint16_t nNoDataValue = 0;
        if (bHasNoData &&
            dfNoDataValue >= std::numeric_limits<uint16_t>::min() &&
            dfNoDataValue <= std::numeric_limits<uint16_t>::max() &&
            dfNoDataValue == static_cast<uint16_t>(dfNoDataValue))
        {
            nNoDataValue = static_cast<uint16_t>(dfNoDataValue);
        }
        else
        {
            bHasNoData = false;
        }
        DiscardLsbT<uint16_t, uint16_t>(pabyBuffer, nBytes, iBand, nBands,
                                        nPlanarConfig, panMaskOffsetLsb,
                                        bHasNoData, nNoDataValue);
    }
    else if (nBitsPerSample == 32 && nSampleFormat == SAMPLEFORMAT_INT)
    {
        int32_t nNoDataValue = 0;
        if (bHasNoData &&
            dfNoDataValue >= std::numeric_limits<int32_t>::min() &&
            dfNoDataValue <= std::numeric_limits<int32_t>::max() &&
            dfNoDataValue == static_cast<int32_t>(dfNoDataValue))
        {
            nNoDataValue = static_cast<int32_t>(dfNoDataValue);
        }
        else
        {
            bHasNoData = false;
        }
        DiscardLsbT<int32_t, int32_t>(pabyBuffer, nBytes, iBand, nBands,
                                      nPlanarConfig, panMaskOffsetLsb,
                                      bHasNoData, nNoDataValue);
    }
    else if (nBitsPerSample == 32 && nSampleFormat == SAMPLEFORMAT_UINT)
    {
        uint32_t nNoDataValue = 0;
        if (bHasNoData &&
            dfNoDataValue >= std::numeric_limits<uint32_t>::min() &&
            dfNoDataValue <= std::numeric_limits<uint32_t>::max() &&
            dfNoDataValue == static_cast<uint32_t>(dfNoDataValue))
        {
            nNoDataValue = static_cast<uint32_t>(dfNoDataValue);
        }
        else
        {
            bHasNoData = false;
        }
        DiscardLsbT<uint32_t, uint32_t>(pabyBuffer, nBytes, iBand, nBands,
                                        nPlanarConfig, panMaskOffsetLsb,
                                        bHasNoData, nNoDataValue);
    }
    else if (nBitsPerSample == 64 && nSampleFormat == SAMPLEFORMAT_INT)
    {
        // FIXME: we should not rely on dfNoDataValue when we support native
        // data type for nodata
        int64_t nNoDataValue = 0;
        if (bHasNoData &&
            dfNoDataValue >=
                static_cast<double>(std::numeric_limits<int64_t>::min()) &&
            dfNoDataValue <=
                static_cast<double>(std::numeric_limits<int64_t>::max()) &&
            dfNoDataValue ==
                static_cast<double>(static_cast<int64_t>(dfNoDataValue)))
        {
            nNoDataValue = static_cast<int64_t>(dfNoDataValue);
        }
        else
        {
            bHasNoData = false;
        }
        DiscardLsbT<int64_t, int64_t>(pabyBuffer, nBytes, iBand, nBands,
                                      nPlanarConfig, panMaskOffsetLsb,
                                      bHasNoData, nNoDataValue);
    }
    else if (nBitsPerSample == 64 && nSampleFormat == SAMPLEFORMAT_UINT)
    {
        // FIXME: we should not rely on dfNoDataValue when we support native
        // data type for nodata
        uint64_t nNoDataValue = 0;
        if (bHasNoData &&
            dfNoDataValue >=
                static_cast<double>(std::numeric_limits<uint64_t>::min()) &&
            dfNoDataValue <=
                static_cast<double>(std::numeric_limits<uint64_t>::max()) &&
            dfNoDataValue ==
                static_cast<double>(static_cast<uint64_t>(dfNoDataValue)))
        {
            nNoDataValue = static_cast<uint64_t>(dfNoDataValue);
        }
        else
        {
            bHasNoData = false;
        }
        DiscardLsbT<uint64_t, uint64_t>(pabyBuffer, nBytes, iBand, nBands,
                                        nPlanarConfig, panMaskOffsetLsb,
                                        bHasNoData, nNoDataValue);
    }
    else if (nBitsPerSample == 32 && nSampleFormat == SAMPLEFORMAT_IEEEFP)
    {
        float fNoDataValue = static_cast<float>(dfNoDataValue);
        DiscardLsbT<float, uint32_t>(pabyBuffer, nBytes, iBand, nBands,
                                     nPlanarConfig, panMaskOffsetLsb,
                                     bHasNoData, fNoDataValue);
    }
    else if (nBitsPerSample == 64 && nSampleFormat == SAMPLEFORMAT_IEEEFP)
    {
        DiscardLsbT<double, uint64_t>(pabyBuffer, nBytes, iBand, nBands,
                                      nPlanarConfig, panMaskOffsetLsb,
                                      bHasNoData, dfNoDataValue);
    }
}

void GTiffDataset::DiscardLsb(GByte *pabyBuffer, GPtrDiff_t nBytes,
                              int iBand) const
{
    ::DiscardLsb(pabyBuffer, nBytes, iBand, nBands, m_nSampleFormat,
                 m_nBitsPerSample, m_nPlanarConfig, m_panMaskOffsetLsb,
                 m_bNoDataSet, m_dfNoDataValue);
}

/************************************************************************/
/*                  WriteEncodedTileOrStrip()                           */
/************************************************************************/

CPLErr GTiffDataset::WriteEncodedTileOrStrip(uint32_t tile_or_strip, void *data,
                                             int bPreserveDataBuffer)
{
    CPLErr eErr = CE_None;

    if (TIFFIsTiled(m_hTIFF))
    {
        if (!(WriteEncodedTile(tile_or_strip, static_cast<GByte *>(data),
                               bPreserveDataBuffer)))
        {
            eErr = CE_Failure;
        }
    }
    else
    {
        if (!(WriteEncodedStrip(tile_or_strip, static_cast<GByte *>(data),
                                bPreserveDataBuffer)))
        {
            eErr = CE_Failure;
        }
    }

    return eErr;
}

/************************************************************************/
/*                           FlushBlockBuf()                            */
/************************************************************************/

CPLErr GTiffDataset::FlushBlockBuf()

{
    if (m_nLoadedBlock < 0 || !m_bLoadedBlockDirty)
        return CE_None;

    m_bLoadedBlockDirty = false;

    const CPLErr eErr =
        WriteEncodedTileOrStrip(m_nLoadedBlock, m_pabyBlockBuf, true);
    if (eErr != CE_None)
    {
        ReportError(CE_Failure, CPLE_AppDefined,
                    "WriteEncodedTile/Strip() failed.");
        m_bWriteError = true;
    }

    return eErr;
}

/************************************************************************/
/*                            LoadBlockBuf()                            */
/*                                                                      */
/*      Load working block buffer with request block (tile/strip).      */
/************************************************************************/

CPLErr GTiffDataset::LoadBlockBuf(int nBlockId, bool bReadFromDisk)

{
    if (m_nLoadedBlock == nBlockId && m_pabyBlockBuf != nullptr)
        return CE_None;

    /* -------------------------------------------------------------------- */
    /*      If we have a dirty loaded block, flush it out first.            */
    /* -------------------------------------------------------------------- */
    if (m_nLoadedBlock != -1 && m_bLoadedBlockDirty)
    {
        const CPLErr eErr = FlushBlockBuf();
        if (eErr != CE_None)
            return eErr;
    }

    /* -------------------------------------------------------------------- */
    /*      Get block size.                                                 */
    /* -------------------------------------------------------------------- */
    const GPtrDiff_t nBlockBufSize = static_cast<GPtrDiff_t>(
        TIFFIsTiled(m_hTIFF) ? TIFFTileSize(m_hTIFF) : TIFFStripSize(m_hTIFF));
    if (!nBlockBufSize)
    {
        ReportError(CE_Failure, CPLE_AppDefined,
                    "Bogus block size; unable to allocate a buffer.");
        return CE_Failure;
    }

    /* -------------------------------------------------------------------- */
    /*      Allocate a temporary buffer for this strip.                     */
    /* -------------------------------------------------------------------- */
    if (m_pabyBlockBuf == nullptr)
    {
        m_pabyBlockBuf =
            static_cast<GByte *>(VSI_CALLOC_VERBOSE(1, nBlockBufSize));
        if (m_pabyBlockBuf == nullptr)
        {
            return CE_Failure;
        }
    }

    if (m_nLoadedBlock == nBlockId)
        return CE_None;

    /* -------------------------------------------------------------------- */
    /*  When called from ::IWriteBlock in separate cases (or in single band */
    /*  geotiffs), the ::IWriteBlock will override the content of the buffer*/
    /*  with pImage, so we don't need to read data from disk                */
    /* -------------------------------------------------------------------- */
    if (!bReadFromDisk || m_bStreamingOut)
    {
        m_nLoadedBlock = nBlockId;
        return CE_None;
    }

    // libtiff 3.X doesn't like mixing read&write of JPEG compressed blocks
    // The below hack is necessary due to another hack that consist in
    // writing zero block to force creation of JPEG tables.
    if (nBlockId == 0 && m_bDontReloadFirstBlock)
    {
        m_bDontReloadFirstBlock = false;
        memset(m_pabyBlockBuf, 0, nBlockBufSize);
        m_nLoadedBlock = nBlockId;
        return CE_None;
    }

    /* -------------------------------------------------------------------- */
    /*      The bottom most partial tiles and strips are sometimes only     */
    /*      partially encoded.  This code reduces the requested data so     */
    /*      an error won't be reported in this case. (#1179)                */
    /*      We exclude tiled WEBP, because as it is a new codec, whole tiles*/
    /*      are written by libtiff. This helps avoiding creating a temporary*/
    /*      decode buffer.                                                  */
    /* -------------------------------------------------------------------- */
    auto nBlockReqSize = nBlockBufSize;
    const int nBlocksPerRow = DIV_ROUND_UP(nRasterXSize, m_nBlockXSize);
    const int nBlockYOff = (nBlockId % m_nBlocksPerBand) / nBlocksPerRow;

    if (nBlockYOff * m_nBlockYSize > nRasterYSize - m_nBlockYSize &&
        !(m_nCompression == COMPRESSION_WEBP && TIFFIsTiled(m_hTIFF)))
    {
        nBlockReqSize =
            (nBlockBufSize / m_nBlockYSize) *
            (m_nBlockYSize -
             static_cast<int>(
                 (static_cast<GIntBig>(nBlockYOff + 1) * m_nBlockYSize) %
                 nRasterYSize));
        memset(m_pabyBlockBuf, 0, nBlockBufSize);
    }

    /* -------------------------------------------------------------------- */
    /*      If we don't have this block already loaded, and we know it      */
    /*      doesn't yet exist on disk, just zero the memory buffer and      */
    /*      pretend we loaded it.                                           */
    /* -------------------------------------------------------------------- */
    bool bErrOccurred = false;
    if (!IsBlockAvailable(nBlockId, nullptr, nullptr, &bErrOccurred))
    {
        memset(m_pabyBlockBuf, 0, nBlockBufSize);
        m_nLoadedBlock = nBlockId;
        if (bErrOccurred)
            return CE_Failure;
        return CE_None;
    }

    /* -------------------------------------------------------------------- */
    /*      Load the block, if it isn't our current block.                  */
    /* -------------------------------------------------------------------- */
    CPLErr eErr = CE_None;

    if (!ReadStrile(nBlockId, m_pabyBlockBuf, nBlockReqSize))
    {
        memset(m_pabyBlockBuf, 0, nBlockBufSize);
        eErr = CE_Failure;
    }

    if (eErr == CE_None)
    {
        if (m_nCompression == COMPRESSION_WEBP && TIFFIsTiled(m_hTIFF) &&
            nBlockYOff * m_nBlockYSize > nRasterYSize - m_nBlockYSize)
        {
            const auto nValidBytes =
                (nBlockBufSize / m_nBlockYSize) *
                (m_nBlockYSize -
                 static_cast<int>(
                     (static_cast<GIntBig>(nBlockYOff + 1) * m_nBlockYSize) %
                     nRasterYSize));
            // Zero-out unused area
            memset(m_pabyBlockBuf + nValidBytes, 0,
                   nBlockBufSize - nValidBytes);
        }

        m_nLoadedBlock = nBlockId;
    }
    else
    {
        m_nLoadedBlock = -1;
    }
    m_bLoadedBlockDirty = false;

    return eErr;
}

/************************************************************************/
/*                   GTiffFillStreamableOffsetAndCount()                */
/************************************************************************/

static void GTiffFillStreamableOffsetAndCount(TIFF *hTIFF, int nSize)
{
    uint32_t nXSize = 0;
    uint32_t nYSize = 0;
    TIFFGetField(hTIFF, TIFFTAG_IMAGEWIDTH, &nXSize);
    TIFFGetField(hTIFF, TIFFTAG_IMAGELENGTH, &nYSize);
    const bool bIsTiled = CPL_TO_BOOL(TIFFIsTiled(hTIFF));
    const int nBlockCount =
        bIsTiled ? TIFFNumberOfTiles(hTIFF) : TIFFNumberOfStrips(hTIFF);

    toff_t *panOffset = nullptr;
    TIFFGetField(hTIFF, bIsTiled ? TIFFTAG_TILEOFFSETS : TIFFTAG_STRIPOFFSETS,
                 &panOffset);
    toff_t *panSize = nullptr;
    TIFFGetField(hTIFF,
                 bIsTiled ? TIFFTAG_TILEBYTECOUNTS : TIFFTAG_STRIPBYTECOUNTS,
                 &panSize);
    toff_t nOffset = nSize;
    // Trick to avoid clang static analyzer raising false positive about
    // divide by zero later.
    int nBlocksPerBand = 1;
    uint32_t nRowsPerStrip = 0;
    if (!bIsTiled)
    {
        TIFFGetField(hTIFF, TIFFTAG_ROWSPERSTRIP, &nRowsPerStrip);
        if (nRowsPerStrip > static_cast<uint32_t>(nYSize))
            nRowsPerStrip = nYSize;
        nBlocksPerBand = DIV_ROUND_UP(nYSize, nRowsPerStrip);
    }
    for (int i = 0; i < nBlockCount; ++i)
    {
        GPtrDiff_t cc = bIsTiled
                            ? static_cast<GPtrDiff_t>(TIFFTileSize(hTIFF))
                            : static_cast<GPtrDiff_t>(TIFFStripSize(hTIFF));
        if (!bIsTiled)
        {
            /* --------------------------------------------------------------------
             */
            /*      If this is the last strip in the image, and is partial, then
             */
            /*      we need to trim the number of scanlines written to the */
            /*      amount of valid data we have. (#2748) */
            /* --------------------------------------------------------------------
             */
            int nStripWithinBand = i % nBlocksPerBand;
            if (nStripWithinBand * nRowsPerStrip > nYSize - nRowsPerStrip)
            {
                cc = (cc / nRowsPerStrip) *
                     (nYSize - nStripWithinBand * nRowsPerStrip);
            }
        }
        panOffset[i] = nOffset;
        panSize[i] = cc;
        nOffset += cc;
    }
}

/************************************************************************/
/*                             Crystalize()                             */
/*                                                                      */
/*      Make sure that the directory information is written out for     */
/*      a new file, require before writing any imagery data.            */
/************************************************************************/

void GTiffDataset::Crystalize()

{
    if (m_bCrystalized)
        return;

    // TODO: libtiff writes extended tags in the order they are specified
    // and not in increasing order.
    WriteMetadata(this, m_hTIFF, true, m_eProfile, m_pszFilename,
                  m_papszCreationOptions);
    WriteGeoTIFFInfo();
    if (m_bNoDataSet)
        WriteNoDataValue(m_hTIFF, m_dfNoDataValue);
    else if (m_bNoDataSetAsInt64)
        WriteNoDataValue(m_hTIFF, m_nNoDataValueInt64);
    else if (m_bNoDataSetAsUInt64)
        WriteNoDataValue(m_hTIFF, m_nNoDataValueUInt64);

    m_bMetadataChanged = false;
    m_bGeoTIFFInfoChanged = false;
    m_bNoDataChanged = false;
    m_bNeedsRewrite = false;

    m_bCrystalized = true;

    TIFFWriteCheck(m_hTIFF, TIFFIsTiled(m_hTIFF), "GTiffDataset::Crystalize");

    TIFFWriteDirectory(m_hTIFF);
    if (m_bStreamingOut)
    {
        // We need to write twice the directory to be sure that custom
        // TIFF tags are correctly sorted and that padding bytes have been
        // added.
        TIFFSetDirectory(m_hTIFF, 0);
        TIFFWriteDirectory(m_hTIFF);

        if (VSIFSeekL(m_fpL, 0, SEEK_END) != 0)
        {
            ReportError(CE_Failure, CPLE_FileIO, "Could not seek");
        }
        const int nSize = static_cast<int>(VSIFTellL(m_fpL));

        TIFFSetDirectory(m_hTIFF, 0);
        GTiffFillStreamableOffsetAndCount(m_hTIFF, nSize);
        TIFFWriteDirectory(m_hTIFF);

        vsi_l_offset nDataLength = 0;
        void *pabyBuffer =
            VSIGetMemFileBuffer(m_pszTmpFilename, &nDataLength, FALSE);
        if (static_cast<int>(VSIFWriteL(
                pabyBuffer, 1, static_cast<int>(nDataLength), m_fpToWrite)) !=
            static_cast<int>(nDataLength))
        {
            ReportError(CE_Failure, CPLE_FileIO, "Could not write %d bytes",
                        static_cast<int>(nDataLength));
        }
        // In case of single strip file, there's a libtiff check that would
        // issue a warning since the file hasn't the required size.
        CPLPushErrorHandler(CPLQuietErrorHandler);
        TIFFSetDirectory(m_hTIFF, 0);
        CPLPopErrorHandler();
    }
    else
    {
        TIFFSetDirectory(
            m_hTIFF, static_cast<tdir_t>(TIFFNumberOfDirectories(m_hTIFF) - 1));
    }

    RestoreVolatileParameters(m_hTIFF);

    m_nDirOffset = TIFFCurrentDirOffset(m_hTIFF);
}

/************************************************************************/
/*                          IsBlockAvailable()                          */
/*                                                                      */
/*      Return true if the indicated strip/tile is available.  We       */
/*      establish this by testing if the stripbytecount is zero.  If    */
/*      zero then the block has never been committed to disk.           */
/************************************************************************/

bool GTiffDataset::IsBlockAvailable(int nBlockId, vsi_l_offset *pnOffset,
                                    vsi_l_offset *pnSize, bool *pbErrOccurred)

{
    if (pbErrOccurred)
        *pbErrOccurred = false;

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    std::pair<vsi_l_offset, vsi_l_offset> oPair;
    if (m_oCacheStrileToOffsetByteCount.tryGet(nBlockId, oPair))
    {
        if (pnOffset)
            *pnOffset = oPair.first;
        if (pnSize)
            *pnSize = oPair.second;
        return oPair.first != 0;
    }
#endif

    WaitCompletionForBlock(nBlockId);

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    // Optimization to avoid fetching the whole Strip/TileCounts and
    // Strip/TileOffsets arrays.
    if (eAccess == GA_ReadOnly && !m_bStreamingIn)
    {
        int nErrOccurred = 0;
        auto bytecount =
            TIFFGetStrileByteCountWithErr(m_hTIFF, nBlockId, &nErrOccurred);
        if (nErrOccurred && pbErrOccurred)
            *pbErrOccurred = true;
        if (pnOffset)
        {
            *pnOffset =
                TIFFGetStrileOffsetWithErr(m_hTIFF, nBlockId, &nErrOccurred);
            if (nErrOccurred && pbErrOccurred)
                *pbErrOccurred = true;
        }
        if (pnSize)
            *pnSize = bytecount;
        return bytecount != 0;
    }
#endif

    toff_t *panByteCounts = nullptr;
    toff_t *panOffsets = nullptr;
    const bool bIsTiled = CPL_TO_BOOL(TIFFIsTiled(m_hTIFF));

    if ((bIsTiled &&
         TIFFGetField(m_hTIFF, TIFFTAG_TILEBYTECOUNTS, &panByteCounts) &&
         (pnOffset == nullptr ||
          TIFFGetField(m_hTIFF, TIFFTAG_TILEOFFSETS, &panOffsets))) ||
        (!bIsTiled &&
         TIFFGetField(m_hTIFF, TIFFTAG_STRIPBYTECOUNTS, &panByteCounts) &&
         (pnOffset == nullptr ||
          TIFFGetField(m_hTIFF, TIFFTAG_STRIPOFFSETS, &panOffsets))))
    {
        if (panByteCounts == nullptr ||
            (pnOffset != nullptr && panOffsets == nullptr))
        {
            if (pbErrOccurred)
                *pbErrOccurred = true;
            return false;
        }
        const int nBlockCount =
            bIsTiled ? TIFFNumberOfTiles(m_hTIFF) : TIFFNumberOfStrips(m_hTIFF);
        if (nBlockId >= nBlockCount)
        {
            if (pbErrOccurred)
                *pbErrOccurred = true;
            return false;
        }

        if (pnOffset)
            *pnOffset = panOffsets[nBlockId];
        if (pnSize)
            *pnSize = panByteCounts[nBlockId];
        return panByteCounts[nBlockId] != 0;
    }
    else
    {
        if (pbErrOccurred)
            *pbErrOccurred = true;
    }

    return false;
}

/************************************************************************/
/*                             FlushCache()                             */
/*                                                                      */
/*      We override this so we can also flush out local tiff strip      */
/*      cache if need be.                                               */
/************************************************************************/

void GTiffDataset::FlushCache(bool bAtClosing)

{
    FlushCacheInternal(bAtClosing, true);
}

void GTiffDataset::FlushCacheInternal(bool bAtClosing, bool bFlushDirectory)
{
    if (m_bIsFinalized)
        return;

    GDALPamDataset::FlushCache(bAtClosing);

    if (m_bLoadedBlockDirty && m_nLoadedBlock != -1)
        FlushBlockBuf();

    CPLFree(m_pabyBlockBuf);
    m_pabyBlockBuf = nullptr;
    m_nLoadedBlock = -1;
    m_bLoadedBlockDirty = false;

    // Finish compression
    auto poQueue = m_poBaseDS ? m_poBaseDS->m_poCompressQueue.get()
                              : m_poCompressQueue.get();
    if (poQueue)
    {
        poQueue->WaitCompletion();

        // Flush remaining data
        auto &oQueue =
            m_poBaseDS ? m_poBaseDS->m_asQueueJobIdx : m_asQueueJobIdx;
        while (!oQueue.empty())
        {
            WaitCompletionForJobIdx(oQueue.front());
        }
    }

    if (bFlushDirectory && GetAccess() == GA_Update)
    {
        FlushDirectory();
    }
}

/************************************************************************/
/*                           FlushDirectory()                           */
/************************************************************************/

void GTiffDataset::FlushDirectory()

{
    const auto ReloadAllOtherDirectories = [this]()
    {
        const auto poBaseDS = m_poBaseDS ? m_poBaseDS : this;
        if (poBaseDS->m_papoOverviewDS)
        {
            for (int i = 0; i < poBaseDS->m_nOverviewCount; ++i)
            {
                if (poBaseDS->m_papoOverviewDS[i]->m_bCrystalized &&
                    poBaseDS->m_papoOverviewDS[i] != this)
                {
                    poBaseDS->m_papoOverviewDS[i]->ReloadDirectory(true);
                }

                if (poBaseDS->m_papoOverviewDS[i]->m_poMaskDS &&
                    poBaseDS->m_papoOverviewDS[i]->m_poMaskDS != this &&
                    poBaseDS->m_papoOverviewDS[i]->m_poMaskDS->m_bCrystalized)
                {
                    poBaseDS->m_papoOverviewDS[i]->m_poMaskDS->ReloadDirectory(
                        true);
                }
            }
        }
        if (poBaseDS->m_poMaskDS && poBaseDS->m_poMaskDS != this &&
            poBaseDS->m_poMaskDS->m_bCrystalized)
        {
            poBaseDS->m_poMaskDS->ReloadDirectory(true);
        }
        if (poBaseDS->m_bCrystalized && poBaseDS != this)
        {
            poBaseDS->ReloadDirectory(true);
        }
    };

    if (eAccess == GA_Update)
    {
        if (m_bMetadataChanged)
        {
            m_bNeedsRewrite =
                WriteMetadata(this, m_hTIFF, true, m_eProfile, m_pszFilename,
                              m_papszCreationOptions);
            m_bMetadataChanged = false;

            if (m_bForceUnsetRPC)
            {
                double *padfRPCTag = nullptr;
                uint16_t nCount;
                if (TIFFGetField(m_hTIFF, TIFFTAG_RPCCOEFFICIENT, &nCount,
                                 &padfRPCTag))
                {
                    std::vector<double> zeroes(92);
                    TIFFSetField(m_hTIFF, TIFFTAG_RPCCOEFFICIENT, 92,
                                 zeroes.data());
                    TIFFUnsetField(m_hTIFF, TIFFTAG_RPCCOEFFICIENT);
                    m_bNeedsRewrite = true;
                }

                GDALWriteRPCTXTFile(m_pszFilename, nullptr);
                GDALWriteRPBFile(m_pszFilename, nullptr);
            }
        }

        if (m_bGeoTIFFInfoChanged)
        {
            WriteGeoTIFFInfo();
            m_bGeoTIFFInfoChanged = false;
        }

        if (m_bNoDataChanged)
        {
            if (m_bNoDataSet)
            {
                WriteNoDataValue(m_hTIFF, m_dfNoDataValue);
            }
            else if (m_bNoDataSetAsInt64)
            {
                WriteNoDataValue(m_hTIFF, m_nNoDataValueInt64);
            }
            else if (m_bNoDataSetAsUInt64)
            {
                WriteNoDataValue(m_hTIFF, m_nNoDataValueUInt64);
            }
            else
            {
                UnsetNoDataValue(m_hTIFF);
            }
            m_bNeedsRewrite = true;
            m_bNoDataChanged = false;
        }

        if (m_bNeedsRewrite)
        {
            if (!m_bCrystalized)
            {
                Crystalize();
            }
            else
            {
                const TIFFSizeProc pfnSizeProc = TIFFGetSizeProc(m_hTIFF);

                m_nDirOffset = pfnSizeProc(TIFFClientdata(m_hTIFF));
                if ((m_nDirOffset % 2) == 1)
                    ++m_nDirOffset;

                TIFFRewriteDirectory(m_hTIFF);

                TIFFSetSubDirectory(m_hTIFF, m_nDirOffset);

                ReloadAllOtherDirectories();

                if (m_bLayoutIFDSBeforeData && m_bBlockOrderRowMajor &&
                    m_bLeaderSizeAsUInt4 &&
                    m_bTrailerRepeatedLast4BytesRepeated &&
                    !m_bKnownIncompatibleEdition &&
                    !m_bWriteKnownIncompatibleEdition)
                {
                    ReportError(CE_Warning, CPLE_AppDefined,
                                "The IFD has been rewritten at the end of "
                                "the file, which breaks COG layout.");
                    m_bKnownIncompatibleEdition = true;
                    m_bWriteKnownIncompatibleEdition = true;
                }
            }

            m_bNeedsRewrite = false;
        }
    }

    // There are some circumstances in which we can reach this point
    // without having made this our directory (SetDirectory()) in which
    // case we should not risk a flush.
    if (GetAccess() == GA_Update &&
        TIFFCurrentDirOffset(m_hTIFF) == m_nDirOffset)
    {
        const TIFFSizeProc pfnSizeProc = TIFFGetSizeProc(m_hTIFF);

        toff_t nNewDirOffset = pfnSizeProc(TIFFClientdata(m_hTIFF));
        if ((nNewDirOffset % 2) == 1)
            ++nNewDirOffset;

        TIFFFlush(m_hTIFF);

        if (m_nDirOffset != TIFFCurrentDirOffset(m_hTIFF))
        {
            m_nDirOffset = nNewDirOffset;
            ReloadAllOtherDirectories();
            CPLDebug("GTiff",
                     "directory moved during flush in FlushDirectory()");
        }
    }

    SetDirectory();
}

/************************************************************************/
/*                           CleanOverviews()                           */
/************************************************************************/

CPLErr GTiffDataset::CleanOverviews()

{
    CPLAssert(!m_poBaseDS);

    ScanDirectories();

    FlushDirectory();

    /* -------------------------------------------------------------------- */
    /*      Cleanup overviews objects, and get offsets to all overview      */
    /*      directories.                                                    */
    /* -------------------------------------------------------------------- */
    std::vector<toff_t> anOvDirOffsets;

    for (int i = 0; i < m_nOverviewCount; ++i)
    {
        anOvDirOffsets.push_back(m_papoOverviewDS[i]->m_nDirOffset);
        if (m_papoOverviewDS[i]->m_poMaskDS)
            anOvDirOffsets.push_back(
                m_papoOverviewDS[i]->m_poMaskDS->m_nDirOffset);
        delete m_papoOverviewDS[i];
    }

    /* -------------------------------------------------------------------- */
    /*      Loop through all the directories, translating the offsets       */
    /*      into indexes we can use with TIFFUnlinkDirectory().             */
    /* -------------------------------------------------------------------- */
    std::vector<uint16_t> anOvDirIndexes;
    int iThisOffset = 1;

    TIFFSetDirectory(m_hTIFF, 0);

    while (true)
    {
        for (toff_t nOffset : anOvDirOffsets)
        {
            if (nOffset == TIFFCurrentDirOffset(m_hTIFF))
            {
                anOvDirIndexes.push_back(static_cast<uint16_t>(iThisOffset));
            }
        }

        if (TIFFLastDirectory(m_hTIFF))
            break;

        TIFFReadDirectory(m_hTIFF);
        ++iThisOffset;
    }

    /* -------------------------------------------------------------------- */
    /*      Actually unlink the target directories.  Note that we do        */
    /*      this from last to first so as to avoid renumbering any of       */
    /*      the earlier directories we need to remove.                      */
    /* -------------------------------------------------------------------- */
    while (!anOvDirIndexes.empty())
    {
        TIFFUnlinkDirectory(m_hTIFF, anOvDirIndexes.back());
        anOvDirIndexes.pop_back();
    }

    CPLFree(m_papoOverviewDS);
    m_nOverviewCount = 0;
    m_papoOverviewDS = nullptr;

    if (m_poMaskDS)
    {
        CPLFree(m_poMaskDS->m_papoOverviewDS);
        m_poMaskDS->m_nOverviewCount = 0;
        m_poMaskDS->m_papoOverviewDS = nullptr;
    }

    if (!SetDirectory())
        return CE_Failure;

    return CE_None;
}

/************************************************************************/
/*                   RegisterNewOverviewDataset()                       */
/************************************************************************/

CPLErr GTiffDataset::RegisterNewOverviewDataset(toff_t nOverviewOffset,
                                                int l_nJpegQuality,
                                                CSLConstList papszOptions)
{
    if (m_nOverviewCount == 127)
        return CE_Failure;

    const auto GetOptionValue =
        [papszOptions](const char *pszOptionKey, const char *pszConfigOptionKey,
                       const char **ppszKeyUsed = nullptr)
    {
        const char *pszVal = CSLFetchNameValue(papszOptions, pszOptionKey);
        if (pszVal)
        {
            if (ppszKeyUsed)
                *ppszKeyUsed = pszOptionKey;
            return pszVal;
        }
        pszVal = CSLFetchNameValue(papszOptions, pszConfigOptionKey);
        if (pszVal)
        {
            if (ppszKeyUsed)
                *ppszKeyUsed = pszConfigOptionKey;
            return pszVal;
        }
        pszVal = CPLGetConfigOption(pszConfigOptionKey, nullptr);
        if (pszVal && ppszKeyUsed)
            *ppszKeyUsed = pszConfigOptionKey;
        return pszVal;
    };

    int nZLevel = m_nZLevel;
    if (const char *opt = GetOptionValue("ZLEVEL", "ZLEVEL_OVERVIEW"))
    {
        nZLevel = atoi(opt);
    }

    int nZSTDLevel = m_nZSTDLevel;
    if (const char *opt = GetOptionValue("ZSTD_LEVEL", "ZSTD_LEVEL_OVERVIEW"))
    {
        nZSTDLevel = atoi(opt);
    }

    bool bWebpLossless = m_bWebPLossless;
    const char *pszWebPLosslessOverview =
        GetOptionValue("WEBP_LOSSLESS", "WEBP_LOSSLESS_OVERVIEW");
    if (pszWebPLosslessOverview)
    {
        bWebpLossless = CPLTestBool(pszWebPLosslessOverview);
    }

    int nWebpLevel = m_nWebPLevel;
    const char *pszKeyWebpLevel = "";
    if (const char *opt = GetOptionValue("WEBP_LEVEL", "WEBP_LEVEL_OVERVIEW",
                                         &pszKeyWebpLevel))
    {
        if (pszWebPLosslessOverview == nullptr && m_bWebPLossless)
        {
            CPLDebug("GTiff",
                     "%s specified, but not WEBP_LOSSLESS_OVERVIEW. "
                     "Assuming WEBP_LOSSLESS_OVERVIEW=NO",
                     pszKeyWebpLevel);
            bWebpLossless = false;
        }
        else if (bWebpLossless)
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "%s is specified, but WEBP_LOSSLESS_OVERVIEW=YES. "
                     "%s will be ignored.",
                     pszKeyWebpLevel, pszKeyWebpLevel);
        }
        nWebpLevel = atoi(opt);
    }

    double dfMaxZError = m_dfMaxZError;
    if (const char *opt = GetOptionValue("MAX_Z_ERROR", "MAX_Z_ERROR_OVERVIEW"))
    {
        dfMaxZError = CPLAtof(opt);
    }

    GTiffDataset *poODS = new GTiffDataset();
    poODS->ShareLockWithParentDataset(this);
    poODS->m_pszFilename = CPLStrdup(m_pszFilename);
    const char *pszSparseOK = GetOptionValue("SPARSE_OK", "SPARSE_OK_OVERVIEW");
    if (pszSparseOK && CPLTestBool(pszSparseOK))
    {
        poODS->m_bWriteEmptyTiles = false;
        poODS->m_bFillEmptyTilesAtClosing = false;
    }
    else
    {
        poODS->m_bWriteEmptyTiles = m_bWriteEmptyTiles;
        poODS->m_bFillEmptyTilesAtClosing = m_bFillEmptyTilesAtClosing;
    }
    poODS->m_nJpegQuality = static_cast<signed char>(l_nJpegQuality);
    poODS->m_nWebPLevel = static_cast<signed char>(nWebpLevel);
    poODS->m_nZLevel = static_cast<signed char>(nZLevel);
    poODS->m_nLZMAPreset = m_nLZMAPreset;
    poODS->m_nZSTDLevel = static_cast<signed char>(nZSTDLevel);
    poODS->m_bWebPLossless = bWebpLossless;
    poODS->m_nJpegTablesMode = m_nJpegTablesMode;
    poODS->m_dfMaxZError = dfMaxZError;
    memcpy(poODS->m_anLercAddCompressionAndVersion,
           m_anLercAddCompressionAndVersion,
           sizeof(m_anLercAddCompressionAndVersion));
#ifdef HAVE_JXL
    poODS->m_bJXLLossless = m_bJXLLossless;
    poODS->m_fJXLDistance = m_fJXLDistance;
    poODS->m_nJXLEffort = m_nJXLEffort;
#endif

    if (poODS->OpenOffset(VSI_TIFFOpenChild(m_hTIFF), nOverviewOffset,
                          GA_Update) != CE_None)
    {
        delete poODS;
        return CE_Failure;
    }

    // Assign color interpretation from main dataset
    const int l_nBands = GetRasterCount();
    for (int i = 1; i <= l_nBands; i++)
    {
        auto poBand = dynamic_cast<GTiffRasterBand *>(poODS->GetRasterBand(i));
        if (poBand)
            poBand->m_eBandInterp = GetRasterBand(i)->GetColorInterpretation();
    }

    // Do that now that m_nCompression is set
    poODS->RestoreVolatileParameters(poODS->m_hTIFF);

    ++m_nOverviewCount;
    m_papoOverviewDS = static_cast<GTiffDataset **>(
        CPLRealloc(m_papoOverviewDS, m_nOverviewCount * (sizeof(void *))));
    m_papoOverviewDS[m_nOverviewCount - 1] = poODS;
    poODS->m_poBaseDS = this;
    poODS->m_bIsOverview = true;
    return CE_None;
}

/************************************************************************/
/*                     CreateTIFFColorTable()                           */
/************************************************************************/

static void CreateTIFFColorTable(GDALColorTable *poColorTable, int nBits,
                                 std::vector<unsigned short> &anTRed,
                                 std::vector<unsigned short> &anTGreen,
                                 std::vector<unsigned short> &anTBlue,
                                 unsigned short *&panRed,
                                 unsigned short *&panGreen,
                                 unsigned short *&panBlue)
{
    int nColors;

    if (nBits == 8)
        nColors = 256;
    else if (nBits < 8)
        nColors = 1 << nBits;
    else
        nColors = 65536;

    anTRed.resize(nColors, 0);
    anTGreen.resize(nColors, 0);
    anTBlue.resize(nColors, 0);

    for (int iColor = 0; iColor < nColors; ++iColor)
    {
        if (iColor < poColorTable->GetColorEntryCount())
        {
            GDALColorEntry sRGB;

            poColorTable->GetColorEntryAsRGB(iColor, &sRGB);

            anTRed[iColor] = static_cast<unsigned short>(257 * sRGB.c1);
            anTGreen[iColor] = static_cast<unsigned short>(257 * sRGB.c2);
            anTBlue[iColor] = static_cast<unsigned short>(257 * sRGB.c3);
        }
        else
        {
            anTRed[iColor] = 0;
            anTGreen[iColor] = 0;
            anTBlue[iColor] = 0;
        }
    }

    panRed = &(anTRed[0]);
    panGreen = &(anTGreen[0]);
    panBlue = &(anTBlue[0]);
}

/************************************************************************/
/*                       GTIFFUpdatePhotometric()                      */
/************************************************************************/

bool GTIFFUpdatePhotometric(const char *pszPhotometric,
                            const char *pszOptionKey, int nCompression,
                            const char *pszInterleave, int nBands,
                            uint16_t &nPhotometric, uint16_t &nPlanarConfig)
{
    if (pszPhotometric != nullptr && pszPhotometric[0] != '\0')
    {
        if (EQUAL(pszPhotometric, "MINISBLACK"))
            nPhotometric = PHOTOMETRIC_MINISBLACK;
        else if (EQUAL(pszPhotometric, "MINISWHITE"))
            nPhotometric = PHOTOMETRIC_MINISWHITE;
        else if (EQUAL(pszPhotometric, "RGB"))
        {
            nPhotometric = PHOTOMETRIC_RGB;
        }
        else if (EQUAL(pszPhotometric, "CMYK"))
        {
            nPhotometric = PHOTOMETRIC_SEPARATED;
        }
        else if (EQUAL(pszPhotometric, "YCBCR"))
        {
            nPhotometric = PHOTOMETRIC_YCBCR;

            // Because of subsampling, setting YCBCR without JPEG compression
            // leads to a crash currently. Would need to make
            // GTiffRasterBand::IWriteBlock() aware of subsampling so that it
            // doesn't overrun buffer size returned by libtiff.
            if (nCompression != COMPRESSION_JPEG)
            {
                CPLError(CE_Failure, CPLE_NotSupported,
                         "Currently, %s=YCBCR requires JPEG compression",
                         pszOptionKey);
                return false;
            }

            if (pszInterleave != nullptr && pszInterleave[0] != '\0' &&
                nPlanarConfig == PLANARCONFIG_SEPARATE)
            {
                CPLError(CE_Failure, CPLE_NotSupported,
                         "%s=YCBCR requires PIXEL interleaving", pszOptionKey);
                return false;
            }
            else
            {
                nPlanarConfig = PLANARCONFIG_CONTIG;
            }

            // YCBCR strictly requires 3 bands. Not less, not more
            // Issue an explicit error message as libtiff one is a bit cryptic:
            // JPEGLib:Bogus input colorspace.
            if (nBands != 3)
            {
                CPLError(CE_Failure, CPLE_NotSupported,
                         "%s=YCBCR requires a source raster "
                         "with only 3 bands (RGB)",
                         pszOptionKey);
                return false;
            }
        }
        else if (EQUAL(pszPhotometric, "CIELAB"))
        {
            nPhotometric = PHOTOMETRIC_CIELAB;
        }
        else if (EQUAL(pszPhotometric, "ICCLAB"))
        {
            nPhotometric = PHOTOMETRIC_ICCLAB;
        }
        else if (EQUAL(pszPhotometric, "ITULAB"))
        {
            nPhotometric = PHOTOMETRIC_ITULAB;
        }
        else
        {
            CPLError(CE_Warning, CPLE_IllegalArg,
                     "%s=%s value not recognised, ignoring.", pszOptionKey,
                     pszPhotometric);
        }
    }
    return true;
}

/************************************************************************/
/*                        GetOverviewParameters()                       */
/************************************************************************/

bool GTiffDataset::GetOverviewParameters(
    int &nCompression, uint16_t &nPlanarConfig, uint16_t &nPredictor,
    uint16_t &nPhotometric, int &nOvrJpegQuality, std::string &osNoData,
    uint16_t *&panExtraSampleValues, uint16_t &nExtraSamples,
    CSLConstList papszOptions) const
{
    const auto GetOptionValue =
        [papszOptions](const char *pszOptionKey, const char *pszConfigOptionKey,
                       const char **ppszKeyUsed = nullptr)
    {
        const char *pszVal = CSLFetchNameValue(papszOptions, pszOptionKey);
        if (pszVal)
        {
            if (ppszKeyUsed)
                *ppszKeyUsed = pszOptionKey;
            return pszVal;
        }
        pszVal = CSLFetchNameValue(papszOptions, pszConfigOptionKey);
        if (pszVal)
        {
            if (ppszKeyUsed)
                *ppszKeyUsed = pszConfigOptionKey;
            return pszVal;
        }
        pszVal = CPLGetConfigOption(pszConfigOptionKey, nullptr);
        if (pszVal && ppszKeyUsed)
            *ppszKeyUsed = pszConfigOptionKey;
        return pszVal;
    };

    /* -------------------------------------------------------------------- */
    /*      Determine compression method.                                   */
    /* -------------------------------------------------------------------- */
    nCompression = m_nCompression;
    const char *pszOptionKey = "";
    const char *pszCompressValue =
        GetOptionValue("COMPRESS", "COMPRESS_OVERVIEW", &pszOptionKey);
    if (pszCompressValue != nullptr)
    {
        nCompression =
            GTIFFGetCompressionMethod(pszCompressValue, pszOptionKey);
        if (nCompression < 0)
        {
            nCompression = m_nCompression;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Determine planar configuration.                                 */
    /* -------------------------------------------------------------------- */
    nPlanarConfig = m_nPlanarConfig;
    if (nCompression == COMPRESSION_WEBP)
    {
        nPlanarConfig = PLANARCONFIG_CONTIG;
    }
    const char *pszInterleave =
        GetOptionValue("INTERLEAVE", "INTERLEAVE_OVERVIEW", &pszOptionKey);
    if (pszInterleave != nullptr && pszInterleave[0] != '\0')
    {
        if (EQUAL(pszInterleave, "PIXEL"))
            nPlanarConfig = PLANARCONFIG_CONTIG;
        else if (EQUAL(pszInterleave, "BAND"))
            nPlanarConfig = PLANARCONFIG_SEPARATE;
        else
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "%s=%s unsupported, "
                     "value must be PIXEL or BAND. ignoring",
                     pszOptionKey, pszInterleave);
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Determine predictor tag                                         */
    /* -------------------------------------------------------------------- */
    nPredictor = PREDICTOR_NONE;
    if (GTIFFSupportsPredictor(nCompression))
    {
        const char *pszPredictor =
            GetOptionValue("PREDICTOR", "PREDICTOR_OVERVIEW");
        if (pszPredictor != nullptr)
        {
            nPredictor = static_cast<uint16_t>(atoi(pszPredictor));
        }
        else if (GTIFFSupportsPredictor(m_nCompression))
            TIFFGetField(m_hTIFF, TIFFTAG_PREDICTOR, &nPredictor);
    }

    /* -------------------------------------------------------------------- */
    /*      Determine photometric tag                                       */
    /* -------------------------------------------------------------------- */
    nPhotometric = m_nPhotometric;
    const char *pszPhotometric =
        GetOptionValue("PHOTOMETRIC", "PHOTOMETRIC_OVERVIEW", &pszOptionKey);
    if (!GTIFFUpdatePhotometric(pszPhotometric, pszOptionKey, nCompression,
                                pszInterleave, nBands, nPhotometric,
                                nPlanarConfig))
    {
        return false;
    }

    /* -------------------------------------------------------------------- */
    /*      Determine JPEG quality                                          */
    /* -------------------------------------------------------------------- */
    nOvrJpegQuality = m_nJpegQuality;
    if (nCompression == COMPRESSION_JPEG)
    {
        const char *pszJPEGQuality =
            GetOptionValue("JPEG_QUALITY", "JPEG_QUALITY_OVERVIEW");
        if (pszJPEGQuality != nullptr)
        {
            nOvrJpegQuality = atoi(pszJPEGQuality);
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Set nodata.                                                     */
    /* -------------------------------------------------------------------- */
    if (m_bNoDataSet)
    {
        osNoData = GTiffFormatGDALNoDataTagValue(m_dfNoDataValue);
    }

    /* -------------------------------------------------------------------- */
    /*      Fetch extra sample tag                                          */
    /* -------------------------------------------------------------------- */
    panExtraSampleValues = nullptr;
    nExtraSamples = 0;
    if (TIFFGetField(m_hTIFF, TIFFTAG_EXTRASAMPLES, &nExtraSamples,
                     &panExtraSampleValues))
    {
        uint16_t *panExtraSampleValuesNew = static_cast<uint16_t *>(
            CPLMalloc(nExtraSamples * sizeof(uint16_t)));
        memcpy(panExtraSampleValuesNew, panExtraSampleValues,
               nExtraSamples * sizeof(uint16_t));
        panExtraSampleValues = panExtraSampleValuesNew;
    }
    else
    {
        panExtraSampleValues = nullptr;
        nExtraSamples = 0;
    }

    return true;
}

/************************************************************************/
/*                  CreateOverviewsFromSrcOverviews()                   */
/************************************************************************/

// If poOvrDS is not null, it is used and poSrcDS is ignored.

CPLErr GTiffDataset::CreateOverviewsFromSrcOverviews(GDALDataset *poSrcDS,
                                                     GDALDataset *poOvrDS,
                                                     int nOverviews)
{
    CPLAssert(poSrcDS->GetRasterCount() != 0);
    CPLAssert(m_nOverviewCount == 0);

    ScanDirectories();

    FlushDirectory();

    int nOvBitsPerSample = m_nBitsPerSample;

    /* -------------------------------------------------------------------- */
    /*      Do we need some metadata for the overviews?                     */
    /* -------------------------------------------------------------------- */
    CPLString osMetadata;

    GTIFFBuildOverviewMetadata("NONE", this, osMetadata);

    int nCompression;
    uint16_t nPlanarConfig;
    uint16_t nPredictor;
    uint16_t nPhotometric;
    int nOvrJpegQuality;
    std::string osNoData;
    uint16_t *panExtraSampleValues = nullptr;
    uint16_t nExtraSamples = 0;
    if (!GetOverviewParameters(nCompression, nPlanarConfig, nPredictor,
                               nPhotometric, nOvrJpegQuality, osNoData,
                               panExtraSampleValues, nExtraSamples,
                               /*papszOptions=*/nullptr))
    {
        return CE_Failure;
    }

    /* -------------------------------------------------------------------- */
    /*      Do we have a palette?  If so, create a TIFF compatible version. */
    /* -------------------------------------------------------------------- */
    std::vector<unsigned short> anTRed;
    std::vector<unsigned short> anTGreen;
    std::vector<unsigned short> anTBlue;
    unsigned short *panRed = nullptr;
    unsigned short *panGreen = nullptr;
    unsigned short *panBlue = nullptr;

    if (nPhotometric == PHOTOMETRIC_PALETTE && m_poColorTable != nullptr)
    {
        CreateTIFFColorTable(m_poColorTable, nOvBitsPerSample, anTRed, anTGreen,
                             anTBlue, panRed, panGreen, panBlue);
    }

    int nOvrBlockXSize = 0;
    int nOvrBlockYSize = 0;
    GTIFFGetOverviewBlockSize(GDALRasterBand::ToHandle(GetRasterBand(1)),
                              &nOvrBlockXSize, &nOvrBlockYSize);

    CPLErr eErr = CE_None;

    for (int i = 0; i < nOverviews && eErr == CE_None; ++i)
    {
        GDALRasterBand *poOvrBand =
            poOvrDS ? ((i == 0) ? poOvrDS->GetRasterBand(1)
                                : poOvrDS->GetRasterBand(1)->GetOverview(i - 1))
                    : poSrcDS->GetRasterBand(1)->GetOverview(i);

        int nOXSize = poOvrBand->GetXSize();
        int nOYSize = poOvrBand->GetYSize();

        toff_t nOverviewOffset = GTIFFWriteDirectory(
            m_hTIFF, FILETYPE_REDUCEDIMAGE, nOXSize, nOYSize, nOvBitsPerSample,
            nPlanarConfig, m_nSamplesPerPixel, nOvrBlockXSize, nOvrBlockYSize,
            TRUE, nCompression, nPhotometric, m_nSampleFormat, nPredictor,
            panRed, panGreen, panBlue, nExtraSamples, panExtraSampleValues,
            osMetadata,
            nOvrJpegQuality >= 0 ? CPLSPrintf("%d", nOvrJpegQuality) : nullptr,
            CPLSPrintf("%d", m_nJpegTablesMode),
            osNoData.empty() ? nullptr : osNoData.c_str(),
            m_anLercAddCompressionAndVersion, m_bWriteCOGLayout);

        if (nOverviewOffset == 0)
            eErr = CE_Failure;
        else
            eErr = RegisterNewOverviewDataset(nOverviewOffset, nOvrJpegQuality,
                                              nullptr);
    }

    // For directory reloading, so that the chaining to the next directory is
    // reloaded, as well as compression parameters.
    ReloadDirectory();

    CPLFree(panExtraSampleValues);
    panExtraSampleValues = nullptr;

    return eErr;
}

/************************************************************************/
/*                           ReloadDirectory()                          */
/************************************************************************/

void GTiffDataset::ReloadDirectory(bool bReopenHandle)
{
    bool bNeedSetInvalidDir = true;
    if (bReopenHandle)
    {
        // When issuing a TIFFRewriteDirectory() or when a TIFFFlush() has
        // caused a move of the directory, we would need to invalidate the
        // tif_lastdiroff member, but it is not possible to do so without
        // re-opening the TIFF handle.
        auto hTIFFNew = VSI_TIFFReOpen(m_hTIFF);
        if (hTIFFNew != nullptr)
        {
            m_hTIFF = hTIFFNew;
            bNeedSetInvalidDir = false;  // we could do it, but not needed
        }
        else
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Cannot re-open TIFF handle for file %s. "
                     "Directory chaining may be corrupted !",
                     m_pszFilename);
        }
    }
    if (bNeedSetInvalidDir)
    {
        TIFFSetSubDirectory(m_hTIFF, 0);
    }
    CPL_IGNORE_RET_VAL(SetDirectory());
}

/************************************************************************/
/*                       CreateInternalMaskOverviews()                  */
/************************************************************************/

CPLErr GTiffDataset::CreateInternalMaskOverviews(int nOvrBlockXSize,
                                                 int nOvrBlockYSize)
{
    ScanDirectories();

    /* -------------------------------------------------------------------- */
    /*      Create overviews for the mask.                                  */
    /* -------------------------------------------------------------------- */
    CPLErr eErr = CE_None;

    const char *pszInternalMask =
        CPLGetConfigOption("GDAL_TIFF_INTERNAL_MASK", nullptr);
    if (m_poMaskDS != nullptr && m_poMaskDS->GetRasterCount() == 1 &&
        (pszInternalMask == nullptr || CPLTestBool(pszInternalMask)))
    {
        int nMaskOvrCompression;
        if (strstr(GDALGetMetadataItem(GDALGetDriverByName("GTiff"),
                                       GDAL_DMD_CREATIONOPTIONLIST, nullptr),
                   "<Value>DEFLATE</Value>") != nullptr)
            nMaskOvrCompression = COMPRESSION_ADOBE_DEFLATE;
        else
            nMaskOvrCompression = COMPRESSION_PACKBITS;

        for (int i = 0; i < m_nOverviewCount; ++i)
        {
            if (m_papoOverviewDS[i]->m_poMaskDS == nullptr)
            {
                const toff_t nOverviewOffset = GTIFFWriteDirectory(
                    m_hTIFF, FILETYPE_REDUCEDIMAGE | FILETYPE_MASK,
                    m_papoOverviewDS[i]->nRasterXSize,
                    m_papoOverviewDS[i]->nRasterYSize, 1, PLANARCONFIG_CONTIG,
                    1, nOvrBlockXSize, nOvrBlockYSize, TRUE,
                    nMaskOvrCompression, PHOTOMETRIC_MASK, SAMPLEFORMAT_UINT,
                    PREDICTOR_NONE, nullptr, nullptr, nullptr, 0, nullptr, "",
                    nullptr, nullptr, nullptr, nullptr, m_bWriteCOGLayout);

                if (nOverviewOffset == 0)
                {
                    eErr = CE_Failure;
                    continue;
                }

                GTiffDataset *poODS = new GTiffDataset();
                poODS->ShareLockWithParentDataset(this);
                poODS->m_pszFilename = CPLStrdup(m_pszFilename);
                if (poODS->OpenOffset(VSI_TIFFOpenChild(m_hTIFF),
                                      nOverviewOffset, GA_Update) != CE_None)
                {
                    delete poODS;
                    eErr = CE_Failure;
                }
                else
                {
                    poODS->m_bPromoteTo8Bits = CPLTestBool(CPLGetConfigOption(
                        "GDAL_TIFF_INTERNAL_MASK_TO_8BIT", "YES"));
                    poODS->m_poBaseDS = this;
                    poODS->m_poImageryDS = m_papoOverviewDS[i];
                    m_papoOverviewDS[i]->m_poMaskDS = poODS;
                    ++m_poMaskDS->m_nOverviewCount;
                    m_poMaskDS->m_papoOverviewDS =
                        static_cast<GTiffDataset **>(CPLRealloc(
                            m_poMaskDS->m_papoOverviewDS,
                            m_poMaskDS->m_nOverviewCount * (sizeof(void *))));
                    m_poMaskDS
                        ->m_papoOverviewDS[m_poMaskDS->m_nOverviewCount - 1] =
                        poODS;
                }
            }
        }
    }

    ReloadDirectory();

    return eErr;
}

/************************************************************************/
/*                          IBuildOverviews()                           */
/************************************************************************/

CPLErr GTiffDataset::IBuildOverviews(const char *pszResampling, int nOverviews,
                                     const int *panOverviewList, int nBandsIn,
                                     const int *panBandList,
                                     GDALProgressFunc pfnProgress,
                                     void *pProgressData,
                                     CSLConstList papszOptions)

{
    ScanDirectories();

    // Make implicit JPEG overviews invisible, but do not destroy
    // them in case they are already used (not sure that the client
    // has the right to do that.  Behavior maybe undefined in GDAL API.
    m_nJPEGOverviewCount = 0;

    /* -------------------------------------------------------------------- */
    /*      If RRD or external OVR overviews requested, then invoke         */
    /*      generic handling.                                               */
    /* -------------------------------------------------------------------- */
    bool bUseGenericHandling = false;

    if (CPLTestBool(CSLFetchNameValueDef(
            papszOptions, "USE_RRD", CPLGetConfigOption("USE_RRD", "NO"))) ||
        CPLTestBool(
            CSLFetchNameValueDef(papszOptions, "TIFF_USE_OVR",
                                 CPLGetConfigOption("TIFF_USE_OVR", "NO"))))
    {
        bUseGenericHandling = true;
    }

    /* -------------------------------------------------------------------- */
    /*      If we don't have read access, then create the overviews         */
    /*      externally.                                                     */
    /* -------------------------------------------------------------------- */
    if (GetAccess() != GA_Update)
    {
        CPLDebug("GTiff", "File open for read-only accessing, "
                          "creating overviews externally.");

        bUseGenericHandling = true;
    }

    if (bUseGenericHandling)
    {
        if (m_nOverviewCount != 0)
        {
            ReportError(CE_Failure, CPLE_NotSupported,
                        "Cannot add external overviews when there are already "
                        "internal overviews");
            return CE_Failure;
        }

        CPLStringList aosOptions(papszOptions);
        if (!m_bWriteEmptyTiles)
        {
            aosOptions.SetNameValue("SPARSE_OK", "YES");
        }

        CPLErr eErr = GDALDataset::IBuildOverviews(
            pszResampling, nOverviews, panOverviewList, nBandsIn, panBandList,
            pfnProgress, pProgressData, aosOptions);
        if (eErr == CE_None && m_poMaskDS)
        {
            ReportError(
                CE_Warning, CPLE_NotSupported,
                "Building external overviews whereas there is an internal "
                "mask is not fully supported. "
                "The overviews of the non-mask bands will be created, "
                "but not the overviews of the mask band.");
        }
        return eErr;
    }

    /* -------------------------------------------------------------------- */
    /*      Our TIFF overview support currently only works safely if all    */
    /*      bands are handled at the same time.                             */
    /* -------------------------------------------------------------------- */
    if (nBandsIn != GetRasterCount())
    {
        ReportError(CE_Failure, CPLE_NotSupported,
                    "Generation of overviews in TIFF currently only "
                    "supported when operating on all bands.  "
                    "Operation failed.");
        return CE_Failure;
    }

    /* -------------------------------------------------------------------- */
    /*      If zero overviews were requested, we need to clear all          */
    /*      existing overviews.                                             */
    /* -------------------------------------------------------------------- */
    if (nOverviews == 0)
    {
        if (m_nOverviewCount == 0)
            return GDALDataset::IBuildOverviews(
                pszResampling, nOverviews, panOverviewList, nBandsIn,
                panBandList, pfnProgress, pProgressData, papszOptions);

        return CleanOverviews();
    }

    CPLErr eErr = CE_None;

    /* -------------------------------------------------------------------- */
    /*      Initialize progress counter.                                    */
    /* -------------------------------------------------------------------- */
    if (!pfnProgress(0.0, nullptr, pProgressData))
    {
        ReportError(CE_Failure, CPLE_UserInterrupt, "User terminated");
        return CE_Failure;
    }

    FlushDirectory();

    /* -------------------------------------------------------------------- */
    /*      If we are averaging bit data to grayscale we need to create     */
    /*      8bit overviews.                                                 */
    /* -------------------------------------------------------------------- */
    int nOvBitsPerSample = m_nBitsPerSample;

    if (STARTS_WITH_CI(pszResampling, "AVERAGE_BIT2"))
        nOvBitsPerSample = 8;

    /* -------------------------------------------------------------------- */
    /*      Do we need some metadata for the overviews?                     */
    /* -------------------------------------------------------------------- */
    CPLString osMetadata;

    GTIFFBuildOverviewMetadata(pszResampling, this, osMetadata);

    int nCompression;
    uint16_t nPlanarConfig;
    uint16_t nPredictor;
    uint16_t nPhotometric;
    int nOvrJpegQuality;
    std::string osNoData;
    uint16_t *panExtraSampleValues = nullptr;
    uint16_t nExtraSamples = 0;
    if (!GetOverviewParameters(nCompression, nPlanarConfig, nPredictor,
                               nPhotometric, nOvrJpegQuality, osNoData,
                               panExtraSampleValues, nExtraSamples,
                               papszOptions))
    {
        return CE_Failure;
    }

    /* -------------------------------------------------------------------- */
    /*      Do we have a palette?  If so, create a TIFF compatible version. */
    /* -------------------------------------------------------------------- */
    std::vector<unsigned short> anTRed;
    std::vector<unsigned short> anTGreen;
    std::vector<unsigned short> anTBlue;
    unsigned short *panRed = nullptr;
    unsigned short *panGreen = nullptr;
    unsigned short *panBlue = nullptr;

    if (nPhotometric == PHOTOMETRIC_PALETTE && m_poColorTable != nullptr)
    {
        CreateTIFFColorTable(m_poColorTable, nOvBitsPerSample, anTRed, anTGreen,
                             anTBlue, panRed, panGreen, panBlue);
    }

    /* -------------------------------------------------------------------- */
    /*      Establish which of the overview levels we already have, and     */
    /*      which are new.  We assume that band 1 of the file is            */
    /*      representative.                                                 */
    /* -------------------------------------------------------------------- */
    int nOvrBlockXSize = 0;
    int nOvrBlockYSize = 0;
    GTIFFGetOverviewBlockSize(GDALRasterBand::ToHandle(GetRasterBand(1)),
                              &nOvrBlockXSize, &nOvrBlockYSize);
    std::vector<bool> abRequireNewOverview(nOverviews, true);
    for (int i = 0; i < nOverviews && eErr == CE_None; ++i)
    {
        for (int j = 0; j < m_nOverviewCount && eErr == CE_None; ++j)
        {
            GTiffDataset *poODS = m_papoOverviewDS[j];

            const int nOvFactor =
                GDALComputeOvFactor(poODS->GetRasterXSize(), GetRasterXSize(),
                                    poODS->GetRasterYSize(), GetRasterYSize());

            // If we already have a 1x1 overview and this new one would result
            // in it too, then don't create it.
            if (poODS->GetRasterXSize() == 1 && poODS->GetRasterYSize() == 1 &&
                (GetRasterXSize() + panOverviewList[i] - 1) /
                        panOverviewList[i] ==
                    1 &&
                (GetRasterYSize() + panOverviewList[i] - 1) /
                        panOverviewList[i] ==
                    1)
            {
                abRequireNewOverview[i] = false;
                break;
            }

            if (nOvFactor == panOverviewList[i] ||
                nOvFactor == GDALOvLevelAdjust2(panOverviewList[i],
                                                GetRasterXSize(),
                                                GetRasterYSize()))
            {
                abRequireNewOverview[i] = false;
                break;
            }
        }

        if (abRequireNewOverview[i])
        {
            if (m_bLayoutIFDSBeforeData && !m_bKnownIncompatibleEdition &&
                !m_bWriteKnownIncompatibleEdition)
            {
                ReportError(CE_Warning, CPLE_AppDefined,
                            "Adding new overviews invalidates the "
                            "LAYOUT=IFDS_BEFORE_DATA property");
                m_bKnownIncompatibleEdition = true;
                m_bWriteKnownIncompatibleEdition = true;
            }

            const int nOXSize = (GetRasterXSize() + panOverviewList[i] - 1) /
                                panOverviewList[i];
            const int nOYSize = (GetRasterYSize() + panOverviewList[i] - 1) /
                                panOverviewList[i];

            const toff_t nOverviewOffset = GTIFFWriteDirectory(
                m_hTIFF, FILETYPE_REDUCEDIMAGE, nOXSize, nOYSize,
                nOvBitsPerSample, nPlanarConfig, m_nSamplesPerPixel,
                nOvrBlockXSize, nOvrBlockYSize, TRUE, nCompression,
                nPhotometric, m_nSampleFormat, nPredictor, panRed, panGreen,
                panBlue, nExtraSamples, panExtraSampleValues, osMetadata,
                nOvrJpegQuality >= 0 ? CPLSPrintf("%d", nOvrJpegQuality)
                                     : nullptr,
                CPLSPrintf("%d", m_nJpegTablesMode),
                osNoData.empty() ? nullptr : osNoData.c_str(),
                m_anLercAddCompressionAndVersion, false);

            if (nOverviewOffset == 0)
                eErr = CE_Failure;
            else
                eErr = RegisterNewOverviewDataset(
                    nOverviewOffset, nOvrJpegQuality, papszOptions);
        }
    }

    CPLFree(panExtraSampleValues);
    panExtraSampleValues = nullptr;

    ReloadDirectory();

    /* -------------------------------------------------------------------- */
    /*      Create overviews for the mask.                                  */
    /* -------------------------------------------------------------------- */
    if (eErr != CE_None)
        return eErr;

    eErr = CreateInternalMaskOverviews(nOvrBlockXSize, nOvrBlockYSize);

    /* -------------------------------------------------------------------- */
    /*      Refresh overviews for the mask                                  */
    /* -------------------------------------------------------------------- */
    if (m_poMaskDS != nullptr && m_poMaskDS->GetRasterCount() == 1)
    {
        int nMaskOverviews = 0;

        GDALRasterBand **papoOverviewBands = static_cast<GDALRasterBand **>(
            CPLCalloc(sizeof(void *), m_nOverviewCount));
        for (int i = 0; i < m_nOverviewCount; ++i)
        {
            if (m_papoOverviewDS[i]->m_poMaskDS != nullptr)
            {
                papoOverviewBands[nMaskOverviews++] =
                    m_papoOverviewDS[i]->m_poMaskDS->GetRasterBand(1);
            }
        }

        eErr = GDALRegenerateOverviewsEx(
            m_poMaskDS->GetRasterBand(1), nMaskOverviews,
            reinterpret_cast<GDALRasterBandH *>(papoOverviewBands),
            pszResampling, GDALDummyProgress, nullptr, papszOptions);
        CPLFree(papoOverviewBands);
    }

    // If we have an alpha band, we want it to be generated before downsampling
    // other bands
    bool bHasAlphaBand = false;
    for (int iBand = 0; iBand < nBands; iBand++)
    {
        if (papoBands[iBand]->GetColorInterpretation() == GCI_AlphaBand)
            bHasAlphaBand = true;
    }

    /* -------------------------------------------------------------------- */
    /*      Refresh old overviews that were listed.                         */
    /* -------------------------------------------------------------------- */
    const auto poColorTable = GetRasterBand(panBandList[0])->GetColorTable();
    if ((m_nPlanarConfig == PLANARCONFIG_CONTIG || bHasAlphaBand) &&
        GDALDataTypeIsComplex(
            GetRasterBand(panBandList[0])->GetRasterDataType()) == FALSE &&
        (poColorTable == nullptr || STARTS_WITH_CI(pszResampling, "NEAR") ||
         poColorTable->IsIdentity()) &&
        (STARTS_WITH_CI(pszResampling, "NEAR") ||
         EQUAL(pszResampling, "AVERAGE") || EQUAL(pszResampling, "RMS") ||
         EQUAL(pszResampling, "GAUSS") || EQUAL(pszResampling, "CUBIC") ||
         EQUAL(pszResampling, "CUBICSPLINE") ||
         EQUAL(pszResampling, "LANCZOS") || EQUAL(pszResampling, "BILINEAR") ||
         EQUAL(pszResampling, "MODE")))
    {
        // In the case of pixel interleaved compressed overviews, we want to
        // generate the overviews for all the bands block by block, and not
        // band after band, in order to write the block once and not loose
        // space in the TIFF file.  We also use that logic for uncompressed
        // overviews, since GDALRegenerateOverviewsMultiBand() will be able to
        // trigger cascading overview regeneration even in the presence
        // of an alpha band.

        int nNewOverviews = 0;

        GDALRasterBand ***papapoOverviewBands = static_cast<GDALRasterBand ***>(
            CPLCalloc(sizeof(void *), nBandsIn));
        GDALRasterBand **papoBandList =
            static_cast<GDALRasterBand **>(CPLCalloc(sizeof(void *), nBandsIn));
        for (int iBand = 0; iBand < nBandsIn; ++iBand)
        {
            GDALRasterBand *poBand = GetRasterBand(panBandList[iBand]);

            papoBandList[iBand] = poBand;
            papapoOverviewBands[iBand] = static_cast<GDALRasterBand **>(
                CPLCalloc(sizeof(void *), poBand->GetOverviewCount()));

            int iCurOverview = 0;
            std::vector<bool> abAlreadyUsedOverviewBand(
                poBand->GetOverviewCount(), false);

            for (int i = 0; i < nOverviews; ++i)
            {
                for (int j = 0; j < poBand->GetOverviewCount(); ++j)
                {
                    if (abAlreadyUsedOverviewBand[j])
                        continue;

                    int nOvFactor;
                    GDALRasterBand *poOverview = poBand->GetOverview(j);

                    nOvFactor = GDALComputeOvFactor(
                        poOverview->GetXSize(), poBand->GetXSize(),
                        poOverview->GetYSize(), poBand->GetYSize());

                    GDALCopyNoDataValue(poOverview, poBand);

                    if (nOvFactor == panOverviewList[i] ||
                        nOvFactor == GDALOvLevelAdjust2(panOverviewList[i],
                                                        poBand->GetXSize(),
                                                        poBand->GetYSize()))
                    {
                        abAlreadyUsedOverviewBand[j] = true;
                        CPLAssert(iCurOverview < poBand->GetOverviewCount());
                        papapoOverviewBands[iBand][iCurOverview] = poOverview;
                        ++iCurOverview;
                        break;
                    }
                }
            }

            if (nNewOverviews == 0)
            {
                nNewOverviews = iCurOverview;
            }
            else if (nNewOverviews != iCurOverview)
            {
                CPLAssert(false);
                return CE_Failure;
            }
        }

        GDALRegenerateOverviewsMultiBand(
            nBandsIn, papoBandList, nNewOverviews, papapoOverviewBands,
            pszResampling, pfnProgress, pProgressData, papszOptions);

        for (int iBand = 0; iBand < nBandsIn; ++iBand)
        {
            CPLFree(papapoOverviewBands[iBand]);
        }
        CPLFree(papapoOverviewBands);
        CPLFree(papoBandList);
    }
    else
    {
        GDALRasterBand **papoOverviewBands = static_cast<GDALRasterBand **>(
            CPLCalloc(sizeof(void *), nOverviews));

        for (int iBand = 0; iBand < nBandsIn && eErr == CE_None; ++iBand)
        {
            GDALRasterBand *poBand = GetRasterBand(panBandList[iBand]);
            if (poBand == nullptr)
            {
                eErr = CE_Failure;
                break;
            }

            std::vector<bool> abAlreadyUsedOverviewBand(
                poBand->GetOverviewCount(), false);

            int nNewOverviews = 0;
            for (int i = 0; i < nOverviews; ++i)
            {
                for (int j = 0; j < poBand->GetOverviewCount(); ++j)
                {
                    if (abAlreadyUsedOverviewBand[j])
                        continue;

                    GDALRasterBand *poOverview = poBand->GetOverview(j);

                    GDALCopyNoDataValue(poOverview, poBand);

                    const int nOvFactor = GDALComputeOvFactor(
                        poOverview->GetXSize(), poBand->GetXSize(),
                        poOverview->GetYSize(), poBand->GetYSize());

                    if (nOvFactor == panOverviewList[i] ||
                        nOvFactor == GDALOvLevelAdjust2(panOverviewList[i],
                                                        poBand->GetXSize(),
                                                        poBand->GetYSize()))
                    {
                        abAlreadyUsedOverviewBand[j] = true;
                        CPLAssert(nNewOverviews < poBand->GetOverviewCount());
                        papoOverviewBands[nNewOverviews++] = poOverview;
                        break;
                    }
                }
            }

            void *pScaledProgressData = GDALCreateScaledProgress(
                iBand / static_cast<double>(nBandsIn),
                (iBand + 1) / static_cast<double>(nBandsIn), pfnProgress,
                pProgressData);

            eErr = GDALRegenerateOverviewsEx(
                poBand, nNewOverviews,
                reinterpret_cast<GDALRasterBandH *>(papoOverviewBands),
                pszResampling, GDALScaledProgress, pScaledProgressData,
                papszOptions);

            GDALDestroyScaledProgress(pScaledProgressData);
        }

        /* --------------------------------------------------------------------
         */
        /*      Cleanup */
        /* --------------------------------------------------------------------
         */
        CPLFree(papoOverviewBands);
    }

    pfnProgress(1.0, nullptr, pProgressData);

    return eErr;
}

/************************************************************************/
/*                      GTiffWriteDummyGeokeyDirectory()                */
/************************************************************************/

static void GTiffWriteDummyGeokeyDirectory(TIFF *hTIFF)
{
    // If we have existing geokeys, try to wipe them
    // by writing a dummy geokey directory. (#2546)
    uint16_t *panVI = nullptr;
    uint16_t nKeyCount = 0;

    if (TIFFGetField(hTIFF, TIFFTAG_GEOKEYDIRECTORY, &nKeyCount, &panVI))
    {
        GUInt16 anGKVersionInfo[4] = {1, 1, 0, 0};
        double adfDummyDoubleParams[1] = {0.0};
        TIFFSetField(hTIFF, TIFFTAG_GEOKEYDIRECTORY, 4, anGKVersionInfo);
        TIFFSetField(hTIFF, TIFFTAG_GEODOUBLEPARAMS, 1, adfDummyDoubleParams);
        TIFFSetField(hTIFF, TIFFTAG_GEOASCIIPARAMS, "");
    }
}

/************************************************************************/
/*               GTiffDatasetLibGeotiffErrorCallback()                  */
/************************************************************************/

static void GTiffDatasetLibGeotiffErrorCallback(GTIF *, int level,
                                                const char *pszMsg, ...)
{
    va_list ap;
    va_start(ap, pszMsg);
    CPLErrorV((level == LIBGEOTIFF_WARNING) ? CE_Warning : CE_Failure,
              CPLE_AppDefined, pszMsg, ap);
    va_end(ap);
}

/************************************************************************/
/*                           GTiffDatasetGTIFNew()                      */
/************************************************************************/

static GTIF *GTiffDatasetGTIFNew(TIFF *hTIFF)
{
    GTIF *gtif = GTIFNewEx(hTIFF, GTiffDatasetLibGeotiffErrorCallback, nullptr);
    if (gtif)
    {
        GTIFAttachPROJContext(gtif, OSRGetProjTLSContext());
    }
    return gtif;
}

/************************************************************************/
/*                    IsSRSCompatibleOfGeoTIFF()                        */
/************************************************************************/

static bool IsSRSCompatibleOfGeoTIFF(const OGRSpatialReference *poSRS)
{
    char *pszWKT = nullptr;
    OGRErr eErr;
    {
        CPLErrorStateBackuper oErrorStateBackuper;
        CPLErrorHandlerPusher oErrorHandler(CPLQuietErrorHandler);
        if (poSRS->IsDerivedGeographic())
            eErr = OGRERR_FAILURE;
        else
        {
            // Geographic3D CRS can't be exported to WKT1, but are
            // valid GeoTIFF 1.1
            const char *const apszOptions[] = {
                poSRS->IsGeographic() ? nullptr : "FORMAT=WKT1", nullptr};
            eErr = poSRS->exportToWkt(&pszWKT, apszOptions);
        }
    }
    const bool bCompatibleOfGeoTIFF =
        (eErr == OGRERR_NONE && pszWKT != nullptr &&
         strstr(pszWKT, "custom_proj4") == nullptr);
    CPLFree(pszWKT);
    return bCompatibleOfGeoTIFF;
}

/************************************************************************/
/*                          WriteGeoTIFFInfo()                          */
/************************************************************************/

void GTiffDataset::WriteGeoTIFFInfo()

{
    bool bPixelIsPoint = false;
    bool bPointGeoIgnore = false;

    const char *pszAreaOrPoint =
        GTiffDataset::GetMetadataItem(GDALMD_AREA_OR_POINT);
    if (pszAreaOrPoint && EQUAL(pszAreaOrPoint, GDALMD_AOP_POINT))
    {
        bPixelIsPoint = true;
        bPointGeoIgnore =
            CPLTestBool(CPLGetConfigOption("GTIFF_POINT_GEO_IGNORE", "FALSE"));
    }

    if (m_bForceUnsetGTOrGCPs)
    {
        m_bNeedsRewrite = true;
        m_bForceUnsetGTOrGCPs = false;

        TIFFUnsetField(m_hTIFF, TIFFTAG_GEOPIXELSCALE);
        TIFFUnsetField(m_hTIFF, TIFFTAG_GEOTIEPOINTS);
        TIFFUnsetField(m_hTIFF, TIFFTAG_GEOTRANSMATRIX);
    }

    if (m_bForceUnsetProjection)
    {
        m_bNeedsRewrite = true;
        m_bForceUnsetProjection = false;

        TIFFUnsetField(m_hTIFF, TIFFTAG_GEOKEYDIRECTORY);
        TIFFUnsetField(m_hTIFF, TIFFTAG_GEODOUBLEPARAMS);
        TIFFUnsetField(m_hTIFF, TIFFTAG_GEOASCIIPARAMS);
    }

    /* -------------------------------------------------------------------- */
    /*      Write geotransform if valid.                                    */
    /* -------------------------------------------------------------------- */
    if (m_bGeoTransformValid)
    {
        m_bNeedsRewrite = true;

        /* --------------------------------------------------------------------
         */
        /*      Clear old tags to ensure we don't end up with conflicting */
        /*      information. (#2625) */
        /* --------------------------------------------------------------------
         */
        TIFFUnsetField(m_hTIFF, TIFFTAG_GEOPIXELSCALE);
        TIFFUnsetField(m_hTIFF, TIFFTAG_GEOTIEPOINTS);
        TIFFUnsetField(m_hTIFF, TIFFTAG_GEOTRANSMATRIX);

        /* --------------------------------------------------------------------
         */
        /*      Write the transform.  If we have a normal north-up image we */
        /*      use the tiepoint plus pixelscale otherwise we use a matrix. */
        /* --------------------------------------------------------------------
         */
        if (m_adfGeoTransform[2] == 0.0 && m_adfGeoTransform[4] == 0.0 &&
            m_adfGeoTransform[5] < 0.0)
        {
            double dfOffset = 0.0;
            if (m_eProfile != GTiffProfile::BASELINE)
            {
                // In the case the SRS has a vertical component and we have
                // a single band, encode its scale/offset in the GeoTIFF tags
                int bHasScale = FALSE;
                double dfScale = GetRasterBand(1)->GetScale(&bHasScale);
                int bHasOffset = FALSE;
                dfOffset = GetRasterBand(1)->GetOffset(&bHasOffset);
                const bool bApplyScaleOffset =
                    m_oSRS.IsVertical() && GetRasterCount() == 1;
                if (bApplyScaleOffset && !bHasScale)
                    dfScale = 1.0;
                if (!bApplyScaleOffset || !bHasOffset)
                    dfOffset = 0.0;
                const double adfPixelScale[3] = {
                    m_adfGeoTransform[1], fabs(m_adfGeoTransform[5]),
                    bApplyScaleOffset ? dfScale : 0.0};
                TIFFSetField(m_hTIFF, TIFFTAG_GEOPIXELSCALE, 3, adfPixelScale);
            }

            double adfTiePoints[6] = {
                0.0,     0.0, 0.0, m_adfGeoTransform[0], m_adfGeoTransform[3],
                dfOffset};

            if (bPixelIsPoint && !bPointGeoIgnore)
            {
                adfTiePoints[3] +=
                    m_adfGeoTransform[1] * 0.5 + m_adfGeoTransform[2] * 0.5;
                adfTiePoints[4] +=
                    m_adfGeoTransform[4] * 0.5 + m_adfGeoTransform[5] * 0.5;
            }

            if (m_eProfile != GTiffProfile::BASELINE)
                TIFFSetField(m_hTIFF, TIFFTAG_GEOTIEPOINTS, 6, adfTiePoints);
        }
        else
        {
            double adfMatrix[16] = {};

            adfMatrix[0] = m_adfGeoTransform[1];
            adfMatrix[1] = m_adfGeoTransform[2];
            adfMatrix[3] = m_adfGeoTransform[0];
            adfMatrix[4] = m_adfGeoTransform[4];
            adfMatrix[5] = m_adfGeoTransform[5];
            adfMatrix[7] = m_adfGeoTransform[3];
            adfMatrix[15] = 1.0;

            if (bPixelIsPoint && !bPointGeoIgnore)
            {
                adfMatrix[3] +=
                    m_adfGeoTransform[1] * 0.5 + m_adfGeoTransform[2] * 0.5;
                adfMatrix[7] +=
                    m_adfGeoTransform[4] * 0.5 + m_adfGeoTransform[5] * 0.5;
            }

            if (m_eProfile != GTiffProfile::BASELINE)
                TIFFSetField(m_hTIFF, TIFFTAG_GEOTRANSMATRIX, 16, adfMatrix);
        }

        // Do we need a world file?
        if (CPLFetchBool(m_papszCreationOptions, "TFW", false))
            GDALWriteWorldFile(m_pszFilename, "tfw", m_adfGeoTransform);
        else if (CPLFetchBool(m_papszCreationOptions, "WORLDFILE", false))
            GDALWriteWorldFile(m_pszFilename, "wld", m_adfGeoTransform);
    }
    else if (GetGCPCount() > 0)
    {
        m_bNeedsRewrite = true;

        double *padfTiePoints = static_cast<double *>(
            CPLMalloc(6 * sizeof(double) * GetGCPCount()));

        for (int iGCP = 0; iGCP < GetGCPCount(); ++iGCP)
        {

            padfTiePoints[iGCP * 6 + 0] = m_pasGCPList[iGCP].dfGCPPixel;
            padfTiePoints[iGCP * 6 + 1] = m_pasGCPList[iGCP].dfGCPLine;
            padfTiePoints[iGCP * 6 + 2] = 0;
            padfTiePoints[iGCP * 6 + 3] = m_pasGCPList[iGCP].dfGCPX;
            padfTiePoints[iGCP * 6 + 4] = m_pasGCPList[iGCP].dfGCPY;
            padfTiePoints[iGCP * 6 + 5] = m_pasGCPList[iGCP].dfGCPZ;

            if (bPixelIsPoint && !bPointGeoIgnore)
            {
                padfTiePoints[iGCP * 6 + 0] += 0.5;
                padfTiePoints[iGCP * 6 + 1] += 0.5;
            }
        }

        if (m_eProfile != GTiffProfile::BASELINE)
            TIFFSetField(m_hTIFF, TIFFTAG_GEOTIEPOINTS, 6 * GetGCPCount(),
                         padfTiePoints);
        CPLFree(padfTiePoints);
    }

    /* -------------------------------------------------------------------- */
    /*      Write out projection definition.                                */
    /* -------------------------------------------------------------------- */
    const bool bHasProjection = !m_oSRS.IsEmpty();
    if ((bHasProjection || bPixelIsPoint) &&
        m_eProfile != GTiffProfile::BASELINE)
    {
        m_bNeedsRewrite = true;

        // If we have existing geokeys, try to wipe them
        // by writing a dummy geokey directory. (#2546)
        GTiffWriteDummyGeokeyDirectory(m_hTIFF);

        GTIF *psGTIF = GTiffDatasetGTIFNew(m_hTIFF);

        // Set according to coordinate system.
        if (bHasProjection)
        {
            if (IsSRSCompatibleOfGeoTIFF(&m_oSRS))
            {
                GTIFSetFromOGISDefnEx(psGTIF,
                                      OGRSpatialReference::ToHandle(&m_oSRS),
                                      m_eGeoTIFFKeysFlavor, m_eGeoTIFFVersion);
            }
            else
            {
                GDALPamDataset::SetSpatialRef(&m_oSRS);
            }
        }

        if (bPixelIsPoint)
        {
            GTIFKeySet(psGTIF, GTRasterTypeGeoKey, TYPE_SHORT, 1,
                       RasterPixelIsPoint);
        }

        GTIFWriteKeys(psGTIF);
        GTIFFree(psGTIF);
    }
}

/************************************************************************/
/*                         AppendMetadataItem()                         */
/************************************************************************/

static void AppendMetadataItem(CPLXMLNode **ppsRoot, CPLXMLNode **ppsTail,
                               const char *pszKey, const char *pszValue,
                               int nBand, const char *pszRole,
                               const char *pszDomain)

{
    /* -------------------------------------------------------------------- */
    /*      Create the Item element, and subcomponents.                     */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psItem = CPLCreateXMLNode(nullptr, CXT_Element, "Item");
    CPLCreateXMLNode(CPLCreateXMLNode(psItem, CXT_Attribute, "name"), CXT_Text,
                     pszKey);

    if (nBand > 0)
    {
        char szBandId[32] = {};
        snprintf(szBandId, sizeof(szBandId), "%d", nBand - 1);
        CPLCreateXMLNode(CPLCreateXMLNode(psItem, CXT_Attribute, "sample"),
                         CXT_Text, szBandId);
    }

    if (pszRole != nullptr)
        CPLCreateXMLNode(CPLCreateXMLNode(psItem, CXT_Attribute, "role"),
                         CXT_Text, pszRole);

    if (pszDomain != nullptr && strlen(pszDomain) > 0)
        CPLCreateXMLNode(CPLCreateXMLNode(psItem, CXT_Attribute, "domain"),
                         CXT_Text, pszDomain);

    // Note: this escaping should not normally be done, as the serialization
    // of the tree to XML also does it, so we end up width double XML escaping,
    // but keep it for backward compatibility.
    char *pszEscapedItemValue = CPLEscapeString(pszValue, -1, CPLES_XML);
    CPLCreateXMLNode(psItem, CXT_Text, pszEscapedItemValue);
    CPLFree(pszEscapedItemValue);

    /* -------------------------------------------------------------------- */
    /*      Create root, if missing.                                        */
    /* -------------------------------------------------------------------- */
    if (*ppsRoot == nullptr)
        *ppsRoot = CPLCreateXMLNode(nullptr, CXT_Element, "GDALMetadata");

    /* -------------------------------------------------------------------- */
    /*      Append item to tail.  We keep track of the tail to avoid        */
    /*      O(nsquared) time as the list gets longer.                       */
    /* -------------------------------------------------------------------- */
    if (*ppsTail == nullptr)
        CPLAddXMLChild(*ppsRoot, psItem);
    else
        CPLAddXMLSibling(*ppsTail, psItem);

    *ppsTail = psItem;
}

/************************************************************************/
/*                         WriteMDMetadata()                            */
/************************************************************************/

static void WriteMDMetadata(GDALMultiDomainMetadata *poMDMD, TIFF *hTIFF,
                            CPLXMLNode **ppsRoot, CPLXMLNode **ppsTail,
                            int nBand, GTiffProfile eProfile)

{

    /* ==================================================================== */
    /*      Process each domain.                                            */
    /* ==================================================================== */
    char **papszDomainList = poMDMD->GetDomainList();
    for (int iDomain = 0; papszDomainList && papszDomainList[iDomain];
         ++iDomain)
    {
        char **papszMD = poMDMD->GetMetadata(papszDomainList[iDomain]);
        bool bIsXML = false;

        if (EQUAL(papszDomainList[iDomain], "IMAGE_STRUCTURE"))
            continue;  // Ignored.
        if (EQUAL(papszDomainList[iDomain], "COLOR_PROFILE"))
            continue;  // Ignored.
        if (EQUAL(papszDomainList[iDomain], MD_DOMAIN_RPC))
            continue;  // Handled elsewhere.
        if (EQUAL(papszDomainList[iDomain], "xml:ESRI") &&
            CPLTestBool(CPLGetConfigOption("ESRI_XML_PAM", "NO")))
            continue;  // Handled elsewhere.
        if (EQUAL(papszDomainList[iDomain], "xml:XMP"))
            continue;  // Handled in SetMetadata.

        if (STARTS_WITH_CI(papszDomainList[iDomain], "xml:"))
            bIsXML = true;

        /* --------------------------------------------------------------------
         */
        /*      Process each item in this domain. */
        /* --------------------------------------------------------------------
         */
        for (int iItem = 0; papszMD && papszMD[iItem]; ++iItem)
        {
            const char *pszItemValue = nullptr;
            char *pszItemName = nullptr;

            if (bIsXML)
            {
                pszItemName = CPLStrdup("doc");
                pszItemValue = papszMD[iItem];
            }
            else
            {
                pszItemValue = CPLParseNameValue(papszMD[iItem], &pszItemName);
                if (pszItemName == nullptr)
                {
                    CPLDebug("GTiff", "Invalid metadata item : %s",
                             papszMD[iItem]);
                    continue;
                }
            }

            /* --------------------------------------------------------------------
             */
            /*      Convert into XML item or handle as a special TIFF tag. */
            /* --------------------------------------------------------------------
             */
            if (strlen(papszDomainList[iDomain]) == 0 && nBand == 0 &&
                (STARTS_WITH_CI(pszItemName, "TIFFTAG_") ||
                 (EQUAL(pszItemName, "GEO_METADATA") &&
                  eProfile == GTiffProfile::GDALGEOTIFF) ||
                 (EQUAL(pszItemName, "TIFF_RSID") &&
                  eProfile == GTiffProfile::GDALGEOTIFF)))
            {
                if (EQUAL(pszItemName, "TIFFTAG_RESOLUTIONUNIT"))
                {
                    // ResolutionUnit can't be 0, which is the default if
                    // atoi() fails.  Set to 1=Unknown.
                    int v = atoi(pszItemValue);
                    if (!v)
                        v = RESUNIT_NONE;
                    TIFFSetField(hTIFF, TIFFTAG_RESOLUTIONUNIT, v);
                }
                else
                {
                    bool bFoundTag = false;
                    size_t iTag = 0;  // Used after for.
                    for (; iTag < sizeof(asTIFFTags) / sizeof(asTIFFTags[0]);
                         ++iTag)
                    {
                        if (EQUAL(pszItemName, asTIFFTags[iTag].pszTagName))
                        {
                            bFoundTag = true;
                            break;
                        }
                    }

                    if (bFoundTag &&
                        asTIFFTags[iTag].eType == GTIFFTAGTYPE_STRING)
                        TIFFSetField(hTIFF, asTIFFTags[iTag].nTagVal,
                                     pszItemValue);
                    else if (bFoundTag &&
                             asTIFFTags[iTag].eType == GTIFFTAGTYPE_FLOAT)
                        TIFFSetField(hTIFF, asTIFFTags[iTag].nTagVal,
                                     CPLAtof(pszItemValue));
                    else if (bFoundTag &&
                             asTIFFTags[iTag].eType == GTIFFTAGTYPE_SHORT)
                        TIFFSetField(hTIFF, asTIFFTags[iTag].nTagVal,
                                     atoi(pszItemValue));
                    else if (bFoundTag &&
                             asTIFFTags[iTag].eType == GTIFFTAGTYPE_BYTE_STRING)
                    {
                        uint32_t nLen =
                            static_cast<uint32_t>(strlen(pszItemValue));
                        if (nLen)
                        {
                            TIFFSetField(hTIFF, asTIFFTags[iTag].nTagVal, nLen,
                                         pszItemValue);
                        }
                    }
                    else
                        CPLError(CE_Warning, CPLE_NotSupported,
                                 "%s metadata item is unhandled and "
                                 "will not be written",
                                 pszItemName);
                }
            }
            else if (nBand == 0 && EQUAL(pszItemName, GDALMD_AREA_OR_POINT))
            {
                /* Do nothing, handled elsewhere. */;
            }
            else
            {
                AppendMetadataItem(ppsRoot, ppsTail, pszItemName, pszItemValue,
                                   nBand, nullptr, papszDomainList[iDomain]);
            }

            CPLFree(pszItemName);
        }

        /* --------------------------------------------------------------------
         */
        /*      Remove TIFFTAG_xxxxxx that are already set but no longer in */
        /*      the metadata list (#5619) */
        /* --------------------------------------------------------------------
         */
        if (strlen(papszDomainList[iDomain]) == 0 && nBand == 0)
        {
            for (size_t iTag = 0;
                 iTag < sizeof(asTIFFTags) / sizeof(asTIFFTags[0]); ++iTag)
            {
                uint32_t nCount = 0;
                char *pszText = nullptr;
                int16_t nVal = 0;
                float fVal = 0.0f;
                const char *pszVal =
                    CSLFetchNameValue(papszMD, asTIFFTags[iTag].pszTagName);
                if (pszVal == nullptr &&
                    ((asTIFFTags[iTag].eType == GTIFFTAGTYPE_STRING &&
                      TIFFGetField(hTIFF, asTIFFTags[iTag].nTagVal,
                                   &pszText)) ||
                     (asTIFFTags[iTag].eType == GTIFFTAGTYPE_SHORT &&
                      TIFFGetField(hTIFF, asTIFFTags[iTag].nTagVal, &nVal)) ||
                     (asTIFFTags[iTag].eType == GTIFFTAGTYPE_FLOAT &&
                      TIFFGetField(hTIFF, asTIFFTags[iTag].nTagVal, &fVal)) ||
                     (asTIFFTags[iTag].eType == GTIFFTAGTYPE_BYTE_STRING &&
                      TIFFGetField(hTIFF, asTIFFTags[iTag].nTagVal, &nCount,
                                   &pszText))))
                {
                    TIFFUnsetField(hTIFF, asTIFFTags[iTag].nTagVal);
                }
            }
        }
    }
}

/************************************************************************/
/*                           WriteRPC()                                 */
/************************************************************************/

void GTiffDataset::WriteRPC(GDALDataset *poSrcDS, TIFF *l_hTIFF,
                            int bSrcIsGeoTIFF, GTiffProfile eProfile,
                            const char *pszTIFFFilename,
                            char **l_papszCreationOptions,
                            bool bWriteOnlyInPAMIfNeeded)
{
    /* -------------------------------------------------------------------- */
    /*      Handle RPC data written to TIFF RPCCoefficient tag, RPB file,   */
    /*      RPCTEXT file or PAM.                                            */
    /* -------------------------------------------------------------------- */
    char **papszRPCMD = poSrcDS->GetMetadata(MD_DOMAIN_RPC);
    if (papszRPCMD != nullptr)
    {
        bool bRPCSerializedOtherWay = false;

        if (eProfile == GTiffProfile::GDALGEOTIFF)
        {
            if (!bWriteOnlyInPAMIfNeeded)
                GTiffDatasetWriteRPCTag(l_hTIFF, papszRPCMD);
            bRPCSerializedOtherWay = true;
        }

        // Write RPB file if explicitly asked, or if a non GDAL specific
        // profile is selected and RPCTXT is not asked.
        bool bRPBExplicitlyAsked =
            CPLFetchBool(l_papszCreationOptions, "RPB", false);
        bool bRPBExplicitlyDenied =
            !CPLFetchBool(l_papszCreationOptions, "RPB", true);
        if ((eProfile != GTiffProfile::GDALGEOTIFF &&
             !CPLFetchBool(l_papszCreationOptions, "RPCTXT", false) &&
             !bRPBExplicitlyDenied) ||
            bRPBExplicitlyAsked)
        {
            if (!bWriteOnlyInPAMIfNeeded)
                GDALWriteRPBFile(pszTIFFFilename, papszRPCMD);
            bRPCSerializedOtherWay = true;
        }

        if (CPLFetchBool(l_papszCreationOptions, "RPCTXT", false))
        {
            if (!bWriteOnlyInPAMIfNeeded)
                GDALWriteRPCTXTFile(pszTIFFFilename, papszRPCMD);
            bRPCSerializedOtherWay = true;
        }

        if (!bRPCSerializedOtherWay && bWriteOnlyInPAMIfNeeded && bSrcIsGeoTIFF)
            cpl::down_cast<GTiffDataset *>(poSrcDS)
                ->GDALPamDataset::SetMetadata(papszRPCMD, MD_DOMAIN_RPC);
    }
}

/************************************************************************/
/*                 GTIFFIsStandardColorInterpretation()                 */
/************************************************************************/

bool GTIFFIsStandardColorInterpretation(GDALDatasetH hSrcDS,
                                        uint16_t nPhotometric,
                                        CSLConstList papszCreationOptions)
{
    GDALDataset *poSrcDS = GDALDataset::FromHandle(hSrcDS);
    bool bStandardColorInterp = true;
    if (nPhotometric == PHOTOMETRIC_MINISBLACK)
    {
        for (int i = 0; i < poSrcDS->GetRasterCount(); ++i)
        {
            const GDALColorInterp eInterp =
                poSrcDS->GetRasterBand(i + 1)->GetColorInterpretation();
            if (!(eInterp == GCI_GrayIndex || eInterp == GCI_Undefined ||
                  (i > 0 && eInterp == GCI_AlphaBand)))
            {
                bStandardColorInterp = false;
                break;
            }
        }
    }
    else if (nPhotometric == PHOTOMETRIC_PALETTE)
    {
        bStandardColorInterp =
            poSrcDS->GetRasterBand(1)->GetColorInterpretation() ==
            GCI_PaletteIndex;
    }
    else if (nPhotometric == PHOTOMETRIC_RGB)
    {
        int iStart = 0;
        if (EQUAL(CSLFetchNameValueDef(papszCreationOptions, "PHOTOMETRIC", ""),
                  "RGB"))
        {
            iStart = 3;
            if (poSrcDS->GetRasterCount() == 4 &&
                CSLFetchNameValue(papszCreationOptions, "ALPHA") != nullptr)
            {
                iStart = 4;
            }
        }
        for (int i = iStart; i < poSrcDS->GetRasterCount(); ++i)
        {
            const GDALColorInterp eInterp =
                poSrcDS->GetRasterBand(i + 1)->GetColorInterpretation();
            if (!((i == 0 && eInterp == GCI_RedBand) ||
                  (i == 1 && eInterp == GCI_GreenBand) ||
                  (i == 2 && eInterp == GCI_BlueBand) ||
                  (i >= 3 &&
                   (eInterp == GCI_Undefined || eInterp == GCI_AlphaBand))))
            {
                bStandardColorInterp = false;
                break;
            }
        }
    }
    else if (nPhotometric == PHOTOMETRIC_YCBCR &&
             poSrcDS->GetRasterCount() == 3)
    {
        // do nothing
    }
    else
    {
        bStandardColorInterp = false;
    }
    return bStandardColorInterp;
}

static signed char GTiffGetWebPLevel(CSLConstList papszOptions)
{
    int nWebPLevel = DEFAULT_WEBP_LEVEL;
    const char *pszValue = CSLFetchNameValue(papszOptions, "WEBP_LEVEL");
    if (pszValue != nullptr)
    {
        nWebPLevel = atoi(pszValue);
        if (!(nWebPLevel >= 1 && nWebPLevel <= 100))
        {
            CPLError(CE_Warning, CPLE_IllegalArg,
                     "WEBP_LEVEL=%s value not recognised, ignoring.", pszValue);
            nWebPLevel = DEFAULT_WEBP_LEVEL;
        }
    }
    return static_cast<signed char>(nWebPLevel);
}

static bool GTiffGetWebPLossless(CSLConstList papszOptions)
{
    return CPLFetchBool(papszOptions, "WEBP_LOSSLESS", false);
}

#if HAVE_JXL
static bool GTiffGetJXLLossless(CSLConstList papszOptions)
{
    return CPLTestBool(
        CSLFetchNameValueDef(papszOptions, "JXL_LOSSLESS", "TRUE"));
}

static uint32_t GTiffGetJXLEffort(CSLConstList papszOptions)
{
    return atoi(CSLFetchNameValueDef(papszOptions, "JXL_EFFORT", "5"));
}

static float GTiffGetJXLDistance(CSLConstList papszOptions)
{
    return static_cast<float>(
        CPLAtof(CSLFetchNameValueDef(papszOptions, "JXL_DISTANCE", "1.0")));
}

#endif

/************************************************************************/
/*                           WriteMetadata()                            */
/************************************************************************/

bool GTiffDataset::WriteMetadata(GDALDataset *poSrcDS, TIFF *l_hTIFF,
                                 bool bSrcIsGeoTIFF, GTiffProfile eProfile,
                                 const char *pszTIFFFilename,
                                 char **l_papszCreationOptions,
                                 bool bExcludeRPBandIMGFileWriting)

{
    /* -------------------------------------------------------------------- */
    /*      Convert all the remaining metadata into a simple XML            */
    /*      format.                                                         */
    /* -------------------------------------------------------------------- */
    CPLXMLNode *psRoot = nullptr;
    CPLXMLNode *psTail = nullptr;

    if (bSrcIsGeoTIFF)
    {
        GTiffDataset *poSrcDSGTiff = cpl::down_cast<GTiffDataset *>(poSrcDS);
        assert(poSrcDSGTiff);
        WriteMDMetadata(&poSrcDSGTiff->m_oGTiffMDMD, l_hTIFF, &psRoot, &psTail,
                        0, eProfile);
    }
    else
    {
        char **papszMD = poSrcDS->GetMetadata();

        if (CSLCount(papszMD) > 0)
        {
            GDALMultiDomainMetadata l_oMDMD;
            l_oMDMD.SetMetadata(papszMD);

            WriteMDMetadata(&l_oMDMD, l_hTIFF, &psRoot, &psTail, 0, eProfile);
        }
    }

    if (!bExcludeRPBandIMGFileWriting)
    {
        WriteRPC(poSrcDS, l_hTIFF, bSrcIsGeoTIFF, eProfile, pszTIFFFilename,
                 l_papszCreationOptions);

        /* --------------------------------------------------------------------
         */
        /*      Handle metadata data written to an IMD file. */
        /* --------------------------------------------------------------------
         */
        char **papszIMDMD = poSrcDS->GetMetadata(MD_DOMAIN_IMD);
        if (papszIMDMD != nullptr)
        {
            GDALWriteIMDFile(pszTIFFFilename, papszIMDMD);
        }
    }

    uint16_t nPhotometric = 0;
    if (!TIFFGetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, &(nPhotometric)))
        nPhotometric = PHOTOMETRIC_MINISBLACK;

    const bool bStandardColorInterp = GTIFFIsStandardColorInterpretation(
        GDALDataset::ToHandle(poSrcDS), nPhotometric, l_papszCreationOptions);

    /* -------------------------------------------------------------------- */
    /*      We also need to address band specific metadata, and special     */
    /*      "role" metadata.                                                */
    /* -------------------------------------------------------------------- */
    for (int nBand = 1; nBand <= poSrcDS->GetRasterCount(); ++nBand)
    {
        GDALRasterBand *poBand = poSrcDS->GetRasterBand(nBand);

        if (bSrcIsGeoTIFF)
        {
            GTiffRasterBand *poSrcBandGTiff =
                cpl::down_cast<GTiffRasterBand *>(poBand);
            assert(poSrcBandGTiff);
            WriteMDMetadata(&poSrcBandGTiff->m_oGTiffMDMD, l_hTIFF, &psRoot,
                            &psTail, nBand, eProfile);
        }
        else
        {
            char **papszMD = poBand->GetMetadata();

            if (CSLCount(papszMD) > 0)
            {
                GDALMultiDomainMetadata l_oMDMD;
                l_oMDMD.SetMetadata(papszMD);

                WriteMDMetadata(&l_oMDMD, l_hTIFF, &psRoot, &psTail, nBand,
                                eProfile);
            }
        }

        const double dfOffset = poBand->GetOffset();
        const double dfScale = poBand->GetScale();
        bool bGeoTIFFScaleOffsetInZ = false;
        double adfGeoTransform[6];
        // Check if we have already encoded scale/offset in the GeoTIFF tags
        if (poSrcDS->GetGeoTransform(adfGeoTransform) == CE_None &&
            adfGeoTransform[2] == 0.0 && adfGeoTransform[4] == 0.0 &&
            adfGeoTransform[5] < 0.0 && poSrcDS->GetSpatialRef() &&
            poSrcDS->GetSpatialRef()->IsVertical() &&
            poSrcDS->GetRasterCount() == 1)
        {
            bGeoTIFFScaleOffsetInZ = true;
        }

        if ((dfOffset != 0.0 || dfScale != 1.0) && !bGeoTIFFScaleOffsetInZ)
        {
            char szValue[128] = {};

            CPLsnprintf(szValue, sizeof(szValue), "%.18g", dfOffset);
            AppendMetadataItem(&psRoot, &psTail, "OFFSET", szValue, nBand,
                               "offset", "");
            CPLsnprintf(szValue, sizeof(szValue), "%.18g", dfScale);
            AppendMetadataItem(&psRoot, &psTail, "SCALE", szValue, nBand,
                               "scale", "");
        }

        const char *pszUnitType = poBand->GetUnitType();
        if (pszUnitType != nullptr && pszUnitType[0] != '\0')
        {
            bool bWriteUnit = true;
            auto poSRS = poSrcDS->GetSpatialRef();
            if (poSRS && poSRS->IsCompound())
            {
                const char *pszVertUnit = nullptr;
                poSRS->GetTargetLinearUnits("COMPD_CS|VERT_CS", &pszVertUnit);
                if (pszVertUnit && EQUAL(pszVertUnit, pszUnitType))
                {
                    bWriteUnit = false;
                }
            }
            if (bWriteUnit)
            {
                AppendMetadataItem(&psRoot, &psTail, "UNITTYPE", pszUnitType,
                                   nBand, "unittype", "");
            }
        }

        if (strlen(poBand->GetDescription()) > 0)
        {
            AppendMetadataItem(&psRoot, &psTail, "DESCRIPTION",
                               poBand->GetDescription(), nBand, "description",
                               "");
        }

        if (!bStandardColorInterp &&
            !(nBand <= 3 && EQUAL(CSLFetchNameValueDef(l_papszCreationOptions,
                                                       "PHOTOMETRIC", ""),
                                  "RGB")))
        {
            AppendMetadataItem(&psRoot, &psTail, "COLORINTERP",
                               GDALGetColorInterpretationName(
                                   poBand->GetColorInterpretation()),
                               nBand, "colorinterp", "");
        }
    }

    const char *pszTilingSchemeName =
        CSLFetchNameValue(l_papszCreationOptions, "@TILING_SCHEME_NAME");
    if (pszTilingSchemeName)
    {
        AppendMetadataItem(&psRoot, &psTail, "NAME", pszTilingSchemeName, 0,
                           nullptr, "TILING_SCHEME");

        const char *pszZoomLevel = CSLFetchNameValue(
            l_papszCreationOptions, "@TILING_SCHEME_ZOOM_LEVEL");
        if (pszZoomLevel)
        {
            AppendMetadataItem(&psRoot, &psTail, "ZOOM_LEVEL", pszZoomLevel, 0,
                               nullptr, "TILING_SCHEME");
        }

        const char *pszAlignedLevels = CSLFetchNameValue(
            l_papszCreationOptions, "@TILING_SCHEME_ALIGNED_LEVELS");
        if (pszAlignedLevels)
        {
            AppendMetadataItem(&psRoot, &psTail, "ALIGNED_LEVELS",
                               pszAlignedLevels, 0, nullptr, "TILING_SCHEME");
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Write information about some codecs.                            */
    /* -------------------------------------------------------------------- */
    if (CPLTestBool(
            CPLGetConfigOption("GTIFF_WRITE_IMAGE_STRUCTURE_METADATA", "YES")))
    {
        const char *pszCompress =
            CSLFetchNameValue(l_papszCreationOptions, "COMPRESS");
        if (pszCompress && EQUAL(pszCompress, "WEBP"))
        {
            if (GTiffGetWebPLossless(l_papszCreationOptions))
            {
                AppendMetadataItem(&psRoot, &psTail,
                                   "COMPRESSION_REVERSIBILITY", "LOSSLESS", 0,
                                   nullptr, "IMAGE_STRUCTURE");
            }
            else
            {
                AppendMetadataItem(
                    &psRoot, &psTail, "WEBP_LEVEL",
                    CPLSPrintf("%d", GTiffGetWebPLevel(l_papszCreationOptions)),
                    0, nullptr, "IMAGE_STRUCTURE");
            }
        }
#if HAVE_JXL
        else if (pszCompress && EQUAL(pszCompress, "JXL"))
        {
            if (GTiffGetJXLLossless(l_papszCreationOptions))
            {
                AppendMetadataItem(&psRoot, &psTail,
                                   "COMPRESSION_REVERSIBILITY", "LOSSLESS", 0,
                                   nullptr, "IMAGE_STRUCTURE");
            }
            else
            {
                AppendMetadataItem(
                    &psRoot, &psTail, "JXL_DISTANCE",
                    CPLSPrintf("%f",
                               GTiffGetJXLDistance(l_papszCreationOptions)),
                    0, nullptr, "IMAGE_STRUCTURE");
            }
            AppendMetadataItem(
                &psRoot, &psTail, "JXL_EFFORT",
                CPLSPrintf("%d", GTiffGetJXLEffort(l_papszCreationOptions)), 0,
                nullptr, "IMAGE_STRUCTURE");
        }
#endif
    }

    /* -------------------------------------------------------------------- */
    /*      Write out the generic XML metadata if there is any.             */
    /* -------------------------------------------------------------------- */
    if (psRoot != nullptr)
    {
        bool bRet = true;

        if (eProfile == GTiffProfile::GDALGEOTIFF)
        {
            char *pszXML_MD = CPLSerializeXMLTree(psRoot);
            TIFFSetField(l_hTIFF, TIFFTAG_GDAL_METADATA, pszXML_MD);
            CPLFree(pszXML_MD);
        }
        else
        {
            if (bSrcIsGeoTIFF)
                cpl::down_cast<GTiffDataset *>(poSrcDS)->PushMetadataToPam();
            else
                bRet = false;
        }

        CPLDestroyXMLNode(psRoot);

        return bRet;
    }

    // If we have no more metadata but it existed before,
    // remove the GDAL_METADATA tag.
    if (eProfile == GTiffProfile::GDALGEOTIFF)
    {
        char *pszText = nullptr;
        if (TIFFGetField(l_hTIFF, TIFFTAG_GDAL_METADATA, &pszText))
        {
            TIFFUnsetField(l_hTIFF, TIFFTAG_GDAL_METADATA);
        }
    }

    return true;
}

/************************************************************************/
/*                         PushMetadataToPam()                          */
/*                                                                      */
/*      When producing a strict profile TIFF or if our aggregate        */
/*      metadata is too big for a single tiff tag we may end up         */
/*      needing to write it via the PAM mechanisms.  This method        */
/*      copies all the appropriate metadata into the PAM level          */
/*      metadata object but with special care to avoid copying          */
/*      metadata handled in other ways in TIFF format.                  */
/************************************************************************/

void GTiffDataset::PushMetadataToPam()

{
    if (GetPamFlags() & GPF_DISABLED)
        return;

    const bool bStandardColorInterp = GTIFFIsStandardColorInterpretation(
        GDALDataset::ToHandle(this), m_nPhotometric, m_papszCreationOptions);

    for (int nBand = 0; nBand <= GetRasterCount(); ++nBand)
    {
        GDALMultiDomainMetadata *poSrcMDMD = nullptr;
        GTiffRasterBand *poBand = nullptr;

        if (nBand == 0)
        {
            poSrcMDMD = &(this->m_oGTiffMDMD);
        }
        else
        {
            poBand = cpl::down_cast<GTiffRasterBand *>(GetRasterBand(nBand));
            poSrcMDMD = &(poBand->m_oGTiffMDMD);
        }

        /* --------------------------------------------------------------------
         */
        /*      Loop over the available domains. */
        /* --------------------------------------------------------------------
         */
        char **papszDomainList = poSrcMDMD->GetDomainList();
        for (int iDomain = 0; papszDomainList && papszDomainList[iDomain];
             ++iDomain)
        {
            char **papszMD = poSrcMDMD->GetMetadata(papszDomainList[iDomain]);

            if (EQUAL(papszDomainList[iDomain], MD_DOMAIN_RPC) ||
                EQUAL(papszDomainList[iDomain], MD_DOMAIN_IMD) ||
                EQUAL(papszDomainList[iDomain], "_temporary_") ||
                EQUAL(papszDomainList[iDomain], "IMAGE_STRUCTURE") ||
                EQUAL(papszDomainList[iDomain], "COLOR_PROFILE"))
                continue;

            papszMD = CSLDuplicate(papszMD);

            for (int i = CSLCount(papszMD) - 1; i >= 0; --i)
            {
                if (STARTS_WITH_CI(papszMD[i], "TIFFTAG_") ||
                    EQUALN(papszMD[i], GDALMD_AREA_OR_POINT,
                           strlen(GDALMD_AREA_OR_POINT)))
                    papszMD = CSLRemoveStrings(papszMD, i, 1, nullptr);
            }

            if (nBand == 0)
                GDALPamDataset::SetMetadata(papszMD, papszDomainList[iDomain]);
            else
                poBand->GDALPamRasterBand::SetMetadata(
                    papszMD, papszDomainList[iDomain]);

            CSLDestroy(papszMD);
        }

        /* --------------------------------------------------------------------
         */
        /*      Handle some "special domain" stuff. */
        /* --------------------------------------------------------------------
         */
        if (poBand != nullptr)
        {
            poBand->GDALPamRasterBand::SetOffset(poBand->GetOffset());
            poBand->GDALPamRasterBand::SetScale(poBand->GetScale());
            poBand->GDALPamRasterBand::SetUnitType(poBand->GetUnitType());
            poBand->GDALPamRasterBand::SetDescription(poBand->GetDescription());
            if (!bStandardColorInterp)
            {
                poBand->GDALPamRasterBand::SetColorInterpretation(
                    poBand->GetColorInterpretation());
            }
        }
    }
    MarkPamDirty();
}

/************************************************************************/
/*                     GTiffDatasetWriteRPCTag()                        */
/*                                                                      */
/*      Format a TAG according to:                                      */
/*                                                                      */
/*      http://geotiff.maptools.org/rpc_prop.html                       */
/************************************************************************/

void GTiffDatasetWriteRPCTag(TIFF *hTIFF, char **papszRPCMD)

{
    GDALRPCInfoV2 sRPC;

    if (!GDALExtractRPCInfoV2(papszRPCMD, &sRPC))
        return;

    double adfRPCTag[92] = {};
    adfRPCTag[0] = sRPC.dfERR_BIAS;  // Error Bias
    adfRPCTag[1] = sRPC.dfERR_RAND;  // Error Random

    adfRPCTag[2] = sRPC.dfLINE_OFF;
    adfRPCTag[3] = sRPC.dfSAMP_OFF;
    adfRPCTag[4] = sRPC.dfLAT_OFF;
    adfRPCTag[5] = sRPC.dfLONG_OFF;
    adfRPCTag[6] = sRPC.dfHEIGHT_OFF;
    adfRPCTag[7] = sRPC.dfLINE_SCALE;
    adfRPCTag[8] = sRPC.dfSAMP_SCALE;
    adfRPCTag[9] = sRPC.dfLAT_SCALE;
    adfRPCTag[10] = sRPC.dfLONG_SCALE;
    adfRPCTag[11] = sRPC.dfHEIGHT_SCALE;

    memcpy(adfRPCTag + 12, sRPC.adfLINE_NUM_COEFF, sizeof(double) * 20);
    memcpy(adfRPCTag + 32, sRPC.adfLINE_DEN_COEFF, sizeof(double) * 20);
    memcpy(adfRPCTag + 52, sRPC.adfSAMP_NUM_COEFF, sizeof(double) * 20);
    memcpy(adfRPCTag + 72, sRPC.adfSAMP_DEN_COEFF, sizeof(double) * 20);

    TIFFSetField(hTIFF, TIFFTAG_RPCCOEFFICIENT, 92, adfRPCTag);
}

/************************************************************************/
/*                             ReadRPCTag()                             */
/*                                                                      */
/*      Format a TAG according to:                                      */
/*                                                                      */
/*      http://geotiff.maptools.org/rpc_prop.html                       */
/************************************************************************/

char **GTiffDatasetReadRPCTag(TIFF *hTIFF)

{
    double *padfRPCTag = nullptr;
    uint16_t nCount;

    if (!TIFFGetField(hTIFF, TIFFTAG_RPCCOEFFICIENT, &nCount, &padfRPCTag) ||
        nCount != 92)
        return nullptr;

    CPLStringList asMD;
    asMD.SetNameValue(RPC_ERR_BIAS, CPLOPrintf("%.15g", padfRPCTag[0]));
    asMD.SetNameValue(RPC_ERR_RAND, CPLOPrintf("%.15g", padfRPCTag[1]));
    asMD.SetNameValue(RPC_LINE_OFF, CPLOPrintf("%.15g", padfRPCTag[2]));
    asMD.SetNameValue(RPC_SAMP_OFF, CPLOPrintf("%.15g", padfRPCTag[3]));
    asMD.SetNameValue(RPC_LAT_OFF, CPLOPrintf("%.15g", padfRPCTag[4]));
    asMD.SetNameValue(RPC_LONG_OFF, CPLOPrintf("%.15g", padfRPCTag[5]));
    asMD.SetNameValue(RPC_HEIGHT_OFF, CPLOPrintf("%.15g", padfRPCTag[6]));
    asMD.SetNameValue(RPC_LINE_SCALE, CPLOPrintf("%.15g", padfRPCTag[7]));
    asMD.SetNameValue(RPC_SAMP_SCALE, CPLOPrintf("%.15g", padfRPCTag[8]));
    asMD.SetNameValue(RPC_LAT_SCALE, CPLOPrintf("%.15g", padfRPCTag[9]));
    asMD.SetNameValue(RPC_LONG_SCALE, CPLOPrintf("%.15g", padfRPCTag[10]));
    asMD.SetNameValue(RPC_HEIGHT_SCALE, CPLOPrintf("%.15g", padfRPCTag[11]));

    CPLString osField;
    CPLString osMultiField;

    for (int i = 0; i < 20; ++i)
    {
        osField.Printf("%.15g", padfRPCTag[12 + i]);
        if (i > 0)
            osMultiField += " ";
        else
            osMultiField = "";
        osMultiField += osField;
    }
    asMD.SetNameValue(RPC_LINE_NUM_COEFF, osMultiField);

    for (int i = 0; i < 20; ++i)
    {
        osField.Printf("%.15g", padfRPCTag[32 + i]);
        if (i > 0)
            osMultiField += " ";
        else
            osMultiField = "";
        osMultiField += osField;
    }
    asMD.SetNameValue(RPC_LINE_DEN_COEFF, osMultiField);

    for (int i = 0; i < 20; ++i)
    {
        osField.Printf("%.15g", padfRPCTag[52 + i]);
        if (i > 0)
            osMultiField += " ";
        else
            osMultiField = "";
        osMultiField += osField;
    }
    asMD.SetNameValue(RPC_SAMP_NUM_COEFF, osMultiField);

    for (int i = 0; i < 20; ++i)
    {
        osField.Printf("%.15g", padfRPCTag[72 + i]);
        if (i > 0)
            osMultiField += " ";
        else
            osMultiField = "";
        osMultiField += osField;
    }
    asMD.SetNameValue(RPC_SAMP_DEN_COEFF, osMultiField);

    return asMD.StealList();
}

/************************************************************************/
/*                  GTiffFormatGDALNoDataTagValue()                     */
/************************************************************************/

CPLString GTiffFormatGDALNoDataTagValue(double dfNoData)
{
    CPLString osVal;
    if (CPLIsNan(dfNoData))
        osVal = "nan";
    else
        osVal.Printf("%.18g", dfNoData);
    return osVal;
}

/************************************************************************/
/*                         WriteNoDataValue()                           */
/************************************************************************/

void GTiffDataset::WriteNoDataValue(TIFF *hTIFF, double dfNoData)

{
    CPLString osVal(GTiffFormatGDALNoDataTagValue(dfNoData));
    TIFFSetField(hTIFF, TIFFTAG_GDAL_NODATA, osVal.c_str());
}

void GTiffDataset::WriteNoDataValue(TIFF *hTIFF, int64_t nNoData)

{
    TIFFSetField(hTIFF, TIFFTAG_GDAL_NODATA,
                 CPLSPrintf(CPL_FRMT_GIB, static_cast<GIntBig>(nNoData)));
}

void GTiffDataset::WriteNoDataValue(TIFF *hTIFF, uint64_t nNoData)

{
    TIFFSetField(hTIFF, TIFFTAG_GDAL_NODATA,
                 CPLSPrintf(CPL_FRMT_GUIB, static_cast<GUIntBig>(nNoData)));
}

/************************************************************************/
/*                         UnsetNoDataValue()                           */
/************************************************************************/

void GTiffDataset::UnsetNoDataValue(TIFF *l_hTIFF)

{
    TIFFUnsetField(l_hTIFF, TIFFTAG_GDAL_NODATA);
}

/************************************************************************/
/*                            SetDirectory()                            */
/************************************************************************/

bool GTiffDataset::SetDirectory()

{
    Crystalize();

    if (TIFFCurrentDirOffset(m_hTIFF) == m_nDirOffset)
    {
        return true;
    }

    const int nSetDirResult = TIFFSetSubDirectory(m_hTIFF, m_nDirOffset);
    if (!nSetDirResult)
        return false;

    RestoreVolatileParameters(m_hTIFF);

    return true;
}

/************************************************************************/
/*                     GTiffSetDeflateSubCodec()                        */
/************************************************************************/

static void GTiffSetDeflateSubCodec(TIFF *hTIFF)
{
    (void)hTIFF;

#if defined(TIFFTAG_DEFLATE_SUBCODEC) && defined(LIBDEFLATE_SUPPORT)
    // Mostly for strict reproducibility purposes
    if (EQUAL(CPLGetConfigOption("GDAL_TIFF_DEFLATE_SUBCODEC", ""), "ZLIB"))
    {
        TIFFSetField(hTIFF, TIFFTAG_DEFLATE_SUBCODEC, DEFLATE_SUBCODEC_ZLIB);
    }
#endif
}

/************************************************************************/
/*                     RestoreVolatileParameters()                      */
/************************************************************************/

void GTiffDataset::RestoreVolatileParameters(TIFF *hTIFF)
{

    /* -------------------------------------------------------------------- */
    /*      YCbCr JPEG compressed images should be translated on the fly    */
    /*      to RGB by libtiff/libjpeg unless specifically requested         */
    /*      otherwise.                                                      */
    /* -------------------------------------------------------------------- */
    if (m_nCompression == COMPRESSION_JPEG &&
        m_nPhotometric == PHOTOMETRIC_YCBCR &&
        CPLTestBool(CPLGetConfigOption("CONVERT_YCBCR_TO_RGB", "YES")))
    {
        int nColorMode = JPEGCOLORMODE_RAW;  // Initialize to 0;

        TIFFGetField(hTIFF, TIFFTAG_JPEGCOLORMODE, &nColorMode);
        if (nColorMode != JPEGCOLORMODE_RGB)
        {
            TIFFSetField(hTIFF, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB);
        }
    }

    if (m_nCompression == COMPRESSION_ADOBE_DEFLATE ||
        m_nCompression == COMPRESSION_LERC)
    {
        GTiffSetDeflateSubCodec(hTIFF);
    }

    /* -------------------------------------------------------------------- */
    /*      Propagate any quality settings.                                 */
    /* -------------------------------------------------------------------- */
    if (eAccess == GA_Update)
    {
        // Now, reset zip and jpeg quality.
        if (m_nJpegQuality > 0 && m_nCompression == COMPRESSION_JPEG)
        {
#ifdef DEBUG_VERBOSE
            CPLDebug("GTiff", "Propagate JPEG_QUALITY(%d) in SetDirectory()",
                     m_nJpegQuality);
#endif
            TIFFSetField(hTIFF, TIFFTAG_JPEGQUALITY, m_nJpegQuality);
        }
        if (m_nJpegTablesMode >= 0 && m_nCompression == COMPRESSION_JPEG)
            TIFFSetField(hTIFF, TIFFTAG_JPEGTABLESMODE, m_nJpegTablesMode);
        if (m_nZLevel > 0 && (m_nCompression == COMPRESSION_ADOBE_DEFLATE ||
                              m_nCompression == COMPRESSION_LERC))
            TIFFSetField(hTIFF, TIFFTAG_ZIPQUALITY, m_nZLevel);
        if (m_nLZMAPreset > 0 && m_nCompression == COMPRESSION_LZMA)
            TIFFSetField(hTIFF, TIFFTAG_LZMAPRESET, m_nLZMAPreset);
        if (m_nZSTDLevel > 0 && (m_nCompression == COMPRESSION_ZSTD ||
                                 m_nCompression == COMPRESSION_LERC))
            TIFFSetField(hTIFF, TIFFTAG_ZSTD_LEVEL, m_nZSTDLevel);
        if (m_nCompression == COMPRESSION_LERC)
        {
            TIFFSetField(hTIFF, TIFFTAG_LERC_MAXZERROR, m_dfMaxZError);
        }
        if (m_nWebPLevel > 0 && m_nCompression == COMPRESSION_WEBP)
            TIFFSetField(hTIFF, TIFFTAG_WEBP_LEVEL, m_nWebPLevel);
        if (m_bWebPLossless && m_nCompression == COMPRESSION_WEBP)
            TIFFSetField(hTIFF, TIFFTAG_WEBP_LOSSLESS, 1);
#ifdef HAVE_JXL
        if (m_nCompression == COMPRESSION_JXL)
        {
            TIFFSetField(hTIFF, TIFFTAG_JXL_LOSSYNESS,
                         m_bJXLLossless ? JXL_LOSSLESS : JXL_LOSSY);
            TIFFSetField(hTIFF, TIFFTAG_JXL_EFFORT, m_nJXLEffort);
            TIFFSetField(hTIFF, TIFFTAG_JXL_DISTANCE, m_fJXLDistance);
        }
#endif
    }
}

/************************************************************************/
/*                              Identify()                              */
/************************************************************************/

int GTiffDataset::Identify(GDALOpenInfo *poOpenInfo)

{
    const char *pszFilename = poOpenInfo->pszFilename;
    if (STARTS_WITH_CI(pszFilename, "GTIFF_RAW:"))
    {
        pszFilename += strlen("GTIFF_RAW:");
        GDALOpenInfo oOpenInfo(pszFilename, poOpenInfo->eAccess);
        return Identify(&oOpenInfo);
    }

    /* -------------------------------------------------------------------- */
    /*      We have a special hook for handling opening a specific          */
    /*      directory of a TIFF file.                                       */
    /* -------------------------------------------------------------------- */
    if (STARTS_WITH_CI(pszFilename, "GTIFF_DIR:"))
        return TRUE;

    /* -------------------------------------------------------------------- */
    /*      First we check to see if the file has the expected header       */
    /*      bytes.                                                          */
    /* -------------------------------------------------------------------- */
    if (poOpenInfo->fpL == nullptr || poOpenInfo->nHeaderBytes < 2)
        return FALSE;

    if ((poOpenInfo->pabyHeader[0] != 'I' ||
         poOpenInfo->pabyHeader[1] != 'I') &&
        (poOpenInfo->pabyHeader[0] != 'M' || poOpenInfo->pabyHeader[1] != 'M'))
        return FALSE;

    if ((poOpenInfo->pabyHeader[2] != 0x2A || poOpenInfo->pabyHeader[3] != 0) &&
        (poOpenInfo->pabyHeader[3] != 0x2A || poOpenInfo->pabyHeader[2] != 0) &&
        (poOpenInfo->pabyHeader[2] != 0x2B || poOpenInfo->pabyHeader[3] != 0) &&
        (poOpenInfo->pabyHeader[3] != 0x2B || poOpenInfo->pabyHeader[2] != 0))
        return FALSE;

    return TRUE;
}

/************************************************************************/
/*                          GTIFFExtendMemoryFile()                     */
/************************************************************************/

static bool GTIFFExtendMemoryFile(const CPLString &osTmpFilename,
                                  VSILFILE *fpTemp, VSILFILE *fpL,
                                  int nNewLength, GByte *&pabyBuffer,
                                  vsi_l_offset &nDataLength)
{
    if (nNewLength <= static_cast<int>(nDataLength))
        return true;
    if (VSIFSeekL(fpTemp, nNewLength - 1, SEEK_SET) != 0)
        return false;
    char ch = 0;
    if (VSIFWriteL(&ch, 1, 1, fpTemp) != 1)
        return false;
    const int nOldDataLength = static_cast<int>(nDataLength);
    pabyBuffer = static_cast<GByte *>(
        VSIGetMemFileBuffer(osTmpFilename, &nDataLength, FALSE));
    const int nToRead = nNewLength - nOldDataLength;
    const int nRead = static_cast<int>(
        VSIFReadL(pabyBuffer + nOldDataLength, 1, nToRead, fpL));
    if (nRead != nToRead)
    {
        CPLError(CE_Failure, CPLE_FileIO,
                 "Needed to read %d bytes. Only %d got", nToRead, nRead);
        return false;
    }
    return true;
}

/************************************************************************/
/*                         GTIFFMakeBufferedStream()                    */
/************************************************************************/

static bool GTIFFMakeBufferedStream(GDALOpenInfo *poOpenInfo)
{
    CPLString osTmpFilename;
    static int nCounter = 0;
    osTmpFilename.Printf("/vsimem/stream_%d.tif", ++nCounter);
    VSILFILE *fpTemp = VSIFOpenL(osTmpFilename, "wb+");
    if (fpTemp == nullptr)
        return false;
    // The seek is needed for /vsistdin/ that has some rewind capabilities.
    if (VSIFSeekL(poOpenInfo->fpL, poOpenInfo->nHeaderBytes, SEEK_SET) != 0)
    {
        CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
        return false;
    }
    CPLAssert(static_cast<int>(VSIFTellL(poOpenInfo->fpL)) ==
              poOpenInfo->nHeaderBytes);
    if (VSIFWriteL(poOpenInfo->pabyHeader, poOpenInfo->nHeaderBytes, 1,
                   fpTemp) != 1)
    {
        CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
        return false;
    }
    vsi_l_offset nDataLength = 0;
    GByte *pabyBuffer = static_cast<GByte *>(
        VSIGetMemFileBuffer(osTmpFilename, &nDataLength, FALSE));
    const bool bLittleEndian = (pabyBuffer[0] == 'I');
#if CPL_IS_LSB
    const bool bSwap = !bLittleEndian;
#else
    const bool bSwap = bLittleEndian;
#endif
    const bool bBigTIFF = pabyBuffer[2] == 43 || pabyBuffer[3] == 43;
    vsi_l_offset nMaxOffset = 0;
    if (bBigTIFF)
    {
        GUInt64 nTmp = 0;
        memcpy(&nTmp, pabyBuffer + 8, 8);
        if (bSwap)
            CPL_SWAP64PTR(&nTmp);
        if (nTmp != 16)
        {
            CPLError(CE_Failure, CPLE_NotSupported,
                     "IFD start should be at offset 16 for a streamed BigTIFF");
            CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
            VSIUnlink(osTmpFilename);
            return false;
        }
        memcpy(&nTmp, pabyBuffer + 16, 8);
        if (bSwap)
            CPL_SWAP64PTR(&nTmp);
        if (nTmp > 1024)
        {
            CPLError(CE_Failure, CPLE_NotSupported,
                     "Too many tags : " CPL_FRMT_GIB, nTmp);
            CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
            VSIUnlink(osTmpFilename);
            return false;
        }
        const int nTags = static_cast<int>(nTmp);
        const int nSpaceForTags = nTags * 20;
        if (!GTIFFExtendMemoryFile(osTmpFilename, fpTemp, poOpenInfo->fpL,
                                   24 + nSpaceForTags, pabyBuffer, nDataLength))
        {
            CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
            VSIUnlink(osTmpFilename);
            return false;
        }
        nMaxOffset = 24 + nSpaceForTags + 8;
        for (int i = 0; i < nTags; ++i)
        {
            GUInt16 nTmp16 = 0;
            memcpy(&nTmp16, pabyBuffer + 24 + i * 20, 2);
            if (bSwap)
                CPL_SWAP16PTR(&nTmp16);
            const int nTag = nTmp16;
            memcpy(&nTmp16, pabyBuffer + 24 + i * 20 + 2, 2);
            if (bSwap)
                CPL_SWAP16PTR(&nTmp16);
            const int nDataType = nTmp16;
            memcpy(&nTmp, pabyBuffer + 24 + i * 20 + 4, 8);
            if (bSwap)
                CPL_SWAP64PTR(&nTmp);
            if (nTmp >= 16 * 1024 * 1024)
            {
                CPLError(CE_Failure, CPLE_NotSupported,
                         "Too many elements for tag %d : " CPL_FRMT_GUIB, nTag,
                         nTmp);
                CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
                VSIUnlink(osTmpFilename);
                return false;
            }
            const GUInt32 nCount = static_cast<GUInt32>(nTmp);
            const GUInt32 nTagSize =
                TIFFDataWidth(static_cast<TIFFDataType>(nDataType)) * nCount;
            if (nTagSize > 8)
            {
                memcpy(&nTmp, pabyBuffer + 24 + i * 20 + 12, 8);
                if (bSwap)
                    CPL_SWAP64PTR(&nTmp);
                if (nTmp > GUINT64_MAX - nTagSize)
                {
                    CPLError(CE_Failure, CPLE_NotSupported,
                             "Overflow with tag %d", nTag);
                    CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
                    VSIUnlink(osTmpFilename);
                    return false;
                }
                if (static_cast<vsi_l_offset>(nTmp + nTagSize) > nMaxOffset)
                    nMaxOffset = nTmp + nTagSize;
            }
        }
    }
    else
    {
        GUInt32 nTmp = 0;
        memcpy(&nTmp, pabyBuffer + 4, 4);
        if (bSwap)
            CPL_SWAP32PTR(&nTmp);
        if (nTmp != 8)
        {
            CPLError(CE_Failure, CPLE_NotSupported,
                     "IFD start should be at offset 8 for a streamed TIFF");
            CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
            VSIUnlink(osTmpFilename);
            return false;
        }
        GUInt16 nTmp16 = 0;
        memcpy(&nTmp16, pabyBuffer + 8, 2);
        if (bSwap)
            CPL_SWAP16PTR(&nTmp16);
        if (nTmp16 > 1024)
        {
            CPLError(CE_Failure, CPLE_NotSupported, "Too many tags : %d",
                     nTmp16);
            CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
            VSIUnlink(osTmpFilename);
            return false;
        }
        const int nTags = nTmp16;
        const int nSpaceForTags = nTags * 12;
        if (!GTIFFExtendMemoryFile(osTmpFilename, fpTemp, poOpenInfo->fpL,
                                   10 + nSpaceForTags, pabyBuffer, nDataLength))
        {
            CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
            VSIUnlink(osTmpFilename);
            return false;
        }
        nMaxOffset = 10 + nSpaceForTags + 4;
        for (int i = 0; i < nTags; ++i)
        {
            memcpy(&nTmp16, pabyBuffer + 10 + i * 12, 2);
            if (bSwap)
                CPL_SWAP16PTR(&nTmp16);
            const int nTag = nTmp16;
            memcpy(&nTmp16, pabyBuffer + 10 + i * 12 + 2, 2);
            if (bSwap)
                CPL_SWAP16PTR(&nTmp16);
            const int nDataType = nTmp16;
            memcpy(&nTmp, pabyBuffer + 10 + i * 12 + 4, 4);
            if (bSwap)
                CPL_SWAP32PTR(&nTmp);
            if (nTmp >= 16 * 1024 * 1024)
            {
                CPLError(CE_Failure, CPLE_NotSupported,
                         "Too many elements for tag %d : %u", nTag, nTmp);
                CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
                VSIUnlink(osTmpFilename);
                return false;
            }
            const GUInt32 nCount = nTmp;
            const GUInt32 nTagSize =
                TIFFDataWidth(static_cast<TIFFDataType>(nDataType)) * nCount;
            if (nTagSize > 4)
            {
                memcpy(&nTmp, pabyBuffer + 10 + i * 12 + 8, 4);
                if (bSwap)
                    CPL_SWAP32PTR(&nTmp);
                if (nTmp > static_cast<GUInt32>(UINT_MAX - nTagSize))
                {
                    CPLError(CE_Failure, CPLE_NotSupported,
                             "Overflow with tag %d", nTag);
                    CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
                    VSIUnlink(osTmpFilename);
                    return false;
                }
                if (nTmp + nTagSize > nMaxOffset)
                    nMaxOffset = nTmp + nTagSize;
            }
        }
    }
    if (nMaxOffset > 10 * 1024 * 1024)
    {
        CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
        VSIUnlink(osTmpFilename);
        return false;
    }
    if (!GTIFFExtendMemoryFile(osTmpFilename, fpTemp, poOpenInfo->fpL,
                               static_cast<int>(nMaxOffset), pabyBuffer,
                               nDataLength))
    {
        CPL_IGNORE_RET_VAL(VSIFCloseL(fpTemp));
        VSIUnlink(osTmpFilename);
        return false;
    }
    CPLAssert(nDataLength == VSIFTellL(poOpenInfo->fpL));
    poOpenInfo->fpL =
        reinterpret_cast<VSILFILE *>(VSICreateBufferedReaderHandle(
            reinterpret_cast<VSIVirtualHandle *>(poOpenInfo->fpL), pabyBuffer,
            static_cast<vsi_l_offset>(INT_MAX) << 32));
    if (VSIFCloseL(fpTemp) != 0)
        return false;
    VSIUnlink(osTmpFilename);

    return true;
}

/************************************************************************/
/*                       AssociateExternalMask()                        */
/************************************************************************/

// Used by GTIFFBuildOverviewsEx() for the COG driver
bool GTiffDataset::AssociateExternalMask()
{
    if (m_poMaskExtOvrDS->GetRasterBand(1)->GetOverviewCount() !=
        GetRasterBand(1)->GetOverviewCount())
        return false;
    if (m_papoOverviewDS == nullptr)
        return false;
    if (m_poMaskDS)
        return false;
    if (m_poMaskExtOvrDS->GetRasterXSize() != nRasterXSize ||
        m_poMaskExtOvrDS->GetRasterYSize() != nRasterYSize)
        return false;
    m_poExternalMaskDS = m_poMaskExtOvrDS.get();
    for (int i = 0; i < m_nOverviewCount; i++)
    {
        if (m_papoOverviewDS[i]->m_poMaskDS)
            return false;
        m_papoOverviewDS[i]->m_poExternalMaskDS =
            m_poMaskExtOvrDS->GetRasterBand(1)->GetOverview(i)->GetDataset();
        if (!m_papoOverviewDS[i]->m_poExternalMaskDS)
            return false;
        auto poOvrBand = m_papoOverviewDS[i]->GetRasterBand(1);
        if (m_papoOverviewDS[i]->m_poExternalMaskDS->GetRasterXSize() !=
                poOvrBand->GetXSize() ||
            m_papoOverviewDS[i]->m_poExternalMaskDS->GetRasterYSize() !=
                poOvrBand->GetYSize())
            return false;
    }
    return true;
}

/************************************************************************/
/*                                Open()                                */
/************************************************************************/

GDALDataset *GTiffDataset::Open(GDALOpenInfo *poOpenInfo)

{
    const char *pszFilename = poOpenInfo->pszFilename;

    /* -------------------------------------------------------------------- */
    /*      Check if it looks like a TIFF file.                             */
    /* -------------------------------------------------------------------- */
    if (!Identify(poOpenInfo))
        return nullptr;

    bool bAllowRGBAInterface = true;
    if (STARTS_WITH_CI(pszFilename, "GTIFF_RAW:"))
    {
        bAllowRGBAInterface = false;
        pszFilename += strlen("GTIFF_RAW:");
    }

    /* -------------------------------------------------------------------- */
    /*      We have a special hook for handling opening a specific          */
    /*      directory of a TIFF file.                                       */
    /* -------------------------------------------------------------------- */
    if (STARTS_WITH_CI(pszFilename, "GTIFF_DIR:"))
        return OpenDir(poOpenInfo);

    if (!GTiffOneTimeInit())
        return nullptr;

    /* -------------------------------------------------------------------- */
    /*      Try opening the dataset.                                        */
    /* -------------------------------------------------------------------- */
    bool bStreaming = false;
    const char *pszReadStreaming =
        CPLGetConfigOption("TIFF_READ_STREAMING", nullptr);
    if (poOpenInfo->fpL == nullptr)
    {
        poOpenInfo->fpL = VSIFOpenL(
            pszFilename, poOpenInfo->eAccess == GA_ReadOnly ? "rb" : "r+b");
        if (poOpenInfo->fpL == nullptr)
            return nullptr;
    }
    else if (!(pszReadStreaming && !CPLTestBool(pszReadStreaming)) &&
             poOpenInfo->nHeaderBytes >= 24 &&
             // A pipe has no seeking capability, so its position is 0 despite
             // having read bytes.
             (static_cast<int>(VSIFTellL(poOpenInfo->fpL)) ==
                  poOpenInfo->nHeaderBytes ||
              strcmp(pszFilename, "/vsistdin/") == 0 ||
              // STARTS_WITH(pszFilename, "/vsicurl_streaming/") ||
              (pszReadStreaming && CPLTestBool(pszReadStreaming))))
    {
        bStreaming = true;
        if (!GTIFFMakeBufferedStream(poOpenInfo))
            return nullptr;
    }

    // Store errors/warnings and emit them later.
    std::vector<CPLErrorHandlerAccumulatorStruct> aoErrors;
    CPLInstallErrorHandlerAccumulator(aoErrors);
    CPLSetCurrentErrorHandlerCatchDebug(FALSE);
    const bool bDeferStrileLoading = CPLTestBool(
        CPLGetConfigOption("GTIFF_USE_DEFER_STRILE_LOADING", "YES"));
    TIFF *l_hTIFF = VSI_TIFFOpen(
        pszFilename,
        poOpenInfo->eAccess == GA_ReadOnly
            ? ((bStreaming || !bDeferStrileLoading) ? "rC" : "rDOC")
            : (!bDeferStrileLoading ? "r+C" : "r+DC"),
        poOpenInfo->fpL);
    CPLUninstallErrorHandlerAccumulator();

    // Now emit errors and change their criticality if needed
    // We only emit failures if we didn't manage to open the file.
    // Otherwise it makes Python bindings unhappy (#5616).
    for (size_t iError = 0; iError < aoErrors.size(); ++iError)
    {
        ReportError(pszFilename,
                    (l_hTIFF == nullptr && aoErrors[iError].type == CE_Failure)
                        ? CE_Failure
                        : CE_Warning,
                    aoErrors[iError].no, "%s", aoErrors[iError].msg.c_str());
    }
    aoErrors.resize(0);

    if (l_hTIFF == nullptr)
        return nullptr;

    uint32_t nXSize = 0;
    TIFFGetField(l_hTIFF, TIFFTAG_IMAGEWIDTH, &nXSize);
    uint32_t nYSize = 0;
    TIFFGetField(l_hTIFF, TIFFTAG_IMAGELENGTH, &nYSize);

    if (nXSize > INT_MAX || nYSize > INT_MAX)
    {
        // GDAL only supports signed 32bit dimensions.
        ReportError(pszFilename, CE_Failure, CPLE_NotSupported,
                    "Too large image size: %u x %u", nXSize, nYSize);
        XTIFFClose(l_hTIFF);
        return nullptr;
    }

    uint16_t l_nCompression = 0;
    if (!TIFFGetField(l_hTIFF, TIFFTAG_COMPRESSION, &(l_nCompression)))
        l_nCompression = COMPRESSION_NONE;

    /* -------------------------------------------------------------------- */
    /*      Create a corresponding GDALDataset.                             */
    /* -------------------------------------------------------------------- */
    GTiffDataset *poDS = new GTiffDataset();
    poDS->SetDescription(pszFilename);
    poDS->m_pszFilename = CPLStrdup(pszFilename);
    poDS->m_fpL = poOpenInfo->fpL;
    poOpenInfo->fpL = nullptr;
    poDS->m_bStreamingIn = bStreaming;
    poDS->m_nCompression = l_nCompression;

    // Check structural metadata (for COG)
    const int nOffsetOfStructuralMetadata =
        poOpenInfo->nHeaderBytes && ((poOpenInfo->pabyHeader[2] == 0x2B ||
                                      poOpenInfo->pabyHeader[3] == 0x2B))
            ? 16
            : 8;
    if (poOpenInfo->nHeaderBytes >
            nOffsetOfStructuralMetadata +
                static_cast<int>(strlen("GDAL_STRUCTURAL_METADATA_SIZE=")) &&
        memcmp(poOpenInfo->pabyHeader + nOffsetOfStructuralMetadata,
               "GDAL_STRUCTURAL_METADATA_SIZE=",
               strlen("GDAL_STRUCTURAL_METADATA_SIZE=")) == 0)
    {
        const char *pszStructuralMD = reinterpret_cast<const char *>(
            poOpenInfo->pabyHeader + nOffsetOfStructuralMetadata);
        poDS->m_bLayoutIFDSBeforeData =
            strstr(pszStructuralMD, "LAYOUT=IFDS_BEFORE_DATA") != nullptr;
        poDS->m_bBlockOrderRowMajor =
            strstr(pszStructuralMD, "BLOCK_ORDER=ROW_MAJOR") != nullptr;
        poDS->m_bLeaderSizeAsUInt4 =
            strstr(pszStructuralMD, "BLOCK_LEADER=SIZE_AS_UINT4") != nullptr;
        poDS->m_bTrailerRepeatedLast4BytesRepeated =
            strstr(pszStructuralMD, "BLOCK_TRAILER=LAST_4_BYTES_REPEATED") !=
            nullptr;
        poDS->m_bMaskInterleavedWithImagery =
            strstr(pszStructuralMD, "MASK_INTERLEAVED_WITH_IMAGERY=YES") !=
            nullptr;
        poDS->m_bKnownIncompatibleEdition =
            strstr(pszStructuralMD, "KNOWN_INCOMPATIBLE_EDITION=YES") !=
            nullptr;
        if (poDS->m_bKnownIncompatibleEdition)
        {
            poDS->ReportError(
                CE_Warning, CPLE_AppDefined,
                "This file used to have optimizations in its layout, "
                "but those have been, at least partly, invalidated by "
                "later changes");
        }
        else if (poDS->m_bLayoutIFDSBeforeData && poDS->m_bBlockOrderRowMajor &&
                 poDS->m_bLeaderSizeAsUInt4 &&
                 poDS->m_bTrailerRepeatedLast4BytesRepeated)
        {
            poDS->m_oGTiffMDMD.SetMetadataItem("LAYOUT", "COG",
                                               "IMAGE_STRUCTURE");
        }
    }

    // In the case of GDAL_DISABLE_READDIR_ON_OPEN = NO / EMPTY_DIR
    if (poOpenInfo->AreSiblingFilesLoaded() &&
        CSLCount(poOpenInfo->GetSiblingFiles()) <= 1)
    {
        poDS->oOvManager.TransferSiblingFiles(
            CSLDuplicate(poOpenInfo->GetSiblingFiles()));
        poDS->m_bHasGotSiblingFiles = true;
    }

    if (poDS->OpenOffset(l_hTIFF, TIFFCurrentDirOffset(l_hTIFF),
                         poOpenInfo->eAccess, bAllowRGBAInterface,
                         true) != CE_None)
    {
        delete poDS;
        return nullptr;
    }

    // Do we want blocks that are set to zero and that haven't yet being
    // allocated as tile/strip to remain implicit?
    if (CPLFetchBool(poOpenInfo->papszOpenOptions, "SPARSE_OK", false))
        poDS->m_bWriteEmptyTiles = false;

    poDS->InitCreationOrOpenOptions(poOpenInfo->eAccess == GA_Update,
                                    poOpenInfo->papszOpenOptions);

    poDS->m_bLoadPam = true;
    poDS->m_bColorProfileMetadataChanged = false;
    poDS->m_bMetadataChanged = false;
    poDS->m_bGeoTIFFInfoChanged = false;
    poDS->m_bNoDataChanged = false;
    poDS->m_bForceUnsetGTOrGCPs = false;
    poDS->m_bForceUnsetProjection = false;

    // Used by GTIFFBuildOverviewsEx() for the COG driver
    const char *pszMaskOverviewDS = CSLFetchNameValue(
        poOpenInfo->papszOpenOptions, "MASK_OVERVIEW_DATASET");
    if (pszMaskOverviewDS)
    {
        poDS->m_poMaskExtOvrDS.reset(GDALDataset::Open(
            pszMaskOverviewDS, GDAL_OF_RASTER | GDAL_OF_INTERNAL));
        if (!poDS->m_poMaskExtOvrDS || !poDS->AssociateExternalMask())
        {
            CPLDebug("GTiff",
                     "Association with external mask overview file failed");
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Initialize info for external overviews.                         */
    /* -------------------------------------------------------------------- */
    poDS->oOvManager.Initialize(poDS, pszFilename);
    if (poOpenInfo->AreSiblingFilesLoaded())
        poDS->oOvManager.TransferSiblingFiles(poOpenInfo->StealSiblingFiles());

    // For backward compatibility, in case GTIFF_POINT_GEO_IGNORE is defined
    // load georeferencing right now so as to not require it to be defined
    // at the GetGeoTransform() time.
    if (CPLGetConfigOption("GTIFF_POINT_GEO_IGNORE", nullptr) != nullptr)
    {
        poDS->LoadGeoreferencingAndPamIfNeeded();
    }

    return poDS;
}

/************************************************************************/
/*                      GTiffDatasetSetAreaOrPointMD()                  */
/************************************************************************/

static void GTiffDatasetSetAreaOrPointMD(GTIF *hGTIF,
                                         GDALMultiDomainMetadata &m_oGTiffMDMD)
{
    // Is this a pixel-is-point dataset?
    unsigned short nRasterType = 0;

    if (GDALGTIFKeyGetSHORT(hGTIF, GTRasterTypeGeoKey, &nRasterType, 0, 1) == 1)
    {
        if (nRasterType == static_cast<short>(RasterPixelIsPoint))
            m_oGTiffMDMD.SetMetadataItem(GDALMD_AREA_OR_POINT,
                                         GDALMD_AOP_POINT);
        else
            m_oGTiffMDMD.SetMetadataItem(GDALMD_AREA_OR_POINT, GDALMD_AOP_AREA);
    }
}

/************************************************************************/
/*                         LoadMDAreaOrPoint()                          */
/************************************************************************/

// This is a light version of LookForProjection(), which saves the
// potential costly cost of GTIFGetOGISDefn(), since we just need to
// access to a raw GeoTIFF key, and not build the full projection object.

void GTiffDataset::LoadMDAreaOrPoint()
{
    if (m_bLookedForProjection || m_bLookedForMDAreaOrPoint ||
        m_oGTiffMDMD.GetMetadataItem(GDALMD_AREA_OR_POINT) != nullptr)
        return;

    m_bLookedForMDAreaOrPoint = true;

    GTIF *hGTIF = GTiffDatasetGTIFNew(m_hTIFF);

    if (!hGTIF)
    {
        ReportError(CE_Warning, CPLE_AppDefined,
                    "GeoTIFF tags apparently corrupt, they are being ignored.");
    }
    else
    {
        GTiffDatasetSetAreaOrPointMD(hGTIF, m_oGTiffMDMD);

        GTIFFree(hGTIF);
    }
}

/************************************************************************/
/*                         LookForProjection()                          */
/************************************************************************/

void GTiffDataset::LookForProjection()

{
    if (m_bLookedForProjection)
        return;

    m_bLookedForProjection = true;

    IdentifyAuthorizedGeoreferencingSources();
    if (m_nINTERNALGeorefSrcIndex < 0)
        return;

    /* -------------------------------------------------------------------- */
    /*      Capture the GeoTIFF projection, if available.                   */
    /* -------------------------------------------------------------------- */
    m_oSRS.Clear();

    GTIF *hGTIF = GTiffDatasetGTIFNew(m_hTIFF);

    if (!hGTIF)
    {
        ReportError(CE_Warning, CPLE_AppDefined,
                    "GeoTIFF tags apparently corrupt, they are being ignored.");
    }
    else
    {
        GTIFDefn *psGTIFDefn = GTIFAllocDefn();

        bool bHasErrorBefore = CPLGetLastErrorType() != 0;
        // Collect (PROJ) error messages and remit them later as warnings
        std::vector<CPLErrorHandlerAccumulatorStruct> aoErrors;
        CPLInstallErrorHandlerAccumulator(aoErrors);
        const int ret = GTIFGetDefn(hGTIF, psGTIFDefn);
        CPLUninstallErrorHandlerAccumulator();

        bool bWarnAboutEllipsoid = true;

        if (ret)
        {
            CPLInstallErrorHandlerAccumulator(aoErrors);

            if (psGTIFDefn->Ellipsoid == 4326 &&
                psGTIFDefn->SemiMajor == 6378137 &&
                psGTIFDefn->SemiMinor == 6356752.314245)
            {
                // Buggy Sentinel1 geotiff files use a wrong 4326 code for the
                // ellipsoid instead of 7030.
                psGTIFDefn->Ellipsoid = 7030;
                bWarnAboutEllipsoid = false;
            }

            OGRSpatialReferenceH hSRS = GTIFGetOGISDefnAsOSR(hGTIF, psGTIFDefn);
            CPLUninstallErrorHandlerAccumulator();

            if (hSRS)
            {
                m_oSRS = *(OGRSpatialReference::FromHandle(hSRS));
                OSRDestroySpatialReference(hSRS);
            }
        }

        std::set<std::string> oSetErrorMsg;
        for (const auto &oError : aoErrors)
        {
            if (!bWarnAboutEllipsoid &&
                oError.msg.find("ellipsoid not found") != std::string::npos)
            {
                continue;
            }

            // Some error messages might be duplicated in GTIFGetDefn()
            // and GTIFGetOGISDefnAsOSR(). Emit them just once.
            if (oSetErrorMsg.find(oError.msg) == oSetErrorMsg.end())
            {
                oSetErrorMsg.insert(oError.msg);
                CPLError(oError.type == CE_Failure ? CE_Warning : oError.type,
                         oError.no, "%s", oError.msg.c_str());
            }
        }

        if (!bHasErrorBefore && oSetErrorMsg.empty())
        {
            CPLErrorReset();
        }

        if (m_oSRS.IsCompound())
        {
            const char *pszVertUnit = nullptr;
            m_oSRS.GetTargetLinearUnits("COMPD_CS|VERT_CS", &pszVertUnit);
            if (pszVertUnit && !EQUAL(pszVertUnit, "unknown"))
            {
                CPLFree(m_pszVertUnit);
                m_pszVertUnit = CPLStrdup(pszVertUnit);
            }

            int versions[3];
            GTIFDirectoryInfo(hGTIF, versions, nullptr);

            // If GeoTIFF 1.0, strip vertical by default
            const char *pszDefaultReportCompdCS =
                (versions[0] == 1 && versions[1] == 1 && versions[2] == 0)
                    ? "NO"
                    : "YES";

            // Should we simplify away vertical CS stuff?
            if (!CPLTestBool(CPLGetConfigOption("GTIFF_REPORT_COMPD_CS",
                                                pszDefaultReportCompdCS)))
            {
                CPLDebug("GTiff", "Got COMPD_CS, but stripping it.");

                m_oSRS.StripVertical();
            }
        }

        GTIFFreeDefn(psGTIFDefn);

        GTiffDatasetSetAreaOrPointMD(hGTIF, m_oGTiffMDMD);

        GTIFFree(hGTIF);
    }
}

/************************************************************************/
/*                            ApplyPamInfo()                            */
/*                                                                      */
/*      PAM Information, if available, overrides the GeoTIFF            */
/*      geotransform and projection definition.  Check for them         */
/*      now.                                                            */
/************************************************************************/

void GTiffDataset::ApplyPamInfo()

{
    bool bGotGTFromPAM = false;

    if (m_nPAMGeorefSrcIndex >= 0 &&
        ((m_bGeoTransformValid &&
          m_nPAMGeorefSrcIndex < m_nGeoTransformGeorefSrcIndex) ||
         m_nGeoTransformGeorefSrcIndex < 0 || !m_bGeoTransformValid))
    {
        double adfPamGeoTransform[6] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
        if (GDALPamDataset::GetGeoTransform(adfPamGeoTransform) == CE_None)
        {
            if (m_nGeoTransformGeorefSrcIndex == m_nWORLDFILEGeorefSrcIndex)
            {
                CPLFree(m_pszGeorefFilename);
                m_pszGeorefFilename = nullptr;
            }
            memcpy(m_adfGeoTransform, adfPamGeoTransform, sizeof(double) * 6);
            m_bGeoTransformValid = true;
            bGotGTFromPAM = true;
        }
    }

    if (m_nPAMGeorefSrcIndex >= 0)
    {
        if ((m_nTABFILEGeorefSrcIndex < 0 ||
             m_nPAMGeorefSrcIndex < m_nTABFILEGeorefSrcIndex) &&
            (m_nINTERNALGeorefSrcIndex < 0 ||
             m_nPAMGeorefSrcIndex < m_nINTERNALGeorefSrcIndex))
        {
            const auto *poPamSRS = GDALPamDataset::GetSpatialRef();
            if (poPamSRS)
            {
                m_oSRS = *poPamSRS;
                m_bLookedForProjection = true;
                // m_nProjectionGeorefSrcIndex = m_nPAMGeorefSrcIndex;
            }
        }
        else
        {
            if (m_nINTERNALGeorefSrcIndex >= 0)
                LookForProjection();
            if (m_oSRS.IsEmpty())
            {
                const auto *poPamSRS = GDALPamDataset::GetSpatialRef();
                if (poPamSRS)
                {
                    m_oSRS = *poPamSRS;
                    m_bLookedForProjection = true;
                    // m_nProjectionGeorefSrcIndex = m_nPAMGeorefSrcIndex;
                }
            }
        }
    }

    int nPamGCPCount;
    if (m_nPAMGeorefSrcIndex >= 0 &&
        (nPamGCPCount = GDALPamDataset::GetGCPCount()) > 0 &&
        ((m_nGCPCount > 0 &&
          m_nPAMGeorefSrcIndex < m_nGeoTransformGeorefSrcIndex) ||
         m_nGeoTransformGeorefSrcIndex < 0 || m_nGCPCount == 0))
    {
        if (m_nGCPCount > 0)
        {
            GDALDeinitGCPs(m_nGCPCount, m_pasGCPList);
            CPLFree(m_pasGCPList);
            m_pasGCPList = nullptr;
        }

        m_nGCPCount = nPamGCPCount;
        m_pasGCPList =
            GDALDuplicateGCPs(m_nGCPCount, GDALPamDataset::GetGCPs());

        // Invalidate Geotransorm got from less prioritary sources
        if (m_nGCPCount > 0 && m_bGeoTransformValid && !bGotGTFromPAM &&
            m_nPAMGeorefSrcIndex == 0)
        {
            m_bGeoTransformValid = false;
        }

        // m_nProjectionGeorefSrcIndex = m_nPAMGeorefSrcIndex;

        const auto *poPamGCPSRS = GDALPamDataset::GetGCPSpatialRef();
        if (poPamGCPSRS)
            m_oSRS = *poPamGCPSRS;
        else
            m_oSRS.Clear();

        m_bLookedForProjection = true;
    }

    if (m_nPAMGeorefSrcIndex >= 0)
    {
        CPLXMLNode *psValueAsXML = nullptr;
        CPLXMLNode *psGeodataXform = nullptr;
        char **papszXML = oMDMD.GetMetadata("xml:ESRI");
        if (CSLCount(papszXML) == 1)
        {
            psValueAsXML = CPLParseXMLString(papszXML[0]);
            if (psValueAsXML)
                psGeodataXform = CPLGetXMLNode(psValueAsXML, "=GeodataXform");
        }

        const char *pszTIFFTagResUnit =
            GetMetadataItem("TIFFTAG_RESOLUTIONUNIT");
        const char *pszTIFFTagXRes = GetMetadataItem("TIFFTAG_XRESOLUTION");
        const char *pszTIFFTagYRes = GetMetadataItem("TIFFTAG_YRESOLUTION");
        if (psGeodataXform && pszTIFFTagResUnit && pszTIFFTagXRes &&
            pszTIFFTagYRes && atoi(pszTIFFTagResUnit) == 2)
        {
            CPLXMLNode *psSourceGCPs =
                CPLGetXMLNode(psGeodataXform, "SourceGCPs");
            CPLXMLNode *psTargetGCPs =
                CPLGetXMLNode(psGeodataXform, "TargetGCPs");
            if (psSourceGCPs && psTargetGCPs)
            {
                std::vector<double> adfSourceGCPs, adfTargetGCPs;
                for (CPLXMLNode *psIter = psSourceGCPs->psChild;
                     psIter != nullptr; psIter = psIter->psNext)
                {
                    if (psIter->eType == CXT_Element &&
                        EQUAL(psIter->pszValue, "Double"))
                    {
                        adfSourceGCPs.push_back(
                            CPLAtof(CPLGetXMLValue(psIter, nullptr, "")));
                    }
                }
                for (CPLXMLNode *psIter = psTargetGCPs->psChild;
                     psIter != nullptr; psIter = psIter->psNext)
                {
                    if (psIter->eType == CXT_Element &&
                        EQUAL(psIter->pszValue, "Double"))
                    {
                        adfTargetGCPs.push_back(
                            CPLAtof(CPLGetXMLValue(psIter, nullptr, "")));
                    }
                }
                if (adfSourceGCPs.size() == adfTargetGCPs.size() &&
                    (adfSourceGCPs.size() % 2) == 0)
                {
                    if (m_nGCPCount > 0)
                    {
                        GDALDeinitGCPs(m_nGCPCount, m_pasGCPList);
                        CPLFree(m_pasGCPList);
                        m_pasGCPList = nullptr;
                        m_nGCPCount = 0;
                    }

                    m_nGCPCount = static_cast<int>(adfSourceGCPs.size() / 2);
                    m_pasGCPList = static_cast<GDAL_GCP *>(
                        CPLCalloc(sizeof(GDAL_GCP), m_nGCPCount));
                    for (int i = 0; i < m_nGCPCount; ++i)
                    {
                        m_pasGCPList[i].pszId = CPLStrdup("");
                        m_pasGCPList[i].pszInfo = CPLStrdup("");
                        // The origin used is the bottom left corner,
                        // and raw values are in inches!
                        m_pasGCPList[i].dfGCPPixel =
                            adfSourceGCPs[2 * i] * CPLAtof(pszTIFFTagXRes);
                        m_pasGCPList[i].dfGCPLine =
                            nRasterYSize -
                            adfSourceGCPs[2 * i + 1] * CPLAtof(pszTIFFTagYRes);
                        m_pasGCPList[i].dfGCPX = adfTargetGCPs[2 * i];
                        m_pasGCPList[i].dfGCPY = adfTargetGCPs[2 * i + 1];
                    }

                    // Invalidate Geotransorm got from less prioritary sources
                    if (m_nGCPCount > 0 && m_bGeoTransformValid &&
                        !bGotGTFromPAM && m_nPAMGeorefSrcIndex == 0)
                    {
                        m_bGeoTransformValid = false;
                    }
                }
            }
        }
        if (psValueAsXML)
            CPLDestroyXMLNode(psValueAsXML);
    }

    /* -------------------------------------------------------------------- */
    /*      Copy any PAM metadata into our GeoTIFF context, and with        */
    /*      the PAM info overriding the GeoTIFF context.                    */
    /* -------------------------------------------------------------------- */
    char **papszPamDomains = oMDMD.GetDomainList();

    for (int iDomain = 0;
         papszPamDomains && papszPamDomains[iDomain] != nullptr; ++iDomain)
    {
        const char *pszDomain = papszPamDomains[iDomain];
        char **papszGT_MD = CSLDuplicate(m_oGTiffMDMD.GetMetadata(pszDomain));
        char **papszPAM_MD = oMDMD.GetMetadata(pszDomain);

        papszGT_MD = CSLMerge(papszGT_MD, papszPAM_MD);

        m_oGTiffMDMD.SetMetadata(papszGT_MD, pszDomain);
        CSLDestroy(papszGT_MD);
    }

    for (int i = 1; i <= GetRasterCount(); ++i)
    {
        GTiffRasterBand *poBand =
            cpl::down_cast<GTiffRasterBand *>(GetRasterBand(i));
        papszPamDomains = poBand->oMDMD.GetDomainList();

        for (int iDomain = 0;
             papszPamDomains && papszPamDomains[iDomain] != nullptr; ++iDomain)
        {
            const char *pszDomain = papszPamDomains[iDomain];
            char **papszGT_MD =
                CSLDuplicate(poBand->m_oGTiffMDMD.GetMetadata(pszDomain));
            char **papszPAM_MD = poBand->oMDMD.GetMetadata(pszDomain);

            papszGT_MD = CSLMerge(papszGT_MD, papszPAM_MD);

            poBand->m_oGTiffMDMD.SetMetadata(papszGT_MD, pszDomain);
            CSLDestroy(papszGT_MD);
        }
    }
}

/************************************************************************/
/*                              OpenDir()                               */
/*                                                                      */
/*      Open a specific directory as encoded into a filename.           */
/************************************************************************/

GDALDataset *GTiffDataset::OpenDir(GDALOpenInfo *poOpenInfo)

{
    bool bAllowRGBAInterface = true;
    const char *pszFilename = poOpenInfo->pszFilename;
    if (STARTS_WITH_CI(pszFilename, "GTIFF_RAW:"))
    {
        bAllowRGBAInterface = false;
        pszFilename += strlen("GTIFF_RAW:");
    }

    if (!STARTS_WITH_CI(pszFilename, "GTIFF_DIR:") ||
        pszFilename[strlen("GTIFF_DIR:")] == '\0')
    {
        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Split out filename, and dir#/offset.                            */
    /* -------------------------------------------------------------------- */
    pszFilename += strlen("GTIFF_DIR:");
    bool bAbsolute = false;

    if (STARTS_WITH_CI(pszFilename, "off:"))
    {
        bAbsolute = true;
        pszFilename += 4;
    }

    toff_t nOffset = atol(pszFilename);
    pszFilename += 1;

    while (*pszFilename != '\0' && pszFilename[-1] != ':')
        ++pszFilename;

    if (*pszFilename == '\0' || nOffset == 0)
    {
        ReportError(
            pszFilename, CE_Failure, CPLE_OpenFailed,
            "Unable to extract offset or filename, should take the form:\n"
            "GTIFF_DIR:<dir>:filename or GTIFF_DIR:off:<dir_offset>:filename");
        return nullptr;
    }

    if (poOpenInfo->eAccess == GA_Update)
    {
        ReportError(pszFilename, CE_Warning, CPLE_AppDefined,
                    "Opening a specific TIFF directory is not supported in "
                    "update mode. Switching to read-only");
    }

    /* -------------------------------------------------------------------- */
    /*      Try opening the dataset.                                        */
    /* -------------------------------------------------------------------- */
    if (!GTiffOneTimeInit())
        return nullptr;

    const char *pszFlag = poOpenInfo->eAccess == GA_Update ? "r+DC" : "rDOC";
    VSILFILE *l_fpL = VSIFOpenL(pszFilename, pszFlag);
    if (l_fpL == nullptr)
        return nullptr;
    TIFF *l_hTIFF = VSI_TIFFOpen(pszFilename, pszFlag, l_fpL);
    if (l_hTIFF == nullptr)
    {
        CPL_IGNORE_RET_VAL(VSIFCloseL(l_fpL));
        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      If a directory was requested by index, advance to it now.       */
    /* -------------------------------------------------------------------- */
    if (!bAbsolute)
    {
        const toff_t nOffsetRequested = nOffset;
        while (nOffset > 1)
        {
            if (TIFFReadDirectory(l_hTIFF) == 0)
            {
                XTIFFClose(l_hTIFF);
                ReportError(pszFilename, CE_Failure, CPLE_OpenFailed,
                            "Requested directory %lu not found.",
                            static_cast<long unsigned int>(nOffsetRequested));
                CPL_IGNORE_RET_VAL(VSIFCloseL(l_fpL));
                return nullptr;
            }
            nOffset--;
        }

        nOffset = TIFFCurrentDirOffset(l_hTIFF);
    }

    /* -------------------------------------------------------------------- */
    /*      Create a corresponding GDALDataset.                             */
    /* -------------------------------------------------------------------- */
    GTiffDataset *poDS = new GTiffDataset();
    poDS->SetDescription(poOpenInfo->pszFilename);
    poDS->m_pszFilename = CPLStrdup(pszFilename);
    poDS->m_fpL = l_fpL;
    poDS->m_hTIFF = l_hTIFF;
    poDS->m_bSingleIFDOpened = true;

    if (!EQUAL(pszFilename, poOpenInfo->pszFilename) &&
        !STARTS_WITH_CI(poOpenInfo->pszFilename, "GTIFF_RAW:"))
    {
        poDS->SetPhysicalFilename(pszFilename);
        poDS->SetSubdatasetName(poOpenInfo->pszFilename);
    }

    if (poOpenInfo->AreSiblingFilesLoaded())
        poDS->oOvManager.TransferSiblingFiles(poOpenInfo->StealSiblingFiles());

    if (poDS->OpenOffset(l_hTIFF, nOffset, poOpenInfo->eAccess,
                         bAllowRGBAInterface, true) != CE_None)
    {
        delete poDS;
        return nullptr;
    }

    return poDS;
}

/************************************************************************/
/*                   ConvertTransferFunctionToString()                  */
/*                                                                      */
/*      Convert a transfer function table into a string.                */
/*      Used by LoadICCProfile().                                       */
/************************************************************************/
static CPLString ConvertTransferFunctionToString(const uint16_t *pTable,
                                                 uint32_t nTableEntries)
{
    CPLString sValue;

    for (uint32_t i = 0; i < nTableEntries; ++i)
    {
        if (i > 0)
            sValue += ", ";
        sValue += CPLSPrintf("%d", static_cast<uint32_t>(pTable[i]));
    }

    return sValue;
}

/************************************************************************/
/*                             LoadICCProfile()                         */
/*                                                                      */
/*      Load ICC Profile or colorimetric data into metadata             */
/************************************************************************/

void GTiffDataset::LoadICCProfile()
{
    if (m_bICCMetadataLoaded)
        return;
    m_bICCMetadataLoaded = true;

    uint32_t nEmbedLen = 0;
    uint8_t *pEmbedBuffer = nullptr;

    if (TIFFGetField(m_hTIFF, TIFFTAG_ICCPROFILE, &nEmbedLen, &pEmbedBuffer))
    {
        char *pszBase64Profile = CPLBase64Encode(
            nEmbedLen, reinterpret_cast<const GByte *>(pEmbedBuffer));

        m_oGTiffMDMD.SetMetadataItem("SOURCE_ICC_PROFILE", pszBase64Profile,
                                     "COLOR_PROFILE");

        CPLFree(pszBase64Profile);

        return;
    }

    // Check for colorimetric tiff.
    float *pCHR = nullptr;
    float *pWP = nullptr;
    uint16_t *pTFR = nullptr;
    uint16_t *pTFG = nullptr;
    uint16_t *pTFB = nullptr;
    uint16_t *pTransferRange = nullptr;

    if (TIFFGetField(m_hTIFF, TIFFTAG_PRIMARYCHROMATICITIES, &pCHR))
    {
        if (TIFFGetField(m_hTIFF, TIFFTAG_WHITEPOINT, &pWP))
        {
            if (!TIFFGetFieldDefaulted(m_hTIFF, TIFFTAG_TRANSFERFUNCTION, &pTFR,
                                       &pTFG, &pTFB) ||
                pTFR == nullptr || pTFG == nullptr || pTFB == nullptr)
            {
                return;
            }

            const int TIFFTAG_TRANSFERRANGE = 0x0156;
            TIFFGetFieldDefaulted(m_hTIFF, TIFFTAG_TRANSFERRANGE,
                                  &pTransferRange);

            // Set all the colorimetric metadata.
            m_oGTiffMDMD.SetMetadataItem(
                "SOURCE_PRIMARIES_RED",
                CPLString().Printf("%.9f, %.9f, 1.0",
                                   static_cast<double>(pCHR[0]),
                                   static_cast<double>(pCHR[1])),
                "COLOR_PROFILE");
            m_oGTiffMDMD.SetMetadataItem(
                "SOURCE_PRIMARIES_GREEN",
                CPLString().Printf("%.9f, %.9f, 1.0",
                                   static_cast<double>(pCHR[2]),
                                   static_cast<double>(pCHR[3])),
                "COLOR_PROFILE");
            m_oGTiffMDMD.SetMetadataItem(
                "SOURCE_PRIMARIES_BLUE",
                CPLString().Printf("%.9f, %.9f, 1.0",
                                   static_cast<double>(pCHR[4]),
                                   static_cast<double>(pCHR[5])),
                "COLOR_PROFILE");

            m_oGTiffMDMD.SetMetadataItem(
                "SOURCE_WHITEPOINT",
                CPLString().Printf("%.9f, %.9f, 1.0",
                                   static_cast<double>(pWP[0]),
                                   static_cast<double>(pWP[1])),
                "COLOR_PROFILE");

            // Set transfer function metadata.

            // Get length of table.
            const uint32_t nTransferFunctionLength = 1 << m_nBitsPerSample;

            m_oGTiffMDMD.SetMetadataItem(
                "TIFFTAG_TRANSFERFUNCTION_RED",
                ConvertTransferFunctionToString(pTFR, nTransferFunctionLength),
                "COLOR_PROFILE");

            m_oGTiffMDMD.SetMetadataItem(
                "TIFFTAG_TRANSFERFUNCTION_GREEN",
                ConvertTransferFunctionToString(pTFG, nTransferFunctionLength),
                "COLOR_PROFILE");

            m_oGTiffMDMD.SetMetadataItem(
                "TIFFTAG_TRANSFERFUNCTION_BLUE",
                ConvertTransferFunctionToString(pTFB, nTransferFunctionLength),
                "COLOR_PROFILE");

            // Set transfer range.
            if (pTransferRange)
            {
                m_oGTiffMDMD.SetMetadataItem(
                    "TIFFTAG_TRANSFERRANGE_BLACK",
                    CPLString().Printf("%d, %d, %d",
                                       static_cast<int>(pTransferRange[0]),
                                       static_cast<int>(pTransferRange[2]),
                                       static_cast<int>(pTransferRange[4])),
                    "COLOR_PROFILE");
                m_oGTiffMDMD.SetMetadataItem(
                    "TIFFTAG_TRANSFERRANGE_WHITE",
                    CPLString().Printf("%d, %d, %d",
                                       static_cast<int>(pTransferRange[1]),
                                       static_cast<int>(pTransferRange[3]),
                                       static_cast<int>(pTransferRange[5])),
                    "COLOR_PROFILE");
            }
        }
    }
}

/************************************************************************/
/*                             SaveICCProfile()                         */
/*                                                                      */
/*      Save ICC Profile or colorimetric data into file                 */
/* pDS:                                                                 */
/*      Dataset that contains the metadata with the ICC or colorimetric */
/*      data. If this argument is specified, all other arguments are    */
/*      ignored. Set them to NULL or 0.                                 */
/* hTIFF:                                                               */
/*      Pointer to TIFF handle. Only needed if pDS is NULL or           */
/*      pDS->m_hTIFF is NULL.                                             */
/* papszParamList:                                                       */
/*      Options containing the ICC profile or colorimetric metadata.    */
/*      Ignored if pDS is not NULL.                                     */
/* nBitsPerSample:                                                      */
/*      Bits per sample. Ignored if pDS is not NULL.                    */
/************************************************************************/

void GTiffDataset::SaveICCProfile(GTiffDataset *pDS, TIFF *l_hTIFF,
                                  char **papszParamList,
                                  uint32_t l_nBitsPerSample)
{
    if ((pDS != nullptr) && (pDS->eAccess != GA_Update))
        return;

    if (l_hTIFF == nullptr)
    {
        if (pDS == nullptr)
            return;

        l_hTIFF = pDS->m_hTIFF;
        if (l_hTIFF == nullptr)
            return;
    }

    if ((papszParamList == nullptr) && (pDS == nullptr))
        return;

    const char *pszValue = nullptr;
    if (pDS != nullptr)
        pszValue = pDS->GetMetadataItem("SOURCE_ICC_PROFILE", "COLOR_PROFILE");
    else
        pszValue = CSLFetchNameValue(papszParamList, "SOURCE_ICC_PROFILE");
    if (pszValue != nullptr)
    {
        char *pEmbedBuffer = CPLStrdup(pszValue);
        int32_t nEmbedLen =
            CPLBase64DecodeInPlace(reinterpret_cast<GByte *>(pEmbedBuffer));

        TIFFSetField(l_hTIFF, TIFFTAG_ICCPROFILE, nEmbedLen, pEmbedBuffer);

        CPLFree(pEmbedBuffer);
    }
    else
    {
        // Output colorimetric data.
        float pCHR[6] = {};     // Primaries.
        uint16_t pTXR[6] = {};  // Transfer range.
        const char *pszCHRNames[] = {"SOURCE_PRIMARIES_RED",
                                     "SOURCE_PRIMARIES_GREEN",
                                     "SOURCE_PRIMARIES_BLUE"};
        const char *pszTXRNames[] = {"TIFFTAG_TRANSFERRANGE_BLACK",
                                     "TIFFTAG_TRANSFERRANGE_WHITE"};

        // Output chromacities.
        bool bOutputCHR = true;
        for (int i = 0; i < 3 && bOutputCHR; ++i)
        {
            if (pDS != nullptr)
                pszValue =
                    pDS->GetMetadataItem(pszCHRNames[i], "COLOR_PROFILE");
            else
                pszValue = CSLFetchNameValue(papszParamList, pszCHRNames[i]);
            if (pszValue == nullptr)
            {
                bOutputCHR = false;
                break;
            }

            char **papszTokens = CSLTokenizeString2(pszValue, ",",
                                                    CSLT_ALLOWEMPTYTOKENS |
                                                        CSLT_STRIPLEADSPACES |
                                                        CSLT_STRIPENDSPACES);

            if (CSLCount(papszTokens) != 3)
            {
                bOutputCHR = false;
                CSLDestroy(papszTokens);
                break;
            }

            for (int j = 0; j < 3; ++j)
            {
                float v = static_cast<float>(CPLAtof(papszTokens[j]));

                if (j == 2)
                {
                    // Last term of xyY color must be 1.0.
                    if (v != 1.0)
                    {
                        bOutputCHR = false;
                        break;
                    }
                }
                else
                {
                    pCHR[i * 2 + j] = v;
                }
            }

            CSLDestroy(papszTokens);
        }

        if (bOutputCHR)
        {
            TIFFSetField(l_hTIFF, TIFFTAG_PRIMARYCHROMATICITIES, pCHR);
        }

        // Output whitepoint.
        if (pDS != nullptr)
            pszValue =
                pDS->GetMetadataItem("SOURCE_WHITEPOINT", "COLOR_PROFILE");
        else
            pszValue = CSLFetchNameValue(papszParamList, "SOURCE_WHITEPOINT");
        if (pszValue != nullptr)
        {
            char **papszTokens = CSLTokenizeString2(pszValue, ",",
                                                    CSLT_ALLOWEMPTYTOKENS |
                                                        CSLT_STRIPLEADSPACES |
                                                        CSLT_STRIPENDSPACES);

            bool bOutputWhitepoint = true;
            float pWP[2] = {0.0f, 0.0f};  // Whitepoint
            if (CSLCount(papszTokens) != 3)
            {
                bOutputWhitepoint = false;
            }
            else
            {
                for (int j = 0; j < 3; ++j)
                {
                    const float v = static_cast<float>(CPLAtof(papszTokens[j]));

                    if (j == 2)
                    {
                        // Last term of xyY color must be 1.0.
                        if (v != 1.0)
                        {
                            bOutputWhitepoint = false;
                            break;
                        }
                    }
                    else
                    {
                        pWP[j] = v;
                    }
                }
            }
            CSLDestroy(papszTokens);

            if (bOutputWhitepoint)
            {
                TIFFSetField(l_hTIFF, TIFFTAG_WHITEPOINT, pWP);
            }
        }

        // Set transfer function metadata.
        char const *pszTFRed = nullptr;
        if (pDS != nullptr)
            pszTFRed = pDS->GetMetadataItem("TIFFTAG_TRANSFERFUNCTION_RED",
                                            "COLOR_PROFILE");
        else
            pszTFRed = CSLFetchNameValue(papszParamList,
                                         "TIFFTAG_TRANSFERFUNCTION_RED");

        char const *pszTFGreen = nullptr;
        if (pDS != nullptr)
            pszTFGreen = pDS->GetMetadataItem("TIFFTAG_TRANSFERFUNCTION_GREEN",
                                              "COLOR_PROFILE");
        else
            pszTFGreen = CSLFetchNameValue(papszParamList,
                                           "TIFFTAG_TRANSFERFUNCTION_GREEN");

        char const *pszTFBlue = nullptr;
        if (pDS != nullptr)
            pszTFBlue = pDS->GetMetadataItem("TIFFTAG_TRANSFERFUNCTION_BLUE",
                                             "COLOR_PROFILE");
        else
            pszTFBlue = CSLFetchNameValue(papszParamList,
                                          "TIFFTAG_TRANSFERFUNCTION_BLUE");

        if ((pszTFRed != nullptr) && (pszTFGreen != nullptr) &&
            (pszTFBlue != nullptr))
        {
            // Get length of table.
            const int nTransferFunctionLength =
                1 << ((pDS != nullptr) ? pDS->m_nBitsPerSample
                                       : l_nBitsPerSample);

            char **papszTokensRed = CSLTokenizeString2(
                pszTFRed, ",",
                CSLT_ALLOWEMPTYTOKENS | CSLT_STRIPLEADSPACES |
                    CSLT_STRIPENDSPACES);
            char **papszTokensGreen = CSLTokenizeString2(
                pszTFGreen, ",",
                CSLT_ALLOWEMPTYTOKENS | CSLT_STRIPLEADSPACES |
                    CSLT_STRIPENDSPACES);
            char **papszTokensBlue = CSLTokenizeString2(
                pszTFBlue, ",",
                CSLT_ALLOWEMPTYTOKENS | CSLT_STRIPLEADSPACES |
                    CSLT_STRIPENDSPACES);

            if ((CSLCount(papszTokensRed) == nTransferFunctionLength) &&
                (CSLCount(papszTokensGreen) == nTransferFunctionLength) &&
                (CSLCount(papszTokensBlue) == nTransferFunctionLength))
            {
                uint16_t *pTransferFuncRed = static_cast<uint16_t *>(
                    CPLMalloc(sizeof(uint16_t) * nTransferFunctionLength));
                uint16_t *pTransferFuncGreen = static_cast<uint16_t *>(
                    CPLMalloc(sizeof(uint16_t) * nTransferFunctionLength));
                uint16_t *pTransferFuncBlue = static_cast<uint16_t *>(
                    CPLMalloc(sizeof(uint16_t) * nTransferFunctionLength));

                // Convert our table in string format into int16_t format.
                for (int i = 0; i < nTransferFunctionLength; ++i)
                {
                    pTransferFuncRed[i] =
                        static_cast<uint16_t>(atoi(papszTokensRed[i]));
                    pTransferFuncGreen[i] =
                        static_cast<uint16_t>(atoi(papszTokensGreen[i]));
                    pTransferFuncBlue[i] =
                        static_cast<uint16_t>(atoi(papszTokensBlue[i]));
                }

                TIFFSetField(l_hTIFF, TIFFTAG_TRANSFERFUNCTION,
                             pTransferFuncRed, pTransferFuncGreen,
                             pTransferFuncBlue);

                CPLFree(pTransferFuncRed);
                CPLFree(pTransferFuncGreen);
                CPLFree(pTransferFuncBlue);
            }

            CSLDestroy(papszTokensRed);
            CSLDestroy(papszTokensGreen);
            CSLDestroy(papszTokensBlue);
        }

        // Output transfer range.
        bool bOutputTransferRange = true;
        for (int i = 0; (i < 2) && bOutputTransferRange; ++i)
        {
            if (pDS != nullptr)
                pszValue =
                    pDS->GetMetadataItem(pszTXRNames[i], "COLOR_PROFILE");
            else
                pszValue = CSLFetchNameValue(papszParamList, pszTXRNames[i]);
            if (pszValue == nullptr)
            {
                bOutputTransferRange = false;
                break;
            }

            char **papszTokens = CSLTokenizeString2(pszValue, ",",
                                                    CSLT_ALLOWEMPTYTOKENS |
                                                        CSLT_STRIPLEADSPACES |
                                                        CSLT_STRIPENDSPACES);

            if (CSLCount(papszTokens) != 3)
            {
                bOutputTransferRange = false;
                CSLDestroy(papszTokens);
                break;
            }

            for (int j = 0; j < 3; ++j)
            {
                pTXR[i + j * 2] = static_cast<uint16_t>(atoi(papszTokens[j]));
            }

            CSLDestroy(papszTokens);
        }

        if (bOutputTransferRange)
        {
            const int TIFFTAG_TRANSFERRANGE = 0x0156;
            TIFFSetField(l_hTIFF, TIFFTAG_TRANSFERRANGE, pTXR);
        }
    }
}

/************************************************************************/
/*                             OpenOffset()                             */
/*                                                                      */
/*      Initialize the GTiffDataset based on a passed in file           */
/*      handle, and directory offset to utilize.  This is called for    */
/*      full res, and overview pages.                                   */
/************************************************************************/

CPLErr GTiffDataset::OpenOffset(TIFF *hTIFFIn, toff_t nDirOffsetIn,
                                GDALAccess eAccessIn, bool bAllowRGBAInterface,
                                bool bReadGeoTransform)

{
    if (!hTIFFIn)
        return CE_Failure;

    eAccess = eAccessIn;

    m_hTIFF = hTIFFIn;

    m_nDirOffset = nDirOffsetIn;

    if (!SetDirectory())
        return CE_Failure;

    /* -------------------------------------------------------------------- */
    /*      Capture some information from the file that is of interest.     */
    /* -------------------------------------------------------------------- */
    uint32_t nXSize = 0;
    uint32_t nYSize = 0;
    TIFFGetField(m_hTIFF, TIFFTAG_IMAGEWIDTH, &nXSize);
    TIFFGetField(m_hTIFF, TIFFTAG_IMAGELENGTH, &nYSize);

    // Unlikely to occur, but could happen on a disk full situation.
    if (nXSize == 0 || nYSize == 0)
        return CE_Failure;

    if (nXSize > INT_MAX || nYSize > INT_MAX)
    {
        // GDAL only supports signed 32bit dimensions.
        ReportError(CE_Failure, CPLE_NotSupported,
                    "Too large image size: %u x %u", nXSize, nYSize);
        return CE_Failure;
    }
    nRasterXSize = nXSize;
    nRasterYSize = nYSize;

    if (!TIFFGetField(m_hTIFF, TIFFTAG_SAMPLESPERPIXEL, &m_nSamplesPerPixel))
        nBands = 1;
    else
        nBands = m_nSamplesPerPixel;

    if (!TIFFGetField(m_hTIFF, TIFFTAG_BITSPERSAMPLE, &(m_nBitsPerSample)))
        m_nBitsPerSample = 1;

    if (!TIFFGetField(m_hTIFF, TIFFTAG_PLANARCONFIG, &(m_nPlanarConfig)))
        m_nPlanarConfig = PLANARCONFIG_CONTIG;

    if (!TIFFGetField(m_hTIFF, TIFFTAG_PHOTOMETRIC, &(m_nPhotometric)))
        m_nPhotometric = PHOTOMETRIC_MINISBLACK;

    if (!TIFFGetField(m_hTIFF, TIFFTAG_SAMPLEFORMAT, &(m_nSampleFormat)))
        m_nSampleFormat = SAMPLEFORMAT_UINT;

    if (!TIFFGetField(m_hTIFF, TIFFTAG_COMPRESSION, &(m_nCompression)))
        m_nCompression = COMPRESSION_NONE;

    if (m_nCompression != COMPRESSION_NONE &&
        !TIFFIsCODECConfigured(m_nCompression))
    {
        const char *pszCompressionMethodName =
            GetCompressionMethodName(m_nCompression);
        if (pszCompressionMethodName)
        {
            ReportError(CE_Failure, CPLE_AppDefined,
                        "Cannot open TIFF file due to missing codec %s.",
                        pszCompressionMethodName);
        }
        else
        {
            ReportError(
                CE_Failure, CPLE_AppDefined,
                "Cannot open TIFF file due to missing codec of code %d.",
                m_nCompression);
        }
        return CE_Failure;
    }

    /* -------------------------------------------------------------------- */
    /*      YCbCr JPEG compressed images should be translated on the fly    */
    /*      to RGB by libtiff/libjpeg unless specifically requested         */
    /*      otherwise.                                                      */
    /* -------------------------------------------------------------------- */
    if (m_nCompression == COMPRESSION_JPEG &&
        m_nPhotometric == PHOTOMETRIC_YCBCR &&
        CPLTestBool(CPLGetConfigOption("CONVERT_YCBCR_TO_RGB", "YES")))
    {
        m_oGTiffMDMD.SetMetadataItem("SOURCE_COLOR_SPACE", "YCbCr",
                                     "IMAGE_STRUCTURE");
        int nColorMode = 0;
        if (!TIFFGetField(m_hTIFF, TIFFTAG_JPEGCOLORMODE, &nColorMode) ||
            nColorMode != JPEGCOLORMODE_RGB)
        {
            TIFFSetField(m_hTIFF, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB);
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Get strip/tile layout.                                          */
    /* -------------------------------------------------------------------- */
    if (TIFFIsTiled(m_hTIFF))
    {
        uint32_t l_nBlockXSize = 0;
        uint32_t l_nBlockYSize = 0;
        TIFFGetField(m_hTIFF, TIFFTAG_TILEWIDTH, &(l_nBlockXSize));
        TIFFGetField(m_hTIFF, TIFFTAG_TILELENGTH, &(l_nBlockYSize));
        if (l_nBlockXSize > INT_MAX || l_nBlockYSize > INT_MAX)
        {
            ReportError(CE_Failure, CPLE_NotSupported,
                        "Too large block size: %u x %u", l_nBlockXSize,
                        l_nBlockYSize);
            return CE_Failure;
        }
        m_nBlockXSize = static_cast<int>(l_nBlockXSize);
        m_nBlockYSize = static_cast<int>(l_nBlockYSize);
    }
    else
    {
        if (!TIFFGetField(m_hTIFF, TIFFTAG_ROWSPERSTRIP, &(m_nRowsPerStrip)))
        {
            ReportError(CE_Warning, CPLE_AppDefined,
                        "RowsPerStrip not defined ... assuming all one strip.");
            m_nRowsPerStrip = nYSize;  // Dummy value.
        }

        // If the rows per strip is larger than the file we will get
        // confused.  libtiff internally will treat the rowsperstrip as
        // the image height and it is best if we do too. (#4468)
        if (m_nRowsPerStrip > static_cast<uint32_t>(nRasterYSize))
            m_nRowsPerStrip = nRasterYSize;

        m_nBlockXSize = nRasterXSize;
        m_nBlockYSize = m_nRowsPerStrip;
    }

    const int l_nBlocksPerColumn = DIV_ROUND_UP(nRasterYSize, m_nBlockYSize);
    const int l_nBlocksPerRow = DIV_ROUND_UP(nRasterXSize, m_nBlockXSize);
    if (l_nBlocksPerColumn > INT_MAX / l_nBlocksPerRow)
    {
        ReportError(CE_Failure, CPLE_AppDefined, "Too many blocks: %d x %d",
                    l_nBlocksPerRow, l_nBlocksPerColumn);
        return CE_Failure;
    }

    // Note: we could potentially go up to UINT_MAX blocks, but currently
    // we use a int nBlockId
    m_nBlocksPerBand = l_nBlocksPerColumn * l_nBlocksPerRow;
    if (m_nPlanarConfig == PLANARCONFIG_SEPARATE &&
        m_nBlocksPerBand > INT_MAX / nBands)
    {
        ReportError(CE_Failure, CPLE_AppDefined,
                    "Too many blocks: %d x %d x %d bands", l_nBlocksPerRow,
                    l_nBlocksPerColumn, nBands);
        return CE_Failure;
    }

    /* -------------------------------------------------------------------- */
    /*      Should we handle this using the GTiffBitmapBand?                */
    /* -------------------------------------------------------------------- */
    bool bTreatAsBitmap = false;

    if (m_nBitsPerSample == 1 && nBands == 1)
    {
        bTreatAsBitmap = true;

        // Lets treat large "one row" bitmaps using the scanline api.
        if (!TIFFIsTiled(m_hTIFF) && m_nBlockYSize == nRasterYSize &&
            nRasterYSize > 2000
            // libtiff does not support reading JBIG files with
            // TIFFReadScanline().
            && m_nCompression != COMPRESSION_JBIG)
        {
            m_bTreatAsSplitBitmap = true;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Should we treat this via the RGBA interface?                    */
    /* -------------------------------------------------------------------- */
    bool bTreatAsRGBA = false;
    if (
#ifdef DEBUG
        CPLTestBool(CPLGetConfigOption("GTIFF_FORCE_RGBA", "NO")) ||
#endif
        (bAllowRGBAInterface && !bTreatAsBitmap && !(m_nBitsPerSample > 8) &&
         (m_nPhotometric == PHOTOMETRIC_CIELAB ||
          m_nPhotometric == PHOTOMETRIC_LOGL ||
          m_nPhotometric == PHOTOMETRIC_LOGLUV ||
          m_nPhotometric == PHOTOMETRIC_SEPARATED ||
          (m_nPhotometric == PHOTOMETRIC_YCBCR &&
           m_nCompression != COMPRESSION_JPEG))))
    {
        char szMessage[1024] = {};

        if (TIFFRGBAImageOK(m_hTIFF, szMessage) == 1)
        {
            const char *pszSourceColorSpace = nullptr;
            nBands = 4;
            switch (m_nPhotometric)
            {
                case PHOTOMETRIC_CIELAB:
                    pszSourceColorSpace = "CIELAB";
                    break;
                case PHOTOMETRIC_LOGL:
                    pszSourceColorSpace = "LOGL";
                    break;
                case PHOTOMETRIC_LOGLUV:
                    pszSourceColorSpace = "LOGLUV";
                    break;
                case PHOTOMETRIC_SEPARATED:
                    pszSourceColorSpace = "CMYK";
                    break;
                case PHOTOMETRIC_YCBCR:
                    pszSourceColorSpace = "YCbCr";
                    nBands = 3;  // probably true for other photometric values
                    break;
            }
            if (pszSourceColorSpace)
                m_oGTiffMDMD.SetMetadataItem("SOURCE_COLOR_SPACE",
                                             pszSourceColorSpace,
                                             "IMAGE_STRUCTURE");
            bTreatAsRGBA = true;
        }
        else
        {
            CPLDebug("GTiff", "TIFFRGBAImageOK says:\n%s", szMessage);
        }
    }

    // libtiff has various issues with OJPEG compression and chunky-strip
    // support with the "classic" scanline/strip/tile interfaces, and that
    // wouldn't work either, so better bail out.
    if (m_nCompression == COMPRESSION_OJPEG && !bTreatAsRGBA)
    {
        ReportError(
            CE_Failure, CPLE_NotSupported,
            "Old-JPEG compression only supported through RGBA interface, "
            "which cannot be used probably because the file is corrupted");
        return CE_Failure;
    }

    // If photometric is YCbCr, scanline/strip/tile interfaces assumes that
    // we are ready with downsampled data. And we are not.
    if (m_nCompression != COMPRESSION_JPEG &&
        m_nCompression != COMPRESSION_OJPEG &&
        m_nPhotometric == PHOTOMETRIC_YCBCR &&
        m_nPlanarConfig == PLANARCONFIG_CONTIG && !bTreatAsRGBA)
    {
        uint16_t nF1, nF2;
        TIFFGetFieldDefaulted(m_hTIFF, TIFFTAG_YCBCRSUBSAMPLING, &nF1, &nF2);
        if (nF1 != 1 || nF2 != 1)
        {
            ReportError(CE_Failure, CPLE_AppDefined,
                        "Cannot open TIFF file with YCbCr, subsampling and "
                        "BitsPerSample > 8 that is not JPEG compressed");
            return CE_Failure;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Should we treat this via the split interface?                   */
    /* -------------------------------------------------------------------- */
    if (!TIFFIsTiled(m_hTIFF) && m_nBitsPerSample == 8 &&
        m_nBlockYSize == nRasterYSize && nRasterYSize > 2000 && !bTreatAsRGBA &&
        CPLTestBool(CPLGetConfigOption("GDAL_ENABLE_TIFF_SPLIT", "YES")))
    {
        // libtiff 4.0.0beta5 (also
        // 20091104) and older will crash when trying to open a
        // all-in-one-strip YCbCr JPEG compressed TIFF (see #3259).
#if (TIFFLIB_VERSION <= 20091104)
        if (m_nPhotometric == PHOTOMETRIC_YCBCR &&
            m_nCompression == COMPRESSION_JPEG)
        {
            CPLDebug("GTiff",
                     "Avoid using split band to open all-in-one-strip "
                     "YCbCr JPEG compressed TIFF because of older libtiff");
        }
        else
#endif
        {
            m_bTreatAsSplit = true;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Should we treat this via the odd bits interface?                */
    /* -------------------------------------------------------------------- */
    bool bTreatAsOdd = false;
    if (m_nSampleFormat == SAMPLEFORMAT_IEEEFP)
    {
        if (m_nBitsPerSample == 16 || m_nBitsPerSample == 24)
            bTreatAsOdd = true;
        else if (m_nBitsPerSample != 32 && m_nBitsPerSample != 64)
        {
            ReportError(CE_Failure, CPLE_AppDefined,
                        "Cannot open TIFF file with SampleFormat=IEEEFP "
                        "and BitsPerSample=%d",
                        m_nBitsPerSample);
            return CE_Failure;
        }
    }
    else if (!bTreatAsRGBA && !bTreatAsBitmap && m_nBitsPerSample != 8 &&
             m_nBitsPerSample != 16 && m_nBitsPerSample != 32 &&
             m_nBitsPerSample != 64 && m_nBitsPerSample != 128)
    {
        bTreatAsOdd = true;
    }

/* -------------------------------------------------------------------- */
/*      We can't support 'chunks' bigger than 2GB on 32 bit builds      */
/* -------------------------------------------------------------------- */
#if SIZEOF_VOIDP == 4
    uint64_t nChunkSize = 0;
    if (m_bTreatAsSplit || m_bTreatAsSplitBitmap)
    {
        nChunkSize = TIFFScanlineSize64(m_hTIFF);
    }
    else
    {
        if (TIFFIsTiled(m_hTIFF))
            nChunkSize = TIFFTileSize64(m_hTIFF);
        else
            nChunkSize = TIFFStripSize64(m_hTIFF);
    }
    if (bTreatAsRGBA)
    {
        nChunkSize =
            std::max(nChunkSize,
                     4 * static_cast<uint64_t>(m_nBlockXSize) * m_nBlockYSize);
    }
    if (nChunkSize > static_cast<uint64_t>(INT_MAX))
    {
        ReportError(CE_Failure, CPLE_NotSupported,
                    "Scanline/tile/strip size bigger than 2GB unsupported "
                    "on 32-bit builds.");
        return CE_Failure;
    }
#endif

    const bool bMinIsWhite = m_nPhotometric == PHOTOMETRIC_MINISWHITE;

    /* -------------------------------------------------------------------- */
    /*      Check for NODATA                                                */
    /* -------------------------------------------------------------------- */
    char *pszText = nullptr;
    if (TIFFGetField(m_hTIFF, TIFFTAG_GDAL_NODATA, &pszText) &&
        !EQUAL(pszText, ""))
    {
        if (m_nBitsPerSample > 32 && m_nBitsPerSample <= 64 &&
            m_nSampleFormat == SAMPLEFORMAT_INT)
        {
            m_bNoDataSetAsInt64 = true;
            m_nNoDataValueInt64 =
                static_cast<int64_t>(std::strtoll(pszText, nullptr, 10));
        }
        else if (m_nBitsPerSample > 32 && m_nBitsPerSample <= 64 &&
                 m_nSampleFormat == SAMPLEFORMAT_UINT)
        {
            m_bNoDataSetAsUInt64 = true;
            m_nNoDataValueUInt64 =
                static_cast<uint64_t>(std::strtoull(pszText, nullptr, 10));
        }
        else
        {
            m_bNoDataSet = true;
            m_dfNoDataValue = CPLAtofM(pszText);
            if (m_nBitsPerSample == 32 &&
                m_nSampleFormat == SAMPLEFORMAT_IEEEFP)
            {
                m_dfNoDataValue =
                    GDALAdjustNoDataCloseToFloatMax(m_dfNoDataValue);
                m_dfNoDataValue = static_cast<float>(m_dfNoDataValue);
            }
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Capture the color table if there is one.                        */
    /* -------------------------------------------------------------------- */
    unsigned short *panRed = nullptr;
    unsigned short *panGreen = nullptr;
    unsigned short *panBlue = nullptr;

    if (bTreatAsRGBA || m_nBitsPerSample > 16 ||
        TIFFGetField(m_hTIFF, TIFFTAG_COLORMAP, &panRed, &panGreen, &panBlue) ==
            0)
    {
        // Build inverted palette if we have inverted photometric.
        // Pixel values remains unchanged.  Avoid doing this for *deep*
        // data types (per #1882)
        if (m_nBitsPerSample <= 16 && m_nPhotometric == PHOTOMETRIC_MINISWHITE)
        {
            m_poColorTable = new GDALColorTable();
            const int nColorCount = 1 << m_nBitsPerSample;

            for (int iColor = 0; iColor < nColorCount; ++iColor)
            {
                const short nValue = static_cast<short>(
                    ((255 * (nColorCount - 1 - iColor)) / (nColorCount - 1)));
                const GDALColorEntry oEntry = {nValue, nValue, nValue,
                                               static_cast<short>(255)};
                m_poColorTable->SetColorEntry(iColor, &oEntry);
            }

            m_nPhotometric = PHOTOMETRIC_PALETTE;
        }
        else
        {
            m_poColorTable = nullptr;
        }
    }
    else
    {
        unsigned short nMaxColor = 0;

        m_poColorTable = new GDALColorTable();

        const int nColorCount = 1 << m_nBitsPerSample;

        for (int iColor = nColorCount - 1; iColor >= 0; iColor--)
        {
            // TODO(schwehr): Ensure the color entries are never negative?
            const unsigned short divisor = 257;
            const GDALColorEntry oEntry = {
                static_cast<short>(panRed[iColor] / divisor),
                static_cast<short>(panGreen[iColor] / divisor),
                static_cast<short>(panBlue[iColor] / divisor),
                static_cast<short>(
                    m_bNoDataSet && static_cast<int>(m_dfNoDataValue) == iColor
                        ? 0
                        : 255)};

            m_poColorTable->SetColorEntry(iColor, &oEntry);

            nMaxColor = std::max(nMaxColor, panRed[iColor]);
            nMaxColor = std::max(nMaxColor, panGreen[iColor]);
            nMaxColor = std::max(nMaxColor, panBlue[iColor]);
        }

        // Bug 1384 - Some TIFF files are generated with color map entry
        // values in range 0-255 instead of 0-65535 - try to handle these
        // gracefully.
        if (nMaxColor > 0 && nMaxColor < 256)
        {
            CPLDebug(
                "GTiff",
                "TIFF ColorTable seems to be improperly scaled, fixing up.");

            for (int iColor = nColorCount - 1; iColor >= 0; iColor--)
            {
                // TODO(schwehr): Ensure the color entries are never negative?
                const GDALColorEntry oEntry = {
                    static_cast<short>(panRed[iColor]),
                    static_cast<short>(panGreen[iColor]),
                    static_cast<short>(panBlue[iColor]),
                    m_bNoDataSet && static_cast<int>(m_dfNoDataValue) == iColor
                        ? static_cast<short>(0)
                        : static_cast<short>(255)};

                m_poColorTable->SetColorEntry(iColor, &oEntry);
            }
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Create band information objects.                                */
    /* -------------------------------------------------------------------- */
    for (int iBand = 0; iBand < nBands; ++iBand)
    {
        if (bTreatAsRGBA)
            SetBand(iBand + 1, new GTiffRGBABand(this, iBand + 1));
        else if (m_bTreatAsSplitBitmap)
            SetBand(iBand + 1, new GTiffSplitBitmapBand(this, iBand + 1));
        else if (m_bTreatAsSplit)
            SetBand(iBand + 1, new GTiffSplitBand(this, iBand + 1));
        else if (bTreatAsBitmap)
            SetBand(iBand + 1, new GTiffBitmapBand(this, iBand + 1));
        else if (bTreatAsOdd)
            SetBand(iBand + 1, new GTiffOddBitsBand(this, iBand + 1));
        else
            SetBand(iBand + 1, new GTiffRasterBand(this, iBand + 1));
    }

    if (GetRasterBand(1)->GetRasterDataType() == GDT_Unknown)
    {
        ReportError(CE_Failure, CPLE_NotSupported,
                    "Unsupported TIFF configuration: BitsPerSample(=%d) and "
                    "SampleType(=%d)",
                    m_nBitsPerSample, m_nSampleFormat);
        return CE_Failure;
    }

    m_bReadGeoTransform = bReadGeoTransform;

    /* -------------------------------------------------------------------- */
    /*      Capture some other potentially interesting information.         */
    /* -------------------------------------------------------------------- */
    char szWorkMDI[200] = {};
    uint16_t nShort = 0;

    for (size_t iTag = 0; iTag < sizeof(asTIFFTags) / sizeof(asTIFFTags[0]);
         ++iTag)
    {
        if (asTIFFTags[iTag].eType == GTIFFTAGTYPE_STRING)
        {
            if (TIFFGetField(m_hTIFF, asTIFFTags[iTag].nTagVal, &pszText))
                m_oGTiffMDMD.SetMetadataItem(asTIFFTags[iTag].pszTagName,
                                             pszText);
        }
        else if (asTIFFTags[iTag].eType == GTIFFTAGTYPE_FLOAT)
        {
            float fVal = 0.0;
            if (TIFFGetField(m_hTIFF, asTIFFTags[iTag].nTagVal, &fVal))
            {
                CPLsnprintf(szWorkMDI, sizeof(szWorkMDI), "%.8g", fVal);
                m_oGTiffMDMD.SetMetadataItem(asTIFFTags[iTag].pszTagName,
                                             szWorkMDI);
            }
        }
        else if (asTIFFTags[iTag].eType == GTIFFTAGTYPE_SHORT &&
                 asTIFFTags[iTag].nTagVal != TIFFTAG_RESOLUTIONUNIT)
        {
            if (TIFFGetField(m_hTIFF, asTIFFTags[iTag].nTagVal, &nShort))
            {
                snprintf(szWorkMDI, sizeof(szWorkMDI), "%d", nShort);
                m_oGTiffMDMD.SetMetadataItem(asTIFFTags[iTag].pszTagName,
                                             szWorkMDI);
            }
        }
        else if (asTIFFTags[iTag].eType == GTIFFTAGTYPE_BYTE_STRING)
        {
            uint32_t nCount = 0;
            if (TIFFGetField(m_hTIFF, asTIFFTags[iTag].nTagVal, &nCount,
                             &pszText))
            {
                std::string osStr;
                osStr.assign(pszText, nCount);
                m_oGTiffMDMD.SetMetadataItem(asTIFFTags[iTag].pszTagName,
                                             osStr.c_str());
            }
        }
    }

    if (TIFFGetField(m_hTIFF, TIFFTAG_RESOLUTIONUNIT, &nShort))
    {
        if (nShort == RESUNIT_NONE)
            snprintf(szWorkMDI, sizeof(szWorkMDI), "%d (unitless)", nShort);
        else if (nShort == RESUNIT_INCH)
            snprintf(szWorkMDI, sizeof(szWorkMDI), "%d (pixels/inch)", nShort);
        else if (nShort == RESUNIT_CENTIMETER)
            snprintf(szWorkMDI, sizeof(szWorkMDI), "%d (pixels/cm)", nShort);
        else
            snprintf(szWorkMDI, sizeof(szWorkMDI), "%d", nShort);
        m_oGTiffMDMD.SetMetadataItem("TIFFTAG_RESOLUTIONUNIT", szWorkMDI);
    }

    int nTagSize = 0;
    void *pData = nullptr;
    if (TIFFGetField(m_hTIFF, TIFFTAG_XMLPACKET, &nTagSize, &pData))
    {
        char *pszXMP = static_cast<char *>(VSI_MALLOC_VERBOSE(nTagSize + 1));
        if (pszXMP)
        {
            memcpy(pszXMP, pData, nTagSize);
            pszXMP[nTagSize] = '\0';

            char *apszMDList[2] = {pszXMP, nullptr};
            m_oGTiffMDMD.SetMetadata(apszMDList, "xml:XMP");

            CPLFree(pszXMP);
        }
    }

    if (m_nCompression != COMPRESSION_NONE)
    {
        const char *pszCompressionMethodName =
            GetCompressionMethodName(m_nCompression);
        if (pszCompressionMethodName)
        {
            m_oGTiffMDMD.SetMetadataItem(
                "COMPRESSION", pszCompressionMethodName, "IMAGE_STRUCTURE");
        }
        else
        {
            CPLString oComp;
            oComp.Printf("%d", m_nCompression);
            m_oGTiffMDMD.SetMetadataItem("COMPRESSION", oComp.c_str());
        }
    }

    if (m_nCompression == COMPRESSION_JPEG &&
        m_nPhotometric == PHOTOMETRIC_YCBCR)
    {
        m_oGTiffMDMD.SetMetadataItem("COMPRESSION", "YCbCr JPEG",
                                     "IMAGE_STRUCTURE");
    }
    else if (m_nCompression == COMPRESSION_LERC)
    {
        uint32_t nLercParamCount = 0;
        uint32_t *panLercParams = nullptr;
        if (TIFFGetField(m_hTIFF, TIFFTAG_LERC_PARAMETERS, &nLercParamCount,
                         &panLercParams) &&
            nLercParamCount == 2)
        {
            memcpy(m_anLercAddCompressionAndVersion, panLercParams,
                   sizeof(m_anLercAddCompressionAndVersion));
        }

        uint32_t nAddVersion = LERC_ADD_COMPRESSION_NONE;
        if (TIFFGetField(m_hTIFF, TIFFTAG_LERC_ADD_COMPRESSION, &nAddVersion) &&
            nAddVersion != LERC_ADD_COMPRESSION_NONE)
        {
            if (nAddVersion == LERC_ADD_COMPRESSION_DEFLATE)
            {
                m_oGTiffMDMD.SetMetadataItem("COMPRESSION", "LERC_DEFLATE",
                                             "IMAGE_STRUCTURE");
            }
            else if (nAddVersion == LERC_ADD_COMPRESSION_ZSTD)
            {
                m_oGTiffMDMD.SetMetadataItem("COMPRESSION", "LERC_ZSTD",
                                             "IMAGE_STRUCTURE");
            }
        }
        uint32_t nLercVersion = LERC_VERSION_2_4;
        if (TIFFGetField(m_hTIFF, TIFFTAG_LERC_VERSION, &nLercVersion))
        {
            if (nLercVersion == LERC_VERSION_2_4)
            {
                m_oGTiffMDMD.SetMetadataItem("LERC_VERSION", "2.4",
                                             "IMAGE_STRUCTURE");
            }
            else
            {
                ReportError(CE_Warning, CPLE_AppDefined,
                            "Unknown Lerc version: %d", nLercVersion);
            }
        }
    }

    if (m_nPlanarConfig == PLANARCONFIG_CONTIG && nBands != 1)
        m_oGTiffMDMD.SetMetadataItem("INTERLEAVE", "PIXEL", "IMAGE_STRUCTURE");
    else
        m_oGTiffMDMD.SetMetadataItem("INTERLEAVE", "BAND", "IMAGE_STRUCTURE");

    if ((GetRasterBand(1)->GetRasterDataType() == GDT_Byte &&
         m_nBitsPerSample != 8) ||
        (GetRasterBand(1)->GetRasterDataType() == GDT_UInt16 &&
         m_nBitsPerSample != 16) ||
        ((GetRasterBand(1)->GetRasterDataType() == GDT_UInt32 ||
          GetRasterBand(1)->GetRasterDataType() == GDT_Float32) &&
         m_nBitsPerSample != 32))
    {
        for (int i = 0; i < nBands; ++i)
            cpl::down_cast<GTiffRasterBand *>(GetRasterBand(i + 1))
                ->m_oGTiffMDMD.SetMetadataItem(
                    "NBITS",
                    CPLString().Printf("%d",
                                       static_cast<int>(m_nBitsPerSample)),
                    "IMAGE_STRUCTURE");
    }

    if (bMinIsWhite)
        m_oGTiffMDMD.SetMetadataItem("MINISWHITE", "YES", "IMAGE_STRUCTURE");

    if (TIFFGetField(m_hTIFF, TIFFTAG_GDAL_METADATA, &pszText))
    {
        CPLXMLNode *psRoot = CPLParseXMLString(pszText);
        const CPLXMLNode *psItem =
            psRoot ? CPLGetXMLNode(psRoot, "=GDALMetadata") : nullptr;
        if (psItem)
            psItem = psItem->psChild;
        for (; psItem != nullptr; psItem = psItem->psNext)
        {

            if (psItem->eType != CXT_Element ||
                !EQUAL(psItem->pszValue, "Item"))
                continue;

            const char *pszKey = CPLGetXMLValue(psItem, "name", nullptr);
            const char *pszValue = CPLGetXMLValue(psItem, nullptr, nullptr);
            int nBand = atoi(CPLGetXMLValue(psItem, "sample", "-1"));
            if (nBand < -1 || nBand > 65535)
                continue;
            nBand++;
            const char *pszRole = CPLGetXMLValue(psItem, "role", "");
            const char *pszDomain = CPLGetXMLValue(psItem, "domain", "");

            if (pszKey == nullptr || pszValue == nullptr)
                continue;
            if (EQUAL(pszDomain, "IMAGE_STRUCTURE"))
            {
                if (m_nCompression == COMPRESSION_WEBP &&
                    EQUAL(pszKey, "COMPRESSION_REVERSIBILITY"))
                {
                    if (EQUAL(pszValue, "LOSSLESS"))
                        m_bWebPLossless = true;
                    else if (EQUAL(pszValue, "LOSSY"))
                        m_bWebPLossless = false;
                }
                else if (m_nCompression == COMPRESSION_WEBP &&
                         EQUAL(pszKey, "WEBP_LEVEL"))
                {
                    const int nLevel = atoi(pszValue);
                    if (nLevel >= 1 && nLevel <= 100)
                    {
                        m_oGTiffMDMD.SetMetadataItem(
                            "COMPRESSION_REVERSIBILITY", "LOSSY",
                            "IMAGE_STRUCTURE");
                        m_bWebPLossless = false;
                        m_nWebPLevel = static_cast<signed char>(nLevel);
                    }
                }
#if HAVE_JXL
                else if (m_nCompression == COMPRESSION_JXL &&
                         EQUAL(pszKey, "COMPRESSION_REVERSIBILITY"))
                {
                    if (EQUAL(pszValue, "LOSSLESS"))
                        m_bJXLLossless = true;
                    else if (EQUAL(pszValue, "LOSSY"))
                        m_bJXLLossless = false;
                }
                else if (m_nCompression == COMPRESSION_JXL &&
                         EQUAL(pszKey, "JXL_DISTANCE"))
                {
                    const double dfVal = CPLAtof(pszValue);
                    if (dfVal > 0 && dfVal <= 15)
                    {
                        m_oGTiffMDMD.SetMetadataItem(
                            "COMPRESSION_REVERSIBILITY", "LOSSY",
                            "IMAGE_STRUCTURE");
                        m_bJXLLossless = false;
                        m_fJXLDistance = static_cast<float>(dfVal);
                    }
                }
                else if (m_nCompression == COMPRESSION_JXL &&
                         EQUAL(pszKey, "JXL_EFFORT"))
                {
                    const int nEffort = atoi(pszValue);
                    if (nEffort >= 1 && nEffort <= 9)
                    {
                        m_nJXLEffort = nEffort;
                    }
                }
#endif
                else
                {
                    continue;
                }
            }

            bool bIsXML = false;

            if (STARTS_WITH_CI(pszDomain, "xml:"))
                bIsXML = TRUE;

            // Note: this un-escaping should not normally be done, as the
            // deserialization of the tree from XML also does it, so we end up
            // width double XML escaping, but keep it for backward
            // compatibility.
            char *pszUnescapedValue =
                CPLUnescapeString(pszValue, nullptr, CPLES_XML);
            if (nBand == 0)
            {
                if (bIsXML)
                {
                    char *apszMD[2] = {pszUnescapedValue, nullptr};
                    m_oGTiffMDMD.SetMetadata(apszMD, pszDomain);
                }
                else
                {
                    m_oGTiffMDMD.SetMetadataItem(pszKey, pszUnescapedValue,
                                                 pszDomain);
                }
            }
            else
            {
                GTiffRasterBand *poBand =
                    cpl::down_cast<GTiffRasterBand *>(GetRasterBand(nBand));
                if (poBand != nullptr)
                {
                    if (EQUAL(pszRole, "scale"))
                    {
                        poBand->m_bHaveOffsetScale = true;
                        poBand->m_dfScale = CPLAtofM(pszUnescapedValue);
                    }
                    else if (EQUAL(pszRole, "offset"))
                    {
                        poBand->m_bHaveOffsetScale = true;
                        poBand->m_dfOffset = CPLAtofM(pszUnescapedValue);
                    }
                    else if (EQUAL(pszRole, "unittype"))
                    {
                        poBand->m_osUnitType = pszUnescapedValue;
                    }
                    else if (EQUAL(pszRole, "description"))
                    {
                        poBand->m_osDescription = pszUnescapedValue;
                    }
                    else if (EQUAL(pszRole, "colorinterp"))
                    {
                        poBand->m_eBandInterp =
                            GDALGetColorInterpretationByName(pszUnescapedValue);
                    }
                    else
                    {
                        if (bIsXML)
                        {
                            char *apszMD[2] = {pszUnescapedValue, nullptr};
                            poBand->m_oGTiffMDMD.SetMetadata(apszMD, pszDomain);
                        }
                        else
                        {
                            poBand->m_oGTiffMDMD.SetMetadataItem(
                                pszKey, pszUnescapedValue, pszDomain);
                        }
                    }
                }
            }
            CPLFree(pszUnescapedValue);
        }

        CPLDestroyXMLNode(psRoot);
    }

    if (m_bStreamingIn)
    {
        toff_t *panOffsets = nullptr;
        TIFFGetField(m_hTIFF,
                     TIFFIsTiled(m_hTIFF) ? TIFFTAG_TILEOFFSETS
                                          : TIFFTAG_STRIPOFFSETS,
                     &panOffsets);
        if (panOffsets)
        {
            int nBlockCount = TIFFIsTiled(m_hTIFF)
                                  ? TIFFNumberOfTiles(m_hTIFF)
                                  : TIFFNumberOfStrips(m_hTIFF);
            for (int i = 1; i < nBlockCount; ++i)
            {
                if (panOffsets[i] < panOffsets[i - 1])
                {
                    m_oGTiffMDMD.SetMetadataItem("UNORDERED_BLOCKS", "YES",
                                                 "TIFF");
                    CPLDebug("GTIFF",
                             "Offset of block %d is lower than previous block. "
                             "Reader must be careful",
                             i);
                    break;
                }
            }
        }
    }

    if (m_nCompression == COMPRESSION_JPEG)
    {
        bool bHasQuantizationTable = false;
        bool bHasHuffmanTable = false;
        int nQuality =
            GuessJPEGQuality(bHasQuantizationTable, bHasHuffmanTable);
        if (nQuality > 0)
        {
            m_oGTiffMDMD.SetMetadataItem(
                "JPEG_QUALITY", CPLSPrintf("%d", nQuality), "IMAGE_STRUCTURE");
            int nJpegTablesMode = JPEGTABLESMODE_QUANT;
            if (bHasHuffmanTable)
            {
                nJpegTablesMode |= JPEGTABLESMODE_HUFF;
            }
            m_oGTiffMDMD.SetMetadataItem("JPEGTABLESMODE",
                                         CPLSPrintf("%d", nJpegTablesMode),
                                         "IMAGE_STRUCTURE");
        }
        if (eAccess == GA_Update)
        {
            SetJPEGQualityAndTablesModeFromFile(nQuality, bHasQuantizationTable,
                                                bHasHuffmanTable);
        }
    }
    else if (eAccess == GA_Update &&
             m_oGTiffMDMD.GetMetadataItem("COMPRESSION_REVERSIBILITY",
                                          "IMAGE_STRUCTURE") == nullptr)
    {
        if (m_nCompression == COMPRESSION_WEBP)
        {
            const char *pszReversibility =
                GetMetadataItem("COMPRESSION_REVERSIBILITY", "IMAGE_STRUCTURE");
            if (pszReversibility && strstr(pszReversibility, "LOSSLESS"))
            {
                m_bWebPLossless = true;
            }
            else if (pszReversibility && strstr(pszReversibility, "LOSSY"))
            {
                m_bWebPLossless = false;
            }
        }
#ifdef HAVE_JXL
        else if (m_nCompression == COMPRESSION_JXL)
        {
            const char *pszReversibility =
                GetMetadataItem("COMPRESSION_REVERSIBILITY", "IMAGE_STRUCTURE");
            if (pszReversibility && strstr(pszReversibility, "LOSSLESS"))
            {
                m_bJXLLossless = true;
            }
            else if (pszReversibility && strstr(pszReversibility, "LOSSY"))
            {
                m_bJXLLossless = false;
            }
        }
#endif
    }

    if (GTIFFSupportsPredictor(m_nCompression))
    {
        uint16_t nPredictor = 0;
        if (TIFFGetField(m_hTIFF, TIFFTAG_PREDICTOR, &nPredictor) &&
            nPredictor > 1)
        {
            m_oGTiffMDMD.SetMetadataItem(
                "PREDICTOR", CPLSPrintf("%d", nPredictor), "IMAGE_STRUCTURE");
        }
    }

    CPLAssert(m_bReadGeoTransform == bReadGeoTransform);
    CPLAssert(!m_bMetadataChanged);
    m_bMetadataChanged = false;

    return CE_None;
}

/************************************************************************/
/*                         GetSiblingFiles()                            */
/************************************************************************/

char **GTiffDataset::GetSiblingFiles()
{
    if (m_bHasGotSiblingFiles)
    {
        return oOvManager.GetSiblingFiles();
    }

    m_bHasGotSiblingFiles = true;
    const int nMaxFiles =
        atoi(CPLGetConfigOption("GDAL_READDIR_LIMIT_ON_OPEN", "1000"));
    char **papszSiblingFiles =
        VSIReadDirEx(CPLGetDirname(m_pszFilename), nMaxFiles);
    if (nMaxFiles > 0 && CSLCount(papszSiblingFiles) > nMaxFiles)
    {
        CPLDebug("GTiff", "GDAL_READDIR_LIMIT_ON_OPEN reached on %s",
                 CPLGetDirname(m_pszFilename));
        CSLDestroy(papszSiblingFiles);
        papszSiblingFiles = nullptr;
    }
    oOvManager.TransferSiblingFiles(papszSiblingFiles);

    return papszSiblingFiles;
}

/************************************************************************/
/*                   IdentifyAuthorizedGeoreferencingSources()          */
/************************************************************************/

void GTiffDataset::IdentifyAuthorizedGeoreferencingSources()
{
    if (m_bHasIdentifiedAuthorizedGeoreferencingSources)
        return;
    m_bHasIdentifiedAuthorizedGeoreferencingSources = true;
    CPLString osGeorefSources = CSLFetchNameValueDef(
        papszOpenOptions, "GEOREF_SOURCES",
        CPLGetConfigOption("GDAL_GEOREF_SOURCES",
                           "PAM,INTERNAL,TABFILE,WORLDFILE"));
    char **papszTokens = CSLTokenizeString2(osGeorefSources, ",", 0);
    m_nPAMGeorefSrcIndex =
        static_cast<signed char>(CSLFindString(papszTokens, "PAM"));
    m_nINTERNALGeorefSrcIndex =
        static_cast<signed char>(CSLFindString(papszTokens, "INTERNAL"));
    m_nTABFILEGeorefSrcIndex =
        static_cast<signed char>(CSLFindString(papszTokens, "TABFILE"));
    m_nWORLDFILEGeorefSrcIndex =
        static_cast<signed char>(CSLFindString(papszTokens, "WORLDFILE"));
    CSLDestroy(papszTokens);
}

/************************************************************************/
/*                     LoadGeoreferencingAndPamIfNeeded()               */
/************************************************************************/

void GTiffDataset::LoadGeoreferencingAndPamIfNeeded()

{
    if (!m_bReadGeoTransform && !m_bLoadPam)
        return;

    IdentifyAuthorizedGeoreferencingSources();

    /* -------------------------------------------------------------------- */
    /*      Get the transform or gcps from the GeoTIFF file.                */
    /* -------------------------------------------------------------------- */
    if (m_bReadGeoTransform)
    {
        m_bReadGeoTransform = false;

        char *pszTabWKT = nullptr;
        double *padfTiePoints = nullptr;
        double *padfScale = nullptr;
        double *padfMatrix = nullptr;
        uint16_t nCount = 0;
        bool bPixelIsPoint = false;
        unsigned short nRasterType = 0;
        bool bPointGeoIgnore = false;

        std::set<signed char> aoSetPriorities;
        if (m_nINTERNALGeorefSrcIndex >= 0)
            aoSetPriorities.insert(m_nINTERNALGeorefSrcIndex);
        if (m_nTABFILEGeorefSrcIndex >= 0)
            aoSetPriorities.insert(m_nTABFILEGeorefSrcIndex);
        if (m_nWORLDFILEGeorefSrcIndex >= 0)
            aoSetPriorities.insert(m_nWORLDFILEGeorefSrcIndex);
        for (const auto nIndex : aoSetPriorities)
        {
            if (m_nINTERNALGeorefSrcIndex == nIndex)
            {
                GTIF *psGTIF =
                    GTiffDatasetGTIFNew(m_hTIFF);  // How expensive this is?

                if (psGTIF)
                {
                    if (GDALGTIFKeyGetSHORT(psGTIF, GTRasterTypeGeoKey,
                                            &nRasterType, 0, 1) == 1 &&
                        nRasterType == static_cast<short>(RasterPixelIsPoint))
                    {
                        bPixelIsPoint = true;
                        bPointGeoIgnore = CPLTestBool(CPLGetConfigOption(
                            "GTIFF_POINT_GEO_IGNORE", "FALSE"));
                    }

                    GTIFFree(psGTIF);
                }

                m_adfGeoTransform[0] = 0.0;
                m_adfGeoTransform[1] = 1.0;
                m_adfGeoTransform[2] = 0.0;
                m_adfGeoTransform[3] = 0.0;
                m_adfGeoTransform[4] = 0.0;
                m_adfGeoTransform[5] = 1.0;

                uint16_t nCountScale = 0;
                if (TIFFGetField(m_hTIFF, TIFFTAG_GEOPIXELSCALE, &nCountScale,
                                 &padfScale) &&
                    nCountScale >= 2 && padfScale[0] != 0.0 &&
                    padfScale[1] != 0.0)
                {
                    m_adfGeoTransform[1] = padfScale[0];
                    if (padfScale[1] < 0)
                    {
                        const char *pszOptionVal = CPLGetConfigOption(
                            "GTIFF_HONOUR_NEGATIVE_SCALEY", nullptr);
                        if (pszOptionVal == nullptr)
                        {
                            ReportError(
                                CE_Warning, CPLE_AppDefined,
                                "File with negative value for ScaleY in "
                                "GeoPixelScale tag. This is rather "
                                "unusual. GDAL, contrary to the GeoTIFF "
                                "specification, assumes that the file "
                                "was intended to be north-up, and will "
                                "treat this file as if ScaleY was "
                                "positive. You may override this behavior "
                                "by setting the GTIFF_HONOUR_NEGATIVE_SCALEY "
                                "configuration option to YES");
                            m_adfGeoTransform[5] = padfScale[1];
                        }
                        else if (CPLTestBool(pszOptionVal))
                        {
                            m_adfGeoTransform[5] = -padfScale[1];
                        }
                        else
                        {
                            m_adfGeoTransform[5] = padfScale[1];
                        }
                    }
                    else
                    {
                        m_adfGeoTransform[5] = -padfScale[1];
                    }

                    if (TIFFGetField(m_hTIFF, TIFFTAG_GEOTIEPOINTS, &nCount,
                                     &padfTiePoints) &&
                        nCount >= 6)
                    {
                        m_adfGeoTransform[0] =
                            padfTiePoints[3] -
                            padfTiePoints[0] * m_adfGeoTransform[1];
                        m_adfGeoTransform[3] =
                            padfTiePoints[4] -
                            padfTiePoints[1] * m_adfGeoTransform[5];

                        if (bPixelIsPoint && !bPointGeoIgnore)
                        {
                            m_adfGeoTransform[0] -=
                                (m_adfGeoTransform[1] * 0.5 +
                                 m_adfGeoTransform[2] * 0.5);
                            m_adfGeoTransform[3] -=
                                (m_adfGeoTransform[4] * 0.5 +
                                 m_adfGeoTransform[5] * 0.5);
                        }

                        m_bGeoTransformValid = true;
                        m_nGeoTransformGeorefSrcIndex = nIndex;

                        if (nCountScale >= 3 && GetRasterCount() == 1 &&
                            (padfScale[2] != 0.0 || padfTiePoints[2] != 0.0 ||
                             padfTiePoints[5] != 0.0))
                        {
                            LookForProjection();
                            if (!m_oSRS.IsEmpty() && m_oSRS.IsVertical())
                            {
                                /* modelTiePointTag = (pixel, line, z0, X, Y,
                                 * Z0) */
                                /* thus Z(some_point) = (z(some_point) - z0) *
                                 * scaleZ + Z0 */
                                /* equivalently written as */
                                /* Z(some_point) = z(some_point) * scaleZ +
                                 * offsetZ with */
                                /* offsetZ = - z0 * scaleZ + Z0 */
                                double dfScale = padfScale[2];
                                double dfOffset = -padfTiePoints[2] * dfScale +
                                                  padfTiePoints[5];
                                GTiffRasterBand *poBand =
                                    cpl::down_cast<GTiffRasterBand *>(
                                        GetRasterBand(1));
                                poBand->m_bHaveOffsetScale = true;
                                poBand->m_dfScale = dfScale;
                                poBand->m_dfOffset = dfOffset;
                            }
                        }
                    }
                }

                else if (TIFFGetField(m_hTIFF, TIFFTAG_GEOTRANSMATRIX, &nCount,
                                      &padfMatrix) &&
                         nCount == 16)
                {
                    m_adfGeoTransform[0] = padfMatrix[3];
                    m_adfGeoTransform[1] = padfMatrix[0];
                    m_adfGeoTransform[2] = padfMatrix[1];
                    m_adfGeoTransform[3] = padfMatrix[7];
                    m_adfGeoTransform[4] = padfMatrix[4];
                    m_adfGeoTransform[5] = padfMatrix[5];

                    if (bPixelIsPoint && !bPointGeoIgnore)
                    {
                        m_adfGeoTransform[0] -= m_adfGeoTransform[1] * 0.5 +
                                                m_adfGeoTransform[2] * 0.5;
                        m_adfGeoTransform[3] -= m_adfGeoTransform[4] * 0.5 +
                                                m_adfGeoTransform[5] * 0.5;
                    }

                    m_bGeoTransformValid = true;
                    m_nGeoTransformGeorefSrcIndex = nIndex;
                }
                if (m_bGeoTransformValid)
                    break;
            }

            /* --------------------------------------------------------------------
             */
            /*      Otherwise try looking for a .tab, .tfw, .tifw or .wld file.
             */
            /* --------------------------------------------------------------------
             */
            if (m_nTABFILEGeorefSrcIndex == nIndex)
            {
                char *pszGeorefFilename = nullptr;

                char **papszSiblingFiles = GetSiblingFiles();

                // Begin with .tab since it can also have projection info.
                const int bTabFileOK = GDALReadTabFile2(
                    m_pszFilename, m_adfGeoTransform, &pszTabWKT, &m_nGCPCount,
                    &m_pasGCPList, papszSiblingFiles, &pszGeorefFilename);

                if (bTabFileOK)
                {
                    m_nGeoTransformGeorefSrcIndex = nIndex;
                    // if( pszTabWKT )
                    // {
                    //     m_nProjectionGeorefSrcIndex = nIndex;
                    // }
                    if (m_nGCPCount == 0)
                    {
                        m_bGeoTransformValid = true;
                    }
                }

                if (pszGeorefFilename)
                {
                    CPLFree(m_pszGeorefFilename);
                    m_pszGeorefFilename = pszGeorefFilename;
                    pszGeorefFilename = nullptr;
                }
                if (m_bGeoTransformValid)
                    break;
            }

            if (m_nWORLDFILEGeorefSrcIndex == nIndex)
            {
                char *pszGeorefFilename = nullptr;

                char **papszSiblingFiles = GetSiblingFiles();

                m_bGeoTransformValid = CPL_TO_BOOL(GDALReadWorldFile2(
                    m_pszFilename, nullptr, m_adfGeoTransform,
                    papszSiblingFiles, &pszGeorefFilename));

                if (!m_bGeoTransformValid)
                {
                    m_bGeoTransformValid = CPL_TO_BOOL(GDALReadWorldFile2(
                        m_pszFilename, "wld", m_adfGeoTransform,
                        papszSiblingFiles, &pszGeorefFilename));
                }
                if (m_bGeoTransformValid)
                    m_nGeoTransformGeorefSrcIndex = nIndex;

                if (pszGeorefFilename)
                {
                    CPLFree(m_pszGeorefFilename);
                    m_pszGeorefFilename = pszGeorefFilename;
                    pszGeorefFilename = nullptr;
                }
                if (m_bGeoTransformValid)
                    break;
            }
        }

        /* --------------------------------------------------------------------
         */
        /*      Check for GCPs. */
        /* --------------------------------------------------------------------
         */
        if (m_nINTERNALGeorefSrcIndex >= 0 &&
            TIFFGetField(m_hTIFF, TIFFTAG_GEOTIEPOINTS, &nCount,
                         &padfTiePoints) &&
            !m_bGeoTransformValid)
        {
            if (m_nGCPCount > 0)
            {
                GDALDeinitGCPs(m_nGCPCount, m_pasGCPList);
                CPLFree(m_pasGCPList);
            }
            m_nGCPCount = nCount / 6;
            m_pasGCPList = static_cast<GDAL_GCP *>(
                CPLCalloc(sizeof(GDAL_GCP), m_nGCPCount));

            for (int iGCP = 0; iGCP < m_nGCPCount; ++iGCP)
            {
                char szID[32] = {};

                snprintf(szID, sizeof(szID), "%d", iGCP + 1);
                m_pasGCPList[iGCP].pszId = CPLStrdup(szID);
                m_pasGCPList[iGCP].pszInfo = CPLStrdup("");
                m_pasGCPList[iGCP].dfGCPPixel = padfTiePoints[iGCP * 6 + 0];
                m_pasGCPList[iGCP].dfGCPLine = padfTiePoints[iGCP * 6 + 1];
                m_pasGCPList[iGCP].dfGCPX = padfTiePoints[iGCP * 6 + 3];
                m_pasGCPList[iGCP].dfGCPY = padfTiePoints[iGCP * 6 + 4];
                m_pasGCPList[iGCP].dfGCPZ = padfTiePoints[iGCP * 6 + 5];

                if (bPixelIsPoint && !bPointGeoIgnore)
                {
                    m_pasGCPList[iGCP].dfGCPPixel += 0.5;
                    m_pasGCPList[iGCP].dfGCPLine += 0.5;
                }
            }
            m_nGeoTransformGeorefSrcIndex = m_nINTERNALGeorefSrcIndex;
        }

        /* --------------------------------------------------------------------
         */
        /*      Did we find a tab file?  If so we will use its coordinate */
        /*      system and give it precedence. */
        /* --------------------------------------------------------------------
         */
        if (pszTabWKT != nullptr && m_oSRS.IsEmpty())
        {
            m_oSRS.importFromWkt(pszTabWKT);
            m_bLookedForProjection = true;
        }

        CPLFree(pszTabWKT);
    }

    if (m_bLoadPam && m_nPAMGeorefSrcIndex >= 0)
    {
        /* --------------------------------------------------------------------
         */
        /*      Initialize any PAM information. */
        /* --------------------------------------------------------------------
         */
        CPLAssert(!m_bColorProfileMetadataChanged);
        CPLAssert(!m_bMetadataChanged);
        CPLAssert(!m_bGeoTIFFInfoChanged);
        CPLAssert(!m_bNoDataChanged);

        // We must absolutely unset m_bLoadPam now, otherwise calling
        // GetFileList() on a .tif with a .aux will result in an (almost)
        // endless sequence of calls.
        m_bLoadPam = false;

        TryLoadXML(GetSiblingFiles());
        ApplyPamInfo();

        m_bColorProfileMetadataChanged = false;
        m_bMetadataChanged = false;
        m_bGeoTIFFInfoChanged = false;
        m_bNoDataChanged = false;

        for (int i = 1; i <= nBands; ++i)
        {
            GTiffRasterBand *poBand =
                cpl::down_cast<GTiffRasterBand *>(GetRasterBand(i));

            /* Load scale, offset and unittype from PAM if available */
            if (!poBand->m_bHaveOffsetScale)
            {
                int nHaveOffsetScale = FALSE;
                poBand->m_dfScale =
                    poBand->GDALPamRasterBand::GetScale(&nHaveOffsetScale);
                poBand->m_bHaveOffsetScale = CPL_TO_BOOL(nHaveOffsetScale);
                poBand->m_dfOffset = poBand->GDALPamRasterBand::GetOffset();
            }
            if (poBand->m_osUnitType.empty())
            {
                const char *pszUnitType =
                    poBand->GDALPamRasterBand::GetUnitType();
                if (pszUnitType)
                    poBand->m_osUnitType = pszUnitType;
            }
            if (poBand->m_osDescription.empty())
                poBand->m_osDescription =
                    poBand->GDALPamRasterBand::GetDescription();

            GDALColorInterp ePAMColorInterp =
                poBand->GDALPamRasterBand::GetColorInterpretation();
            if (ePAMColorInterp != GCI_Undefined)
                poBand->m_eBandInterp = ePAMColorInterp;

            if (i == 1)
            {
                auto poCT = poBand->GDALPamRasterBand::GetColorTable();
                if (poCT)
                {
                    delete m_poColorTable;
                    m_poColorTable = poCT->Clone();
                }
            }
        }
    }
    m_bLoadPam = false;
}

/************************************************************************/
/*                   SetStructuralMDFromParent()                        */
/************************************************************************/

void GTiffDataset::SetStructuralMDFromParent(GTiffDataset *poParentDS)
{
    m_bBlockOrderRowMajor = poParentDS->m_bBlockOrderRowMajor;
    m_bLeaderSizeAsUInt4 = poParentDS->m_bLeaderSizeAsUInt4;
    m_bTrailerRepeatedLast4BytesRepeated =
        poParentDS->m_bTrailerRepeatedLast4BytesRepeated;
    m_bMaskInterleavedWithImagery = poParentDS->m_bMaskInterleavedWithImagery;
    m_bWriteEmptyTiles = poParentDS->m_bWriteEmptyTiles;
}

/************************************************************************/
/*                          ScanDirectories()                           */
/*                                                                      */
/*      Scan through all the directories finding overviews, masks       */
/*      and subdatasets.                                                */
/************************************************************************/

void GTiffDataset::ScanDirectories()

{
    /* -------------------------------------------------------------------- */
    /*      We only scan once.  We do not scan for non-base datasets.       */
    /* -------------------------------------------------------------------- */
    if (!m_bScanDeferred)
        return;

    m_bScanDeferred = false;

    if (m_poBaseDS)
        return;

    Crystalize();

    CPLDebug("GTiff", "ScanDirectories()");

    /* ==================================================================== */
    /*      Scan all directories.                                           */
    /* ==================================================================== */
    CPLStringList aosSubdatasets;
    int iDirIndex = 0;

    FlushDirectory();

    do
    {
        // Only libtiff 4.0.4 can handle between 32768 and 65535 directories.
#if !defined(SUPPORTS_MORE_THAN_32768_DIRECTORIES)
        if (iDirIndex == 32768)
            break;
#endif
        toff_t nTopDir = TIFFCurrentDirOffset(m_hTIFF);
        uint32_t nSubType = 0;

        ++iDirIndex;

        toff_t *tmpSubIFDOffsets = nullptr;
        toff_t *subIFDOffsets = nullptr;
        uint16_t nSubIFDs = 0;
        if (TIFFGetField(m_hTIFF, TIFFTAG_SUBIFD, &nSubIFDs,
                         &tmpSubIFDOffsets) &&
            iDirIndex == 1)
        {
            subIFDOffsets =
                static_cast<toff_t *>(CPLMalloc(nSubIFDs * sizeof(toff_t)));
            for (uint16_t iSubIFD = 0; iSubIFD < nSubIFDs; iSubIFD++)
            {
                subIFDOffsets[iSubIFD] = tmpSubIFDOffsets[iSubIFD];
            }
        }

        // early break for backwards compatibility: if the first directory read
        // is also the last, and there are no subIFDs, no use continuing
        if (iDirIndex == 1 && nSubIFDs == 0 && TIFFLastDirectory(m_hTIFF))
        {
            CPLFree(subIFDOffsets);
            break;
        }

        for (uint16_t iSubIFD = 0; iSubIFD <= nSubIFDs; iSubIFD++)
        {
            toff_t nThisDir = nTopDir;
            if (iSubIFD > 0 && iDirIndex > 1)  // don't read subIFDs if we are
                                               // not in the original directory
                break;
            if (iSubIFD > 0)
            {
                // make static analyzer happy. subIFDOffsets cannot be null if
                // iSubIFD>0
                assert(subIFDOffsets != nullptr);
                nThisDir = subIFDOffsets[iSubIFD - 1];
                // CPLDebug("GTiff", "Opened subIFD %d/%d at offset %llu.",
                // iSubIFD, nSubIFDs, nThisDir);
                if (!TIFFSetSubDirectory(m_hTIFF, nThisDir))
                    break;
            }

            if (!TIFFGetField(m_hTIFF, TIFFTAG_SUBFILETYPE, &nSubType))
                nSubType = 0;

            /* Embedded overview of the main image */
            if ((nSubType & FILETYPE_REDUCEDIMAGE) != 0 &&
                (nSubType & FILETYPE_MASK) == 0 &&
                ((nSubIFDs == 0 && iDirIndex != 1) || iSubIFD > 0) &&
                m_nOverviewCount < 30 /* to avoid DoS */)
            {
                GTiffDataset *poODS = new GTiffDataset();
                poODS->ShareLockWithParentDataset(this);
                poODS->SetStructuralMDFromParent(this);
                poODS->m_pszFilename = CPLStrdup(m_pszFilename);
                if (poODS->OpenOffset(VSI_TIFFOpenChild(m_hTIFF), nThisDir,
                                      eAccess) != CE_None ||
                    poODS->GetRasterCount() != GetRasterCount())
                {
                    delete poODS;
                }
                else
                {
                    CPLDebug("GTiff", "Opened %dx%d overview.",
                             poODS->GetRasterXSize(), poODS->GetRasterYSize());
                    ++m_nOverviewCount;
                    m_papoOverviewDS = static_cast<GTiffDataset **>(CPLRealloc(
                        m_papoOverviewDS, m_nOverviewCount * (sizeof(void *))));
                    m_papoOverviewDS[m_nOverviewCount - 1] = poODS;
                    poODS->m_poBaseDS = this;
                    poODS->m_bIsOverview = true;
                }
            }
            // Embedded mask of the main image.
            else if ((nSubType & FILETYPE_MASK) != 0 &&
                     (nSubType & FILETYPE_REDUCEDIMAGE) == 0 &&
                     ((nSubIFDs == 0 && iDirIndex != 1) || iSubIFD > 0) &&
                     m_poMaskDS == nullptr)
            {
                m_poMaskDS = new GTiffDataset();
                m_poMaskDS->ShareLockWithParentDataset(this);
                m_poMaskDS->SetStructuralMDFromParent(this);
                m_poMaskDS->m_pszFilename = CPLStrdup(m_pszFilename);

                // The TIFF6 specification - page 37 - only allows 1
                // SamplesPerPixel and 1 BitsPerSample Here we support either 1
                // or 8 bit per sample and we support either 1 sample per pixel
                // or as many samples as in the main image We don't check the
                // value of the PhotometricInterpretation tag, which should be
                // set to "Transparency mask" (4) according to the specification
                // (page 36).  However, the TIFF6 specification allows image
                // masks to have a higher resolution than the main image, what
                // we don't support here.

                if (m_poMaskDS->OpenOffset(VSI_TIFFOpenChild(m_hTIFF), nThisDir,
                                           eAccess) != CE_None ||
                    m_poMaskDS->GetRasterCount() == 0 ||
                    !(m_poMaskDS->GetRasterCount() == 1 ||
                      m_poMaskDS->GetRasterCount() == GetRasterCount()) ||
                    m_poMaskDS->GetRasterXSize() != GetRasterXSize() ||
                    m_poMaskDS->GetRasterYSize() != GetRasterYSize() ||
                    m_poMaskDS->GetRasterBand(1)->GetRasterDataType() !=
                        GDT_Byte)
                {
                    delete m_poMaskDS;
                    m_poMaskDS = nullptr;
                }
                else
                {
                    CPLDebug("GTiff", "Opened band mask.");
                    m_poMaskDS->m_poBaseDS = this;
                    m_poMaskDS->m_poImageryDS = this;

                    m_poMaskDS->m_bPromoteTo8Bits =
                        CPLTestBool(CPLGetConfigOption(
                            "GDAL_TIFF_INTERNAL_MASK_TO_8BIT", "YES"));
                }
            }

            // Embedded mask of an overview.  The TIFF6 specification allows the
            // combination of the FILETYPE_xxxx masks.
            else if ((nSubType & FILETYPE_REDUCEDIMAGE) != 0 &&
                     (nSubType & FILETYPE_MASK) != 0 &&
                     ((nSubIFDs == 0 && iDirIndex != 1) || iSubIFD > 0))
            {
                GTiffDataset *poDS = new GTiffDataset();
                poDS->ShareLockWithParentDataset(this);
                poDS->SetStructuralMDFromParent(this);
                poDS->m_pszFilename = CPLStrdup(m_pszFilename);
                if (poDS->OpenOffset(VSI_TIFFOpenChild(m_hTIFF), nThisDir,
                                     eAccess) != CE_None ||
                    poDS->GetRasterCount() == 0 ||
                    poDS->GetRasterBand(1)->GetRasterDataType() != GDT_Byte)
                {
                    delete poDS;
                }
                else
                {
                    int i = 0;  // Used after for.
                    for (; i < m_nOverviewCount; ++i)
                    {
                        auto poOvrDS = cpl::down_cast<GTiffDataset *>(
                            GDALDataset::FromHandle(m_papoOverviewDS[i]));
                        if (poOvrDS->m_poMaskDS == nullptr &&
                            poDS->GetRasterXSize() ==
                                m_papoOverviewDS[i]->GetRasterXSize() &&
                            poDS->GetRasterYSize() ==
                                m_papoOverviewDS[i]->GetRasterYSize() &&
                            (poDS->GetRasterCount() == 1 ||
                             poDS->GetRasterCount() == GetRasterCount()))
                        {
                            CPLDebug(
                                "GTiff", "Opened band mask for %dx%d overview.",
                                poDS->GetRasterXSize(), poDS->GetRasterYSize());
                            poDS->m_poImageryDS = poOvrDS;
                            poOvrDS->m_poMaskDS = poDS;
                            poDS->m_bPromoteTo8Bits =
                                CPLTestBool(CPLGetConfigOption(
                                    "GDAL_TIFF_INTERNAL_MASK_TO_8BIT", "YES"));
                            poDS->m_poBaseDS = this;
                            break;
                        }
                    }
                    if (i == m_nOverviewCount)
                    {
                        delete poDS;
                    }
                }
            }
            else if (!m_bSingleIFDOpened &&
                     (nSubType == 0 || nSubType == FILETYPE_PAGE))
            {
                uint32_t nXSize = 0;
                uint32_t nYSize = 0;

                TIFFGetField(m_hTIFF, TIFFTAG_IMAGEWIDTH, &nXSize);
                TIFFGetField(m_hTIFF, TIFFTAG_IMAGELENGTH, &nYSize);

                // For Geodetic TIFF grids (GTG)
                // (https://proj.org/specifications/geodetictiffgrids.html)
                // extract the grid_name to put it in the description
                std::string osFriendlyName;
                char *pszText = nullptr;
                if (TIFFGetField(m_hTIFF, TIFFTAG_GDAL_METADATA, &pszText) &&
                    strstr(pszText, "grid_name") != nullptr)
                {
                    CPLXMLNode *psRoot = CPLParseXMLString(pszText);
                    const CPLXMLNode *psItem =
                        psRoot ? CPLGetXMLNode(psRoot, "=GDALMetadata")
                               : nullptr;
                    if (psItem)
                        psItem = psItem->psChild;
                    for (; psItem != nullptr; psItem = psItem->psNext)
                    {

                        if (psItem->eType != CXT_Element ||
                            !EQUAL(psItem->pszValue, "Item"))
                            continue;

                        const char *pszKey =
                            CPLGetXMLValue(psItem, "name", nullptr);
                        const char *pszValue =
                            CPLGetXMLValue(psItem, nullptr, nullptr);
                        int nBand =
                            atoi(CPLGetXMLValue(psItem, "sample", "-1"));
                        if (pszKey && pszValue && nBand <= 0 &&
                            EQUAL(pszKey, "grid_name"))
                        {
                            osFriendlyName = ": ";
                            osFriendlyName += pszValue;
                            break;
                        }
                    }

                    CPLDestroyXMLNode(psRoot);
                }

                if (nXSize > INT_MAX || nYSize > INT_MAX)
                {
                    CPLDebug("GTiff",
                             "Skipping directory with too large image: %u x %u",
                             nXSize, nYSize);
                }
                else
                {
                    uint16_t nSPP = 0;
                    if (!TIFFGetField(m_hTIFF, TIFFTAG_SAMPLESPERPIXEL, &nSPP))
                        nSPP = 1;

                    CPLString osName, osDesc;
                    osName.Printf("SUBDATASET_%d_NAME=GTIFF_DIR:%d:%s",
                                  iDirIndex, iDirIndex, m_pszFilename);
                    osDesc.Printf(
                        "SUBDATASET_%d_DESC=Page %d (%dP x %dL x %dB)",
                        iDirIndex, iDirIndex, static_cast<int>(nXSize),
                        static_cast<int>(nYSize), nSPP);
                    osDesc += osFriendlyName;

                    aosSubdatasets.AddString(osName);
                    aosSubdatasets.AddString(osDesc);
                }
            }
        }
        CPLFree(subIFDOffsets);

        // Make sure we are stepping from the expected directory regardless
        // of churn done processing the above.
        if (TIFFCurrentDirOffset(m_hTIFF) != nTopDir)
            TIFFSetSubDirectory(m_hTIFF, nTopDir);
    } while (!m_bSingleIFDOpened && !TIFFLastDirectory(m_hTIFF) &&
             TIFFReadDirectory(m_hTIFF) != 0);

    ReloadDirectory();

    // If we have a mask for the main image, loop over the overviews, and if
    // they have a mask, let's set this mask as an overview of the main mask.
    if (m_poMaskDS != nullptr)
    {
        for (int i = 0; i < m_nOverviewCount; ++i)
        {
            if (cpl::down_cast<GTiffDataset *>(
                    GDALDataset::FromHandle(m_papoOverviewDS[i]))
                    ->m_poMaskDS != nullptr)
            {
                ++m_poMaskDS->m_nOverviewCount;
                m_poMaskDS->m_papoOverviewDS =
                    static_cast<GTiffDataset **>(CPLRealloc(
                        m_poMaskDS->m_papoOverviewDS,
                        m_poMaskDS->m_nOverviewCount * (sizeof(void *))));
                m_poMaskDS->m_papoOverviewDS[m_poMaskDS->m_nOverviewCount - 1] =
                    cpl::down_cast<GTiffDataset *>(
                        GDALDataset::FromHandle(m_papoOverviewDS[i]))
                        ->m_poMaskDS;
            }
        }
    }

    // Assign color interpretation from main dataset
    const int l_nBands = GetRasterCount();
    for (int iOvr = 0; iOvr < m_nOverviewCount; ++iOvr)
    {
        for (int i = 1; i <= l_nBands; i++)
        {
            auto poBand = dynamic_cast<GTiffRasterBand *>(
                m_papoOverviewDS[iOvr]->GetRasterBand(i));
            if (poBand)
                poBand->m_eBandInterp =
                    GetRasterBand(i)->GetColorInterpretation();
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Only keep track of subdatasets if we have more than one         */
    /*      subdataset (pair).                                              */
    /* -------------------------------------------------------------------- */
    if (aosSubdatasets.size() > 2)
    {
        m_oGTiffMDMD.SetMetadata(aosSubdatasets.List(), "SUBDATASETS");
    }
}

static signed char GTiffGetLZMAPreset(char **papszOptions)
{
    int nLZMAPreset = -1;
    const char *pszValue = CSLFetchNameValue(papszOptions, "LZMA_PRESET");
    if (pszValue != nullptr)
    {
        nLZMAPreset = atoi(pszValue);
        if (!(nLZMAPreset >= 0 && nLZMAPreset <= 9))
        {
            CPLError(CE_Warning, CPLE_IllegalArg,
                     "LZMA_PRESET=%s value not recognised, ignoring.",
                     pszValue);
            nLZMAPreset = -1;
        }
    }
    return static_cast<signed char>(nLZMAPreset);
}

static signed char GTiffGetZSTDPreset(char **papszOptions)
{
    int nZSTDLevel = -1;
    const char *pszValue = CSLFetchNameValue(papszOptions, "ZSTD_LEVEL");
    if (pszValue != nullptr)
    {
        nZSTDLevel = atoi(pszValue);
        if (!(nZSTDLevel >= 1 && nZSTDLevel <= 22))
        {
            CPLError(CE_Warning, CPLE_IllegalArg,
                     "ZSTD_LEVEL=%s value not recognised, ignoring.", pszValue);
            nZSTDLevel = -1;
        }
    }
    return static_cast<signed char>(nZSTDLevel);
}

static double GTiffGetLERCMaxZError(char **papszOptions)
{
    return CPLAtof(CSLFetchNameValueDef(papszOptions, "MAX_Z_ERROR", "0.0"));
}

static signed char GTiffGetZLevel(char **papszOptions)
{
    int nZLevel = -1;
    const char *pszValue = CSLFetchNameValue(papszOptions, "ZLEVEL");
    if (pszValue != nullptr)
    {
        nZLevel = atoi(pszValue);
#ifdef TIFFTAG_DEFLATE_SUBCODEC
        constexpr int nMaxLevel = 12;
#ifndef LIBDEFLATE_SUPPORT
        if (nZLevel > 9 && nZLevel <= nMaxLevel)
        {
            CPLDebug("GTiff",
                     "ZLEVEL=%d not supported in a non-libdeflate enabled "
                     "libtiff build. Capping to 9",
                     nZLevel);
            nZLevel = 9;
        }
#endif
#else
        constexpr int nMaxLevel = 9;
#endif
        if (nZLevel < 1 || nZLevel > nMaxLevel)
        {
            CPLError(CE_Warning, CPLE_IllegalArg,
                     "ZLEVEL=%s value not recognised, ignoring.", pszValue);
            nZLevel = -1;
        }
    }
    return static_cast<signed char>(nZLevel);
}

static signed char GTiffGetJpegQuality(char **papszOptions)
{
    int nJpegQuality = -1;
    const char *pszValue = CSLFetchNameValue(papszOptions, "JPEG_QUALITY");
    if (pszValue != nullptr)
    {
        nJpegQuality = atoi(pszValue);
        if (nJpegQuality < 1 || nJpegQuality > 100)
        {
            CPLError(CE_Warning, CPLE_IllegalArg,
                     "JPEG_QUALITY=%s value not recognised, ignoring.",
                     pszValue);
            nJpegQuality = -1;
        }
    }
    return static_cast<signed char>(nJpegQuality);
}

static signed char GTiffGetJpegTablesMode(char **papszOptions)
{
    return static_cast<signed char>(atoi(
        CSLFetchNameValueDef(papszOptions, "JPEGTABLESMODE",
                             CPLSPrintf("%d", knGTIFFJpegTablesModeDefault))));
}

/************************************************************************/
/*                        GetDiscardLsbOption()                         */
/************************************************************************/

static GTiffDataset::MaskOffset *GetDiscardLsbOption(TIFF *hTIFF,
                                                     char **papszOptions)
{
    const char *pszBits = CSLFetchNameValue(papszOptions, "DISCARD_LSB");
    if (pszBits == nullptr)
        return nullptr;

    uint16_t nPhotometric = 0;
    TIFFGetFieldDefaulted(hTIFF, TIFFTAG_PHOTOMETRIC, &nPhotometric);

    uint16_t nBitsPerSample = 0;
    if (!TIFFGetField(hTIFF, TIFFTAG_BITSPERSAMPLE, &nBitsPerSample))
        nBitsPerSample = 1;

    uint16_t nSamplesPerPixel = 0;
    if (!TIFFGetField(hTIFF, TIFFTAG_SAMPLESPERPIXEL, &nSamplesPerPixel))
        nSamplesPerPixel = 1;

    uint16_t nSampleFormat = 0;
    if (!TIFFGetField(hTIFF, TIFFTAG_SAMPLEFORMAT, &nSampleFormat))
        nSampleFormat = SAMPLEFORMAT_UINT;

    if (nPhotometric == PHOTOMETRIC_PALETTE)
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "DISCARD_LSB ignored on a paletted image");
        return nullptr;
    }
    if (!(nBitsPerSample == 8 || nBitsPerSample == 16 || nBitsPerSample == 32 ||
          nBitsPerSample == 64))
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "DISCARD_LSB ignored on non 8, 16, 32 or 64 bits images");
        return nullptr;
    }

    const CPLStringList aosTokens(CSLTokenizeString2(pszBits, ",", 0));
    const int nTokens = aosTokens.size();
    GTiffDataset::MaskOffset *panMaskOffsetLsb = nullptr;
    if (nTokens == 1 || nTokens == nSamplesPerPixel)
    {
        panMaskOffsetLsb = static_cast<GTiffDataset::MaskOffset *>(
            CPLCalloc(nSamplesPerPixel, sizeof(GTiffDataset::MaskOffset)));
        for (int i = 0; i < nSamplesPerPixel; ++i)
        {
            const int nBits = atoi(aosTokens[nTokens == 1 ? 0 : i]);
            const int nMaxBits =
                (nSampleFormat == SAMPLEFORMAT_IEEEFP && nBits == 32)   ? 23 - 1
                : (nSampleFormat == SAMPLEFORMAT_IEEEFP && nBits == 64) ? 53 - 1
                : nSampleFormat == SAMPLEFORMAT_INT ? nBitsPerSample - 2
                                                    : nBitsPerSample - 1;

            if (nBits < 0 || nBits > nMaxBits)
            {
                CPLError(
                    CE_Warning, CPLE_AppDefined,
                    "DISCARD_LSB ignored: values should be in [0,%d] range",
                    nMaxBits);
                VSIFree(panMaskOffsetLsb);
                return nullptr;
            }
            panMaskOffsetLsb[i].nMask =
                ~((static_cast<uint64_t>(1) << nBits) - 1);
            if (nBits > 1)
            {
                panMaskOffsetLsb[i].nRoundUpBitTest = static_cast<uint64_t>(1)
                                                      << (nBits - 1);
            }
        }
    }
    else
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "DISCARD_LSB ignored: wrong number of components");
    }
    return panMaskOffsetLsb;
}

void GTiffDataset::GetDiscardLsbOption(char **papszOptions)
{
    m_panMaskOffsetLsb = ::GetDiscardLsbOption(m_hTIFF, papszOptions);
}

/************************************************************************/
/*                             GetProfile()                             */
/************************************************************************/

static GTiffProfile GetProfile(const char *pszProfile)
{
    GTiffProfile eProfile = GTiffProfile::GDALGEOTIFF;
    if (pszProfile != nullptr)
    {
        if (EQUAL(pszProfile, szPROFILE_BASELINE))
            eProfile = GTiffProfile::BASELINE;
        else if (EQUAL(pszProfile, szPROFILE_GeoTIFF))
            eProfile = GTiffProfile::GEOTIFF;
        else if (!EQUAL(pszProfile, szPROFILE_GDALGeoTIFF))
        {
            CPLError(CE_Warning, CPLE_NotSupported,
                     "Unsupported value for PROFILE: %s", pszProfile);
        }
    }
    return eProfile;
}

/************************************************************************/
/*                            GTiffCreate()                             */
/*                                                                      */
/*      Shared functionality between GTiffDataset::Create() and         */
/*      GTiffCreateCopy() for creating TIFF file based on a set of      */
/*      options and a configuration.                                    */
/************************************************************************/

TIFF *GTiffDataset::CreateLL(const char *pszFilename, int nXSize, int nYSize,
                             int l_nBands, GDALDataType eType,
                             double dfExtraSpaceForOverviews,
                             char **papszParamList, VSILFILE **pfpL,
                             CPLString &l_osTmpFilename)

{
    if (!GTiffOneTimeInit())
        return nullptr;

    /* -------------------------------------------------------------------- */
    /*      Blow on a few errors.                                           */
    /* -------------------------------------------------------------------- */
    if (nXSize < 1 || nYSize < 1 || l_nBands < 1)
    {
        ReportError(
            pszFilename, CE_Failure, CPLE_AppDefined,
            "Attempt to create %dx%dx%d TIFF file, but width, height and bands"
            "must be positive.",
            nXSize, nYSize, l_nBands);

        return nullptr;
    }

    if (l_nBands > 65535)
    {
        ReportError(pszFilename, CE_Failure, CPLE_AppDefined,
                    "Attempt to create %dx%dx%d TIFF file, but bands "
                    "must be lesser or equal to 65535.",
                    nXSize, nYSize, l_nBands);

        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Setup values based on options.                                  */
    /* -------------------------------------------------------------------- */
    const GTiffProfile eProfile =
        GetProfile(CSLFetchNameValue(papszParamList, "PROFILE"));

    const bool bTiled = CPLFetchBool(papszParamList, "TILED", false);

    int l_nBlockXSize = 0;
    const char *pszValue = CSLFetchNameValue(papszParamList, "BLOCKXSIZE");
    if (pszValue != nullptr)
    {
        l_nBlockXSize = atoi(pszValue);
        if (l_nBlockXSize < 0)
        {
            ReportError(pszFilename, CE_Failure, CPLE_IllegalArg,
                        "Invalid value for BLOCKXSIZE");
            return nullptr;
        }
    }

    int l_nBlockYSize = 0;
    pszValue = CSLFetchNameValue(papszParamList, "BLOCKYSIZE");
    if (pszValue != nullptr)
    {
        l_nBlockYSize = atoi(pszValue);
        if (l_nBlockYSize < 0)
        {
            ReportError(pszFilename, CE_Failure, CPLE_IllegalArg,
                        "Invalid value for BLOCKYSIZE");
            return nullptr;
        }
    }

    if (bTiled)
    {
        if (l_nBlockXSize == 0)
            l_nBlockXSize = 256;

        if (l_nBlockYSize == 0)
            l_nBlockYSize = 256;
    }

    int nPlanar = 0;
    pszValue = CSLFetchNameValue(papszParamList, "INTERLEAVE");
    if (pszValue != nullptr)
    {
        if (EQUAL(pszValue, "PIXEL"))
            nPlanar = PLANARCONFIG_CONTIG;
        else if (EQUAL(pszValue, "BAND"))
        {
            nPlanar = PLANARCONFIG_SEPARATE;
        }
        else
        {
            ReportError(
                pszFilename, CE_Failure, CPLE_IllegalArg,
                "INTERLEAVE=%s unsupported, value must be PIXEL or BAND.",
                pszValue);
            return nullptr;
        }
    }
    else
    {
        nPlanar = PLANARCONFIG_CONTIG;
    }

    int l_nCompression = COMPRESSION_NONE;
    pszValue = CSLFetchNameValue(papszParamList, "COMPRESS");
    if (pszValue != nullptr)
    {
        l_nCompression = GTIFFGetCompressionMethod(pszValue, "COMPRESS");
        if (l_nCompression < 0)
            return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      How many bits per sample?  We have a special case if NBITS      */
    /*      specified for GDT_Byte, GDT_UInt16, GDT_UInt32.                 */
    /* -------------------------------------------------------------------- */
    int l_nBitsPerSample = GDALGetDataTypeSizeBits(eType);
    if (CSLFetchNameValue(papszParamList, "NBITS") != nullptr)
    {
        int nMinBits = 0;
        int nMaxBits = 0;
        l_nBitsPerSample = atoi(CSLFetchNameValue(papszParamList, "NBITS"));
        if (eType == GDT_Byte)
        {
            nMinBits = 1;
            nMaxBits = 8;
        }
        else if (eType == GDT_UInt16)
        {
            nMinBits = 9;
            nMaxBits = 16;
        }
        else if (eType == GDT_UInt32)
        {
            nMinBits = 17;
            nMaxBits = 32;
        }
        else if (eType == GDT_Float32)
        {
            if (l_nBitsPerSample != 16 && l_nBitsPerSample != 32)
            {
                ReportError(pszFilename, CE_Warning, CPLE_NotSupported,
                            "Only NBITS=16 is supported for data type Float32");
                l_nBitsPerSample = GDALGetDataTypeSizeBits(eType);
            }
        }
        else
        {
            ReportError(pszFilename, CE_Warning, CPLE_NotSupported,
                        "NBITS is not supported for data type %s",
                        GDALGetDataTypeName(eType));
            l_nBitsPerSample = GDALGetDataTypeSizeBits(eType);
        }

        if (nMinBits != 0)
        {
            if (l_nBitsPerSample < nMinBits)
            {
                ReportError(
                    pszFilename, CE_Warning, CPLE_AppDefined,
                    "NBITS=%d is invalid for data type %s. Using NBITS=%d",
                    l_nBitsPerSample, GDALGetDataTypeName(eType), nMinBits);
                l_nBitsPerSample = nMinBits;
            }
            else if (l_nBitsPerSample > nMaxBits)
            {
                ReportError(
                    pszFilename, CE_Warning, CPLE_AppDefined,
                    "NBITS=%d is invalid for data type %s. Using NBITS=%d",
                    l_nBitsPerSample, GDALGetDataTypeName(eType), nMaxBits);
                l_nBitsPerSample = nMaxBits;
            }
        }
    }

    int nPredictor = PREDICTOR_NONE;
    pszValue = CSLFetchNameValue(papszParamList, "PREDICTOR");
    if (pszValue != nullptr)
    {
        nPredictor = atoi(pszValue);
    }

    // Do early checks as libtiff will only error out when starting to write.
    if (nPredictor != PREDICTOR_NONE &&
        CPLTestBool(CPLGetConfigOption("GDAL_GTIFF_PREDICTOR_CHECKS", "YES")))
    {
#if (TIFFLIB_VERSION > 20210416) || defined(INTERNAL_LIBTIFF)
#define HAVE_PREDICTOR_2_FOR_64BIT
#endif
        if (nPredictor == 2)
        {
            if (l_nBitsPerSample != 8 && l_nBitsPerSample != 16 &&
                l_nBitsPerSample != 32
#ifdef HAVE_PREDICTOR_2_FOR_64BIT
                && l_nBitsPerSample != 64
#endif
            )
            {
#if !defined(HAVE_PREDICTOR_2_FOR_64BIT)
                if (l_nBitsPerSample == 64)
                {
                    ReportError(
                        pszFilename, CE_Failure, CPLE_AppDefined,
                        "PREDICTOR=2 is only supported with 64 bit samples "
                        "starting with libtiff > 4.3.0.");
                }
                else
#endif
                {
                    ReportError(pszFilename, CE_Failure, CPLE_AppDefined,
#ifdef HAVE_PREDICTOR_2_FOR_64BIT
                                "PREDICTOR=2 is only supported with 8/16/32/64 "
                                "bit samples."
#else
                                "PREDICTOR=2 is only supported with 8/16/32 "
                                "bit samples."
#endif
                    );
                }
                return nullptr;
            }
        }
        else if (nPredictor == 3)
        {
            if (eType != GDT_Float32 && eType != GDT_Float64)
            {
                ReportError(
                    pszFilename, CE_Failure, CPLE_AppDefined,
                    "PREDICTOR=3 is only supported with Float32 or Float64.");
                return nullptr;
            }
        }
        else
        {
            ReportError(pszFilename, CE_Failure, CPLE_AppDefined,
                        "PREDICTOR=%s is not supported.", pszValue);
            return nullptr;
        }
    }

    const int l_nZLevel = GTiffGetZLevel(papszParamList);
    const int l_nLZMAPreset = GTiffGetLZMAPreset(papszParamList);
    const int l_nZSTDLevel = GTiffGetZSTDPreset(papszParamList);
    const int l_nWebPLevel = GTiffGetWebPLevel(papszParamList);
    const bool l_bWebPLossless = GTiffGetWebPLossless(papszParamList);
    const int l_nJpegQuality = GTiffGetJpegQuality(papszParamList);
    const int l_nJpegTablesMode = GTiffGetJpegTablesMode(papszParamList);
    const double l_dfMaxZError = GTiffGetLERCMaxZError(papszParamList);
#if HAVE_JXL
    const bool l_bJXLLossless = GTiffGetJXLLossless(papszParamList);
    const uint32_t l_nJXLEffort = GTiffGetJXLEffort(papszParamList);
    const float l_fJXLDistance = GTiffGetJXLDistance(papszParamList);
#endif
    /* -------------------------------------------------------------------- */
    /*      Streaming related code                                          */
    /* -------------------------------------------------------------------- */
    const CPLString osOriFilename(pszFilename);
    bool bStreaming = strcmp(pszFilename, "/vsistdout/") == 0 ||
                      CPLFetchBool(papszParamList, "STREAMABLE_OUTPUT", false);
#ifdef S_ISFIFO
    if (!bStreaming)
    {
        VSIStatBufL sStat;
        if (VSIStatExL(pszFilename, &sStat,
                       VSI_STAT_EXISTS_FLAG | VSI_STAT_NATURE_FLAG) == 0 &&
            S_ISFIFO(sStat.st_mode))
        {
            bStreaming = true;
        }
    }
#endif
    if (bStreaming && !EQUAL("NONE", CSLFetchNameValueDef(papszParamList,
                                                          "COMPRESS", "NONE")))
    {
        ReportError(pszFilename, CE_Failure, CPLE_NotSupported,
                    "Streaming only supported to uncompressed TIFF");
        return nullptr;
    }
    if (bStreaming && CPLFetchBool(papszParamList, "SPARSE_OK", false))
    {
        ReportError(pszFilename, CE_Failure, CPLE_NotSupported,
                    "Streaming not supported with SPARSE_OK");
        return nullptr;
    }
    const bool bCopySrcOverviews =
        CPLFetchBool(papszParamList, "COPY_SRC_OVERVIEWS", false);
    if (bStreaming && bCopySrcOverviews)
    {
        ReportError(pszFilename, CE_Failure, CPLE_NotSupported,
                    "Streaming not supported with COPY_SRC_OVERVIEWS");
        return nullptr;
    }
    if (bStreaming)
    {
        static int nCounter = 0;
        l_osTmpFilename = CPLSPrintf("/vsimem/vsistdout_%d.tif", ++nCounter);
        pszFilename = l_osTmpFilename.c_str();
    }

    /* -------------------------------------------------------------------- */
    /*      Compute the uncompressed size.                                  */
    /* -------------------------------------------------------------------- */
    const unsigned nTileXCount =
        bTiled ? DIV_ROUND_UP(nXSize, l_nBlockXSize) : 0;
    const unsigned nTileYCount =
        bTiled ? DIV_ROUND_UP(nYSize, l_nBlockYSize) : 0;
    const double dfUncompressedImageSize =
        (bTiled ? (static_cast<double>(nTileXCount) * nTileYCount *
                   l_nBlockXSize * l_nBlockYSize)
                : (nXSize * static_cast<double>(nYSize))) *
            l_nBands * GDALGetDataTypeSizeBytes(eType) +
        dfExtraSpaceForOverviews;

    /* -------------------------------------------------------------------- */
    /*      Should the file be created as a bigtiff file?                   */
    /* -------------------------------------------------------------------- */
    const char *pszBIGTIFF = CSLFetchNameValue(papszParamList, "BIGTIFF");

    if (pszBIGTIFF == nullptr)
        pszBIGTIFF = "IF_NEEDED";

    bool bCreateBigTIFF = false;
    if (EQUAL(pszBIGTIFF, "IF_NEEDED"))
    {
        if (l_nCompression == COMPRESSION_NONE &&
            dfUncompressedImageSize > 4200000000.0)
            bCreateBigTIFF = true;
    }
    else if (EQUAL(pszBIGTIFF, "IF_SAFER"))
    {
        if (dfUncompressedImageSize > 2000000000.0)
            bCreateBigTIFF = true;
    }
    else
    {
        bCreateBigTIFF = CPLTestBool(pszBIGTIFF);
        if (!bCreateBigTIFF && l_nCompression == COMPRESSION_NONE &&
            dfUncompressedImageSize > 4200000000.0)
        {
            ReportError(pszFilename, CE_Failure, CPLE_NotSupported,
                        "The TIFF file will be larger than 4GB, so BigTIFF is "
                        "necessary.  Creation failed.");
            return nullptr;
        }
    }

    if (bCreateBigTIFF)
        CPLDebug("GTiff", "File being created as a BigTIFF.");

    /* -------------------------------------------------------------------- */
    /*      Sanity check.                                                   */
    /* -------------------------------------------------------------------- */
    if (bTiled)
    {
        // libtiff implementation limitation
        if (nTileXCount > 0x80000000U / (bCreateBigTIFF ? 8 : 4) / nTileYCount)
        {
            ReportError(pszFilename, CE_Failure, CPLE_NotSupported,
                        "File too large regarding tile size. This would result "
                        "in a file with tile arrays larger than 2GB");
            return nullptr;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Check free space (only for big, non sparse, uncompressed)       */
    /* -------------------------------------------------------------------- */
    if (l_nCompression == COMPRESSION_NONE && dfUncompressedImageSize >= 1e9 &&
        !CPLFetchBool(papszParamList, "SPARSE_OK", false) &&
        osOriFilename != "/vsistdout/" &&
        osOriFilename != "/vsistdout_redirect/" &&
        CPLTestBool(CPLGetConfigOption("CHECK_DISK_FREE_SPACE", "TRUE")))
    {
        GIntBig nFreeDiskSpace =
            VSIGetDiskFreeSpace(CPLGetDirname(pszFilename));
        if (nFreeDiskSpace >= 0 && nFreeDiskSpace < dfUncompressedImageSize)
        {
            ReportError(pszFilename, CE_Failure, CPLE_FileIO,
                        "Free disk space available is " CPL_FRMT_GIB " bytes, "
                        "whereas " CPL_FRMT_GIB " are at least necessary. "
                        "You can disable this check by defining the "
                        "CHECK_DISK_FREE_SPACE configuration option to FALSE.",
                        nFreeDiskSpace,
                        static_cast<GIntBig>(dfUncompressedImageSize));
            return nullptr;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Check if the user wishes a particular endianness                */
    /* -------------------------------------------------------------------- */

    int eEndianness = ENDIANNESS_NATIVE;
    pszValue = CSLFetchNameValue(papszParamList, "ENDIANNESS");
    if (pszValue == nullptr)
        pszValue = CPLGetConfigOption("GDAL_TIFF_ENDIANNESS", nullptr);
    if (pszValue != nullptr)
    {
        if (EQUAL(pszValue, "LITTLE"))
        {
            eEndianness = ENDIANNESS_LITTLE;
        }
        else if (EQUAL(pszValue, "BIG"))
        {
            eEndianness = ENDIANNESS_BIG;
        }
        else if (EQUAL(pszValue, "INVERTED"))
        {
#ifdef CPL_LSB
            eEndianness = ENDIANNESS_BIG;
#else
            eEndianness = ENDIANNESS_LITTLE;
#endif
        }
        else if (!EQUAL(pszValue, "NATIVE"))
        {
            ReportError(pszFilename, CE_Warning, CPLE_NotSupported,
                        "ENDIANNESS=%s not supported. Defaulting to NATIVE",
                        pszValue);
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Try opening the dataset.                                        */
    /* -------------------------------------------------------------------- */

    const bool bAppend =
        CPLFetchBool(papszParamList, "APPEND_SUBDATASET", false);

    char szOpeningFlag[5] = {};
    strcpy(szOpeningFlag, bAppend ? "r+" : "w+");
    if (bCreateBigTIFF)
        strcat(szOpeningFlag, "8");
    if (eEndianness == ENDIANNESS_BIG)
        strcat(szOpeningFlag, "b");
    else if (eEndianness == ENDIANNESS_LITTLE)
        strcat(szOpeningFlag, "l");

    VSILFILE *l_fpL = VSIFOpenL(pszFilename, bAppend ? "r+b" : "w+b");
    if (l_fpL == nullptr)
    {
        CPLError(CE_Failure, CPLE_OpenFailed,
                 "Attempt to create new tiff file `%s' failed: %s", pszFilename,
                 VSIStrerror(errno));
        return nullptr;
    }
    TIFF *l_hTIFF = VSI_TIFFOpen(pszFilename, szOpeningFlag, l_fpL);
    if (l_hTIFF == nullptr)
    {
        if (CPLGetLastErrorNo() == 0)
            CPLError(CE_Failure, CPLE_OpenFailed,
                     "Attempt to create new tiff file `%s' "
                     "failed in XTIFFOpen().",
                     pszFilename);
        CPL_IGNORE_RET_VAL(VSIFCloseL(l_fpL));
        return nullptr;
    }

    if (bAppend)
    {
        // This is a bit of a hack to cause (*tif->tif_cleanup)(tif); to be
        // called. See https://trac.osgeo.org/gdal/ticket/2055
        TIFFSetField(l_hTIFF, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
        TIFFFreeDirectory(l_hTIFF);
        TIFFCreateDirectory(l_hTIFF);
    }

    /* -------------------------------------------------------------------- */
    /*      Do we have a custom pixel type (just used for signed byte now). */
    /* -------------------------------------------------------------------- */
    const char *pszPixelType = CSLFetchNameValue(papszParamList, "PIXELTYPE");
    if (pszPixelType == nullptr)
        pszPixelType = "";

    /* -------------------------------------------------------------------- */
    /*      Setup some standard flags.                                      */
    /* -------------------------------------------------------------------- */
    TIFFSetField(l_hTIFF, TIFFTAG_IMAGEWIDTH, nXSize);
    TIFFSetField(l_hTIFF, TIFFTAG_IMAGELENGTH, nYSize);
    TIFFSetField(l_hTIFF, TIFFTAG_BITSPERSAMPLE, l_nBitsPerSample);

    uint16_t l_nSampleFormat = 0;
    if ((eType == GDT_Byte && EQUAL(pszPixelType, "SIGNEDBYTE")) ||
        eType == GDT_Int16 || eType == GDT_Int32 || eType == GDT_Int64)
        l_nSampleFormat = SAMPLEFORMAT_INT;
    else if (eType == GDT_CInt16 || eType == GDT_CInt32)
        l_nSampleFormat = SAMPLEFORMAT_COMPLEXINT;
    else if (eType == GDT_Float32 || eType == GDT_Float64)
        l_nSampleFormat = SAMPLEFORMAT_IEEEFP;
    else if (eType == GDT_CFloat32 || eType == GDT_CFloat64)
        l_nSampleFormat = SAMPLEFORMAT_COMPLEXIEEEFP;
    else
        l_nSampleFormat = SAMPLEFORMAT_UINT;

    TIFFSetField(l_hTIFF, TIFFTAG_SAMPLEFORMAT, l_nSampleFormat);
    TIFFSetField(l_hTIFF, TIFFTAG_SAMPLESPERPIXEL, l_nBands);
    TIFFSetField(l_hTIFF, TIFFTAG_PLANARCONFIG, nPlanar);

    /* -------------------------------------------------------------------- */
    /*      Setup Photometric Interpretation. Take this value from the user */
    /*      passed option or guess correct value otherwise.                 */
    /* -------------------------------------------------------------------- */
    int nSamplesAccountedFor = 1;
    bool bForceColorTable = false;

    pszValue = CSLFetchNameValue(papszParamList, "PHOTOMETRIC");
    if (pszValue != nullptr)
    {
        if (EQUAL(pszValue, "MINISBLACK"))
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
        else if (EQUAL(pszValue, "MINISWHITE"))
        {
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISWHITE);
        }
        else if (EQUAL(pszValue, "PALETTE"))
        {
            if (eType == GDT_Byte || eType == GDT_UInt16)
            {
                TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_PALETTE);
                nSamplesAccountedFor = 1;
                bForceColorTable = true;
            }
            else
            {
                ReportError(
                    pszFilename, CE_Warning, CPLE_AppDefined,
                    "PHOTOMETRIC=PALETTE only compatible with Byte or UInt16");
            }
        }
        else if (EQUAL(pszValue, "RGB"))
        {
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
            nSamplesAccountedFor = 3;
        }
        else if (EQUAL(pszValue, "CMYK"))
        {
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_SEPARATED);
            nSamplesAccountedFor = 4;
        }
        else if (EQUAL(pszValue, "YCBCR"))
        {
            // Because of subsampling, setting YCBCR without JPEG compression
            // leads to a crash currently. Would need to make
            // GTiffRasterBand::IWriteBlock() aware of subsampling so that it
            // doesn't overrun buffer size returned by libtiff.
            if (l_nCompression != COMPRESSION_JPEG)
            {
                ReportError(
                    pszFilename, CE_Failure, CPLE_NotSupported,
                    "Currently, PHOTOMETRIC=YCBCR requires COMPRESS=JPEG");
                XTIFFClose(l_hTIFF);
                CPL_IGNORE_RET_VAL(VSIFCloseL(l_fpL));
                return nullptr;
            }

            if (nPlanar == PLANARCONFIG_SEPARATE)
            {
                ReportError(pszFilename, CE_Failure, CPLE_NotSupported,
                            "PHOTOMETRIC=YCBCR requires INTERLEAVE=PIXEL");
                XTIFFClose(l_hTIFF);
                CPL_IGNORE_RET_VAL(VSIFCloseL(l_fpL));
                return nullptr;
            }

            // YCBCR strictly requires 3 bands. Not less, not more Issue an
            // explicit error message as libtiff one is a bit cryptic:
            // TIFFVStripSize64:Invalid td_samplesperpixel value.
            if (l_nBands != 3)
            {
                ReportError(
                    pszFilename, CE_Failure, CPLE_NotSupported,
                    "PHOTOMETRIC=YCBCR not supported on a %d-band raster: "
                    "only compatible of a 3-band (RGB) raster",
                    l_nBands);
                XTIFFClose(l_hTIFF);
                CPL_IGNORE_RET_VAL(VSIFCloseL(l_fpL));
                return nullptr;
            }

            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_YCBCR);
            nSamplesAccountedFor = 3;

            // Explicitly register the subsampling so that JPEGFixupTags
            // is a no-op (helps for cloud optimized geotiffs)
            TIFFSetField(l_hTIFF, TIFFTAG_YCBCRSUBSAMPLING, 2, 2);
        }
        else if (EQUAL(pszValue, "CIELAB"))
        {
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_CIELAB);
            nSamplesAccountedFor = 3;
        }
        else if (EQUAL(pszValue, "ICCLAB"))
        {
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_ICCLAB);
            nSamplesAccountedFor = 3;
        }
        else if (EQUAL(pszValue, "ITULAB"))
        {
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_ITULAB);
            nSamplesAccountedFor = 3;
        }
        else
        {
            ReportError(pszFilename, CE_Warning, CPLE_IllegalArg,
                        "PHOTOMETRIC=%s value not recognised, ignoring.  "
                        "Set the Photometric Interpretation as MINISBLACK.",
                        pszValue);
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
        }

        if (l_nBands < nSamplesAccountedFor)
        {
            ReportError(pszFilename, CE_Warning, CPLE_IllegalArg,
                        "PHOTOMETRIC=%s value does not correspond to number "
                        "of bands (%d), ignoring.  "
                        "Set the Photometric Interpretation as MINISBLACK.",
                        pszValue, l_nBands);
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
        }
    }
    else
    {
        // If image contains 3 or 4 bands and datatype is Byte then we will
        // assume it is RGB. In all other cases assume it is MINISBLACK.
        if (l_nBands == 3 && eType == GDT_Byte)
        {
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
            nSamplesAccountedFor = 3;
        }
        else if (l_nBands == 4 && eType == GDT_Byte)
        {
            uint16_t v[1] = {
                GTiffGetAlphaValue(CSLFetchNameValue(papszParamList, "ALPHA"),
                                   DEFAULT_ALPHA_TYPE)};

            TIFFSetField(l_hTIFF, TIFFTAG_EXTRASAMPLES, 1, v);
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
            nSamplesAccountedFor = 4;
        }
        else
        {
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
            nSamplesAccountedFor = 1;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      If there are extra samples, we need to mark them with an        */
    /*      appropriate extrasamples definition here.                       */
    /* -------------------------------------------------------------------- */
    if (l_nBands > nSamplesAccountedFor)
    {
        const int nExtraSamples = l_nBands - nSamplesAccountedFor;

        uint16_t *v = static_cast<uint16_t *>(
            CPLMalloc(sizeof(uint16_t) * nExtraSamples));

        v[0] = GTiffGetAlphaValue(CSLFetchNameValue(papszParamList, "ALPHA"),
                                  EXTRASAMPLE_UNSPECIFIED);

        for (int i = 1; i < nExtraSamples; ++i)
            v[i] = EXTRASAMPLE_UNSPECIFIED;

        TIFFSetField(l_hTIFF, TIFFTAG_EXTRASAMPLES, nExtraSamples, v);

        CPLFree(v);
    }

    // Set the ICC color profile.
    if (eProfile != GTiffProfile::BASELINE)
    {
        SaveICCProfile(nullptr, l_hTIFF, papszParamList, l_nBitsPerSample);
    }

    // Set the compression method before asking the default strip size
    // This is useful when translating to a JPEG-In-TIFF file where
    // the default strip size is 8 or 16 depending on the photometric value.
    TIFFSetField(l_hTIFF, TIFFTAG_COMPRESSION, l_nCompression);

    if (l_nCompression == COMPRESSION_LERC)
    {
        const char *pszCompress =
            CSLFetchNameValueDef(papszParamList, "COMPRESS", "");
        if (EQUAL(pszCompress, "LERC_DEFLATE"))
        {
            TIFFSetField(l_hTIFF, TIFFTAG_LERC_ADD_COMPRESSION,
                         LERC_ADD_COMPRESSION_DEFLATE);
        }
        else if (EQUAL(pszCompress, "LERC_ZSTD"))
        {
            if (TIFFSetField(l_hTIFF, TIFFTAG_LERC_ADD_COMPRESSION,
                             LERC_ADD_COMPRESSION_ZSTD) != 1)
            {
                XTIFFClose(l_hTIFF);
                CPL_IGNORE_RET_VAL(VSIFCloseL(l_fpL));
                return nullptr;
            }
        }
    }
    // TODO later: take into account LERC version

    /* -------------------------------------------------------------------- */
    /*      Setup tiling/stripping flags.                                   */
    /* -------------------------------------------------------------------- */
    if (bTiled)
    {
        if (!TIFFSetField(l_hTIFF, TIFFTAG_TILEWIDTH, l_nBlockXSize) ||
            !TIFFSetField(l_hTIFF, TIFFTAG_TILELENGTH, l_nBlockYSize))
        {
            XTIFFClose(l_hTIFF);
            CPL_IGNORE_RET_VAL(VSIFCloseL(l_fpL));
            return nullptr;
        }
    }
    else
    {
        const uint32_t l_nRowsPerStrip = std::min(
            nYSize, l_nBlockYSize == 0
                        ? static_cast<int>(TIFFDefaultStripSize(l_hTIFF, 0))
                        : l_nBlockYSize);

        TIFFSetField(l_hTIFF, TIFFTAG_ROWSPERSTRIP, l_nRowsPerStrip);
    }

    /* -------------------------------------------------------------------- */
    /*      Set compression related tags.                                   */
    /* -------------------------------------------------------------------- */
    if (GTIFFSupportsPredictor(l_nCompression))
        TIFFSetField(l_hTIFF, TIFFTAG_PREDICTOR, nPredictor);
    if (l_nCompression == COMPRESSION_ADOBE_DEFLATE ||
        l_nCompression == COMPRESSION_LERC)
    {
        GTiffSetDeflateSubCodec(l_hTIFF);

        if (l_nZLevel != -1)
            TIFFSetField(l_hTIFF, TIFFTAG_ZIPQUALITY, l_nZLevel);
    }
    if (l_nCompression == COMPRESSION_JPEG && l_nJpegQuality != -1)
        TIFFSetField(l_hTIFF, TIFFTAG_JPEGQUALITY, l_nJpegQuality);
    if (l_nCompression == COMPRESSION_LZMA && l_nLZMAPreset != -1)
        TIFFSetField(l_hTIFF, TIFFTAG_LZMAPRESET, l_nLZMAPreset);
    if ((l_nCompression == COMPRESSION_ZSTD ||
         l_nCompression == COMPRESSION_LERC) &&
        l_nZSTDLevel != -1)
        TIFFSetField(l_hTIFF, TIFFTAG_ZSTD_LEVEL, l_nZSTDLevel);
    if (l_nCompression == COMPRESSION_LERC)
    {
        TIFFSetField(l_hTIFF, TIFFTAG_LERC_MAXZERROR, l_dfMaxZError);
    }
#if HAVE_JXL
    if (l_nCompression == COMPRESSION_JXL)
    {
        TIFFSetField(l_hTIFF, TIFFTAG_JXL_LOSSYNESS,
                     l_bJXLLossless ? JXL_LOSSLESS : JXL_LOSSY);
        TIFFSetField(l_hTIFF, TIFFTAG_JXL_EFFORT, l_nJXLEffort);
        TIFFSetField(l_hTIFF, TIFFTAG_JXL_DISTANCE, l_fJXLDistance);
    }
#endif
    if (l_nCompression == COMPRESSION_WEBP)
        TIFFSetField(l_hTIFF, TIFFTAG_WEBP_LEVEL, l_nWebPLevel);
    if (l_nCompression == COMPRESSION_WEBP && l_bWebPLossless)
        TIFFSetField(l_hTIFF, TIFFTAG_WEBP_LOSSLESS, 1);

    if (l_nCompression == COMPRESSION_JPEG)
        TIFFSetField(l_hTIFF, TIFFTAG_JPEGTABLESMODE, l_nJpegTablesMode);

    /* -------------------------------------------------------------------- */
    /*      If we forced production of a file with photometric=palette,     */
    /*      we need to push out a default color table.                      */
    /* -------------------------------------------------------------------- */
    if (bForceColorTable)
    {
        const int nColors = eType == GDT_Byte ? 256 : 65536;

        unsigned short *panTRed = static_cast<unsigned short *>(
            CPLMalloc(sizeof(unsigned short) * nColors));
        unsigned short *panTGreen = static_cast<unsigned short *>(
            CPLMalloc(sizeof(unsigned short) * nColors));
        unsigned short *panTBlue = static_cast<unsigned short *>(
            CPLMalloc(sizeof(unsigned short) * nColors));

        for (int iColor = 0; iColor < nColors; ++iColor)
        {
            if (eType == GDT_Byte)
            {
                panTRed[iColor] = static_cast<unsigned short>(257 * iColor);
                panTGreen[iColor] = static_cast<unsigned short>(257 * iColor);
                panTBlue[iColor] = static_cast<unsigned short>(257 * iColor);
            }
            else
            {
                panTRed[iColor] = static_cast<unsigned short>(iColor);
                panTGreen[iColor] = static_cast<unsigned short>(iColor);
                panTBlue[iColor] = static_cast<unsigned short>(iColor);
            }
        }

        TIFFSetField(l_hTIFF, TIFFTAG_COLORMAP, panTRed, panTGreen, panTBlue);

        CPLFree(panTRed);
        CPLFree(panTGreen);
        CPLFree(panTBlue);
    }

    // This trick
    // creates a temporary in-memory file and fetches its JPEG tables so that
    // we can directly set them, before tif_jpeg.c compute them at the first
    // strip/tile writing, which is too late, since we have already crystalized
    // the directory. This way we avoid a directory rewriting.
    if (l_nCompression == COMPRESSION_JPEG &&
        !STARTS_WITH(pszFilename, szJPEGGTiffDatasetTmpPrefix) &&
        CPLTestBool(
            CSLFetchNameValueDef(papszParamList, "WRITE_JPEGTABLE_TAG", "YES")))
    {
        GTiffWriteJPEGTables(
            l_hTIFF, CSLFetchNameValue(papszParamList, "PHOTOMETRIC"),
            CSLFetchNameValue(papszParamList, "JPEG_QUALITY"),
            CSLFetchNameValue(papszParamList, "JPEGTABLESMODE"));
    }

    *pfpL = l_fpL;

    return l_hTIFF;
}

/************************************************************************/
/*                      GTiffWriteJPEGTables()                          */
/*                                                                      */
/*      Sets the TIFFTAG_JPEGTABLES (and TIFFTAG_REFERENCEBLACKWHITE)   */
/*      tags immediately, instead of relying on the TIFF JPEG codec     */
/*      to write them when it starts compressing imagery. This avoids   */
/*      an IFD rewrite at the end of the file.                          */
/*      Must be used after having set TIFFTAG_SAMPLESPERPIXEL,          */
/*      TIFFTAG_BITSPERSAMPLE.                                          */
/************************************************************************/

void GTiffWriteJPEGTables(TIFF *hTIFF, const char *pszPhotometric,
                          const char *pszJPEGQuality,
                          const char *pszJPEGTablesMode)
{
    // This trick
    // creates a temporary in-memory file and fetches its JPEG tables so that
    // we can directly set them, before tif_jpeg.c compute them at the first
    // strip/tile writing, which is too late, since we have already crystalized
    // the directory. This way we avoid a directory rewriting.
    uint16_t nBands = 0;
    if (!TIFFGetField(hTIFF, TIFFTAG_SAMPLESPERPIXEL, &nBands))
        nBands = 1;

    uint16_t l_nBitsPerSample = 0;
    if (!TIFFGetField(hTIFF, TIFFTAG_BITSPERSAMPLE, &(l_nBitsPerSample)))
        l_nBitsPerSample = 1;

    CPLString osTmpFilenameIn;
    osTmpFilenameIn.Printf("%s%p", szJPEGGTiffDatasetTmpPrefix, hTIFF);
    VSILFILE *fpTmp = nullptr;
    CPLString osTmp;
    char **papszLocalParameters = nullptr;
    const int nInMemImageWidth = 16;
    const int nInMemImageHeight = 16;
    papszLocalParameters =
        CSLSetNameValue(papszLocalParameters, "COMPRESS", "JPEG");
    papszLocalParameters =
        CSLSetNameValue(papszLocalParameters, "JPEG_QUALITY", pszJPEGQuality);
    if (nBands <= 4)
    {
        papszLocalParameters = CSLSetNameValue(papszLocalParameters,
                                               "PHOTOMETRIC", pszPhotometric);
    }
    papszLocalParameters = CSLSetNameValue(papszLocalParameters, "BLOCKYSIZE",
                                           CPLSPrintf("%u", nInMemImageHeight));
    papszLocalParameters = CSLSetNameValue(papszLocalParameters, "NBITS",
                                           CPLSPrintf("%u", l_nBitsPerSample));
    papszLocalParameters = CSLSetNameValue(papszLocalParameters,
                                           "JPEGTABLESMODE", pszJPEGTablesMode);

    TIFF *hTIFFTmp =
        GTiffDataset::CreateLL(osTmpFilenameIn, nInMemImageWidth,
                               nInMemImageHeight, (nBands <= 4) ? nBands : 1,
                               (l_nBitsPerSample <= 8) ? GDT_Byte : GDT_UInt16,
                               0.0, papszLocalParameters, &fpTmp, osTmp);
    CSLDestroy(papszLocalParameters);
    if (hTIFFTmp)
    {
        uint16_t l_nPhotometric = 0;
        int nJpegTablesModeIn = 0;
        TIFFGetField(hTIFFTmp, TIFFTAG_PHOTOMETRIC, &(l_nPhotometric));
        TIFFGetField(hTIFFTmp, TIFFTAG_JPEGTABLESMODE, &nJpegTablesModeIn);
        TIFFWriteCheck(hTIFFTmp, FALSE, "CreateLL");
        TIFFWriteDirectory(hTIFFTmp);
        TIFFSetDirectory(hTIFFTmp, 0);
        // Now, reset quality and jpegcolormode.
        const int l_nJpegQuality = pszJPEGQuality ? atoi(pszJPEGQuality) : 0;
        if (l_nJpegQuality > 0)
            TIFFSetField(hTIFFTmp, TIFFTAG_JPEGQUALITY, l_nJpegQuality);
        if (l_nPhotometric == PHOTOMETRIC_YCBCR &&
            CPLTestBool(CPLGetConfigOption("CONVERT_YCBCR_TO_RGB", "YES")))
        {
            TIFFSetField(hTIFFTmp, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB);
        }
        if (nJpegTablesModeIn >= 0)
            TIFFSetField(hTIFFTmp, TIFFTAG_JPEGTABLESMODE, nJpegTablesModeIn);

        GPtrDiff_t nBlockSize = static_cast<GPtrDiff_t>(nInMemImageWidth) *
                                nInMemImageHeight *
                                ((nBands <= 4) ? nBands : 1);
        if (l_nBitsPerSample == 12)
            nBlockSize = (nBlockSize * 3) / 2;
        std::vector<GByte> abyZeroData(nBlockSize, 0);
        TIFFWriteEncodedStrip(hTIFFTmp, 0, &abyZeroData[0], nBlockSize);

        uint32_t nJPEGTableSize = 0;
        void *pJPEGTable = nullptr;
        if (TIFFGetField(hTIFFTmp, TIFFTAG_JPEGTABLES, &nJPEGTableSize,
                         &pJPEGTable))
            TIFFSetField(hTIFF, TIFFTAG_JPEGTABLES, nJPEGTableSize, pJPEGTable);

        float *ref = nullptr;
        if (TIFFGetField(hTIFFTmp, TIFFTAG_REFERENCEBLACKWHITE, &ref))
            TIFFSetField(hTIFF, TIFFTAG_REFERENCEBLACKWHITE, ref);

        XTIFFClose(hTIFFTmp);
        CPL_IGNORE_RET_VAL(VSIFCloseL(fpTmp));
    }
    VSIUnlink(osTmpFilenameIn);
}

/************************************************************************/
/*                            GuessJPEGQuality()                        */
/*                                                                      */
/*      Guess JPEG quality from JPEGTABLES tag.                         */
/************************************************************************/

static const GByte *GTIFFFindNextTable(const GByte *paby, GByte byMarker,
                                       int nLen, int *pnLenTable)
{
    for (int i = 0; i + 1 < nLen;)
    {
        if (paby[i] != 0xFF)
            return nullptr;
        ++i;
        if (paby[i] == 0xD8)
        {
            ++i;
            continue;
        }
        if (i + 2 >= nLen)
            return nullptr;
        int nMarkerLen = paby[i + 1] * 256 + paby[i + 2];
        if (i + 1 + nMarkerLen >= nLen)
            return nullptr;
        if (paby[i] == byMarker)
        {
            if (pnLenTable)
                *pnLenTable = nMarkerLen;
            return paby + i + 1;
        }
        i += 1 + nMarkerLen;
    }
    return nullptr;
}

constexpr GByte MARKER_HUFFMAN_TABLE = 0xC4;
constexpr GByte MARKER_QUANT_TABLE = 0xDB;

// We assume that if there are several quantization tables, they are
// in the same order. Which is a reasonable assumption for updating
// a file generated by ourselves.
static bool GTIFFQuantizationTablesEqual(const GByte *paby1, int nLen1,
                                         const GByte *paby2, int nLen2)
{
    bool bFound = false;
    while (true)
    {
        int nLenTable1 = 0;
        int nLenTable2 = 0;
        const GByte *paby1New =
            GTIFFFindNextTable(paby1, MARKER_QUANT_TABLE, nLen1, &nLenTable1);
        const GByte *paby2New =
            GTIFFFindNextTable(paby2, MARKER_QUANT_TABLE, nLen2, &nLenTable2);
        if (paby1New == nullptr && paby2New == nullptr)
            return bFound;
        if (paby1New == nullptr || paby2New == nullptr)
            return false;
        if (nLenTable1 != nLenTable2)
            return false;
        if (memcmp(paby1New, paby2New, nLenTable1) != 0)
            return false;
        paby1New += nLenTable1;
        paby2New += nLenTable2;
        nLen1 -= static_cast<int>(paby1New - paby1);
        nLen2 -= static_cast<int>(paby2New - paby2);
        paby1 = paby1New;
        paby2 = paby2New;
        bFound = true;
    }
}

// Guess the JPEG quality by comparing against the MD5Sum of precomputed
// quantization tables
static int GuessJPEGQualityFromMD5(const uint8_t md5JPEGQuantTable[][16],
                                   const GByte *const pabyJPEGTable,
                                   int nJPEGTableSize)
{
    int nRemainingLen = nJPEGTableSize;
    const GByte *pabyCur = pabyJPEGTable;

    struct CPLMD5Context context;
    CPLMD5Init(&context);

    while (true)
    {
        int nLenTable = 0;
        const GByte *pabyNew = GTIFFFindNextTable(pabyCur, MARKER_QUANT_TABLE,
                                                  nRemainingLen, &nLenTable);
        if (pabyNew == nullptr)
            break;
        CPLMD5Update(&context, pabyNew, nLenTable);
        pabyNew += nLenTable;
        nRemainingLen -= static_cast<int>(pabyNew - pabyCur);
        pabyCur = pabyNew;
    }

    GByte digest[16];
    CPLMD5Final(digest, &context);

    for (int i = 0; i < 100; i++)
    {
        if (memcmp(md5JPEGQuantTable[i], digest, 16) == 0)
        {
            return i + 1;
        }
    }
    return -1;
}

int GTiffDataset::GuessJPEGQuality(bool &bOutHasQuantizationTable,
                                   bool &bOutHasHuffmanTable)
{
    CPLAssert(m_nCompression == COMPRESSION_JPEG);
    uint32_t nJPEGTableSize = 0;
    void *pJPEGTable = nullptr;
    if (!TIFFGetField(m_hTIFF, TIFFTAG_JPEGTABLES, &nJPEGTableSize,
                      &pJPEGTable))
    {
        bOutHasQuantizationTable = false;
        bOutHasHuffmanTable = false;
        return -1;
    }

    bOutHasQuantizationTable =
        GTIFFFindNextTable(static_cast<const GByte *>(pJPEGTable),
                           MARKER_QUANT_TABLE, nJPEGTableSize,
                           nullptr) != nullptr;
    bOutHasHuffmanTable =
        GTIFFFindNextTable(static_cast<const GByte *>(pJPEGTable),
                           MARKER_HUFFMAN_TABLE, nJPEGTableSize,
                           nullptr) != nullptr;
    if (!bOutHasQuantizationTable)
        return -1;

    if ((nBands == 1 && m_nBitsPerSample == 8) ||
        (nBands == 3 && m_nBitsPerSample == 8 &&
         m_nPhotometric == PHOTOMETRIC_RGB) ||
        (nBands == 4 && m_nBitsPerSample == 8 &&
         m_nPhotometric == PHOTOMETRIC_SEPARATED))
    {
        return GuessJPEGQualityFromMD5(md5JPEGQuantTable_generic_8bit,
                                       static_cast<const GByte *>(pJPEGTable),
                                       static_cast<int>(nJPEGTableSize));
    }

    if (nBands == 3 && m_nBitsPerSample == 8 &&
        m_nPhotometric == PHOTOMETRIC_YCBCR)
    {
        int nRet =
            GuessJPEGQualityFromMD5(md5JPEGQuantTable_3_YCBCR_8bit,
                                    static_cast<const GByte *>(pJPEGTable),
                                    static_cast<int>(nJPEGTableSize));
        if (nRet < 0)
        {
            // libjpeg 9e has modified the YCbCr quantization tables.
            nRet =
                GuessJPEGQualityFromMD5(md5JPEGQuantTable_3_YCBCR_8bit_jpeg9e,
                                        static_cast<const GByte *>(pJPEGTable),
                                        static_cast<int>(nJPEGTableSize));
        }
        return nRet;
    }

    char **papszLocalParameters = nullptr;
    papszLocalParameters =
        CSLSetNameValue(papszLocalParameters, "COMPRESS", "JPEG");
    if (m_nPhotometric == PHOTOMETRIC_YCBCR)
        papszLocalParameters =
            CSLSetNameValue(papszLocalParameters, "PHOTOMETRIC", "YCBCR");
    else if (m_nPhotometric == PHOTOMETRIC_SEPARATED)
        papszLocalParameters =
            CSLSetNameValue(papszLocalParameters, "PHOTOMETRIC", "CMYK");
    papszLocalParameters =
        CSLSetNameValue(papszLocalParameters, "BLOCKYSIZE", "16");
    if (m_nBitsPerSample == 12)
        papszLocalParameters =
            CSLSetNameValue(papszLocalParameters, "NBITS", "12");

    CPLString osTmpFilenameIn;
    osTmpFilenameIn.Printf("/vsimem/gtiffdataset_guess_jpeg_quality_tmp_%p",
                           this);

    int nRet = -1;
    for (int nQuality = 0; nQuality <= 100 && nRet < 0; ++nQuality)
    {
        VSILFILE *fpTmp = nullptr;
        if (nQuality == 0)
            papszLocalParameters =
                CSLSetNameValue(papszLocalParameters, "JPEG_QUALITY", "75");
        else
            papszLocalParameters =
                CSLSetNameValue(papszLocalParameters, "JPEG_QUALITY",
                                CPLSPrintf("%d", nQuality));

        CPLPushErrorHandler(CPLQuietErrorHandler);
        CPLString osTmp;
        TIFF *hTIFFTmp =
            CreateLL(osTmpFilenameIn, 16, 16, (nBands <= 4) ? nBands : 1,
                     GetRasterBand(1)->GetRasterDataType(), 0.0,
                     papszLocalParameters, &fpTmp, osTmp);
        CPLPopErrorHandler();
        if (!hTIFFTmp)
        {
            break;
        }

        TIFFWriteCheck(hTIFFTmp, FALSE, "CreateLL");
        TIFFWriteDirectory(hTIFFTmp);
        TIFFSetDirectory(hTIFFTmp, 0);
        // Now reset jpegcolormode.
        if (m_nPhotometric == PHOTOMETRIC_YCBCR &&
            CPLTestBool(CPLGetConfigOption("CONVERT_YCBCR_TO_RGB", "YES")))
        {
            TIFFSetField(hTIFFTmp, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB);
        }

        GByte abyZeroData[(16 * 16 * 4 * 3) / 2] = {};
        const int nBlockSize =
            (16 * 16 * ((nBands <= 4) ? nBands : 1) * m_nBitsPerSample) / 8;
        TIFFWriteEncodedStrip(hTIFFTmp, 0, abyZeroData, nBlockSize);

        uint32_t nJPEGTableSizeTry = 0;
        void *pJPEGTableTry = nullptr;
        if (TIFFGetField(hTIFFTmp, TIFFTAG_JPEGTABLES, &nJPEGTableSizeTry,
                         &pJPEGTableTry))
        {
            if (GTIFFQuantizationTablesEqual(
                    static_cast<GByte *>(pJPEGTable), nJPEGTableSize,
                    static_cast<GByte *>(pJPEGTableTry), nJPEGTableSizeTry))
            {
                nRet = (nQuality == 0) ? 75 : nQuality;
            }
        }

        XTIFFClose(hTIFFTmp);
        CPL_IGNORE_RET_VAL(VSIFCloseL(fpTmp));
    }

    CSLDestroy(papszLocalParameters);
    VSIUnlink(osTmpFilenameIn);

    return nRet;
}

/************************************************************************/
/*               SetJPEGQualityAndTablesModeFromFile()                  */
/************************************************************************/

void GTiffDataset::SetJPEGQualityAndTablesModeFromFile(
    int nQuality, bool bHasQuantizationTable, bool bHasHuffmanTable)
{
    if (nQuality > 0)
    {
        CPLDebug("GTiff", "Guessed JPEG quality to be %d", nQuality);
        m_nJpegQuality = static_cast<signed char>(nQuality);
        TIFFSetField(m_hTIFF, TIFFTAG_JPEGQUALITY, nQuality);

        // This means we will use the quantization tables from the
        // JpegTables tag.
        m_nJpegTablesMode = JPEGTABLESMODE_QUANT;
    }
    else
    {
        uint32_t nJPEGTableSize = 0;
        void *pJPEGTable = nullptr;
        if (!TIFFGetField(m_hTIFF, TIFFTAG_JPEGTABLES, &nJPEGTableSize,
                          &pJPEGTable))
        {
            toff_t *panByteCounts = nullptr;
            const int nBlockCount = m_nPlanarConfig == PLANARCONFIG_SEPARATE
                                        ? m_nBlocksPerBand * nBands
                                        : m_nBlocksPerBand;
            if (TIFFIsTiled(m_hTIFF))
                TIFFGetField(m_hTIFF, TIFFTAG_TILEBYTECOUNTS, &panByteCounts);
            else
                TIFFGetField(m_hTIFF, TIFFTAG_STRIPBYTECOUNTS, &panByteCounts);

            bool bFoundNonEmptyBlock = false;
            if (panByteCounts != nullptr)
            {
                for (int iBlock = 0; iBlock < nBlockCount; ++iBlock)
                {
                    if (panByteCounts[iBlock] != 0)
                    {
                        bFoundNonEmptyBlock = true;
                        break;
                    }
                }
            }
            if (bFoundNonEmptyBlock)
            {
                CPLDebug("GTiff", "Could not guess JPEG quality. "
                                  "JPEG tables are missing, so going in "
                                  "TIFFTAG_JPEGTABLESMODE = 0/2 mode");
                // Write quantization tables in each strile.
                m_nJpegTablesMode = 0;
            }
        }
        else
        {
            if (bHasQuantizationTable)
            {
                // FIXME in libtiff: this is likely going to cause issues
                // since libtiff will reuse in each strile the number of
                // the global quantization table, which is invalid.
                CPLDebug("GTiff",
                         "Could not guess JPEG quality although JPEG "
                         "quantization tables are present, so going in "
                         "TIFFTAG_JPEGTABLESMODE = 0/2 mode");
            }
            else
            {
                CPLDebug("GTiff",
                         "Could not guess JPEG quality since JPEG "
                         "quantization tables are not present, so going in "
                         "TIFFTAG_JPEGTABLESMODE = 0/2 mode");
            }

            // Write quantization tables in each strile.
            m_nJpegTablesMode = 0;
        }
    }
    if (bHasHuffmanTable)
    {
        // If there are Huffman tables in header use them, otherwise
        // if we use optimized tables, libtiff will currently reuse
        // the number of the Huffman tables of the header for the
        // optimized version of each strile, which is illegal.
        m_nJpegTablesMode |= JPEGTABLESMODE_HUFF;
    }
    if (m_nJpegTablesMode >= 0)
        TIFFSetField(m_hTIFF, TIFFTAG_JPEGTABLESMODE, m_nJpegTablesMode);
}

/************************************************************************/
/*                               Create()                               */
/*                                                                      */
/*      Create a new GeoTIFF or TIFF file.                              */
/************************************************************************/

GDALDataset *GTiffDataset::Create(const char *pszFilename, int nXSize,
                                  int nYSize, int l_nBands, GDALDataType eType,
                                  char **papszParamList)

{
    VSILFILE *l_fpL = nullptr;
    CPLString l_osTmpFilename;

    /* -------------------------------------------------------------------- */
    /*      Create the underlying TIFF file.                                */
    /* -------------------------------------------------------------------- */
    TIFF *l_hTIFF = CreateLL(pszFilename, nXSize, nYSize, l_nBands, eType, 0,
                             papszParamList, &l_fpL, l_osTmpFilename);
    const bool bStreaming = !l_osTmpFilename.empty();

    if (l_hTIFF == nullptr)
        return nullptr;

    /* -------------------------------------------------------------------- */
    /*      Create the new GTiffDataset object.                             */
    /* -------------------------------------------------------------------- */
    GTiffDataset *poDS = new GTiffDataset();
    poDS->m_hTIFF = l_hTIFF;
    poDS->m_fpL = l_fpL;
    if (bStreaming)
    {
        poDS->m_bStreamingOut = true;
        poDS->m_pszTmpFilename = CPLStrdup(l_osTmpFilename);
        poDS->m_fpToWrite = VSIFOpenL(pszFilename, "wb");
        if (poDS->m_fpToWrite == nullptr)
        {
            VSIUnlink(l_osTmpFilename);
            delete poDS;
            return nullptr;
        }
    }
    poDS->nRasterXSize = nXSize;
    poDS->nRasterYSize = nYSize;
    poDS->eAccess = GA_Update;
    poDS->m_bCrystalized = false;
    poDS->m_nSamplesPerPixel = static_cast<uint16_t>(l_nBands);
    poDS->m_pszFilename = CPLStrdup(pszFilename);

    // Don't try to load external metadata files (#6597).
    poDS->m_bIMDRPCMetadataLoaded = true;

    // Avoid premature crystalization that will cause directory re-writing if
    // GetProjectionRef() or GetGeoTransform() are called on the newly created
    // GeoTIFF.
    poDS->m_bLookedForProjection = true;

    TIFFGetField(l_hTIFF, TIFFTAG_SAMPLEFORMAT, &(poDS->m_nSampleFormat));
    TIFFGetField(l_hTIFF, TIFFTAG_PLANARCONFIG, &(poDS->m_nPlanarConfig));
    // Weird that we need this, but otherwise we get a Valgrind warning on
    // tiff_write_124.
    if (!TIFFGetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, &(poDS->m_nPhotometric)))
        poDS->m_nPhotometric = PHOTOMETRIC_MINISBLACK;
    TIFFGetField(l_hTIFF, TIFFTAG_BITSPERSAMPLE, &(poDS->m_nBitsPerSample));
    TIFFGetField(l_hTIFF, TIFFTAG_COMPRESSION, &(poDS->m_nCompression));

    if (TIFFIsTiled(l_hTIFF))
    {
        TIFFGetField(l_hTIFF, TIFFTAG_TILEWIDTH, &(poDS->m_nBlockXSize));
        TIFFGetField(l_hTIFF, TIFFTAG_TILELENGTH, &(poDS->m_nBlockYSize));
    }
    else
    {
        if (!TIFFGetField(l_hTIFF, TIFFTAG_ROWSPERSTRIP,
                          &(poDS->m_nRowsPerStrip)))
            poDS->m_nRowsPerStrip = 1;  // Dummy value.

        poDS->m_nBlockXSize = nXSize;
        poDS->m_nBlockYSize =
            std::min(static_cast<int>(poDS->m_nRowsPerStrip), nYSize);
    }

    poDS->m_nBlocksPerBand = DIV_ROUND_UP(nYSize, poDS->m_nBlockYSize) *
                             DIV_ROUND_UP(nXSize, poDS->m_nBlockXSize);

    poDS->m_eProfile = GetProfile(CSLFetchNameValue(papszParamList, "PROFILE"));

    /* -------------------------------------------------------------------- */
    /*      YCbCr JPEG compressed images should be translated on the fly    */
    /*      to RGB by libtiff/libjpeg unless specifically requested         */
    /*      otherwise.                                                      */
    /* -------------------------------------------------------------------- */
    if (poDS->m_nCompression == COMPRESSION_JPEG &&
        poDS->m_nPhotometric == PHOTOMETRIC_YCBCR &&
        CPLTestBool(CPLGetConfigOption("CONVERT_YCBCR_TO_RGB", "YES")))
    {
        int nColorMode = 0;

        poDS->SetMetadataItem("SOURCE_COLOR_SPACE", "YCbCr", "IMAGE_STRUCTURE");
        if (!TIFFGetField(l_hTIFF, TIFFTAG_JPEGCOLORMODE, &nColorMode) ||
            nColorMode != JPEGCOLORMODE_RGB)
            TIFFSetField(l_hTIFF, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB);
    }

    if (poDS->m_nCompression == COMPRESSION_LERC)
    {
        uint32_t nLercParamCount = 0;
        uint32_t *panLercParams = nullptr;
        if (TIFFGetField(l_hTIFF, TIFFTAG_LERC_PARAMETERS, &nLercParamCount,
                         &panLercParams) &&
            nLercParamCount == 2)
        {
            memcpy(poDS->m_anLercAddCompressionAndVersion, panLercParams,
                   sizeof(poDS->m_anLercAddCompressionAndVersion));
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Read palette back as a color table if it has one.               */
    /* -------------------------------------------------------------------- */
    unsigned short *panRed = nullptr;
    unsigned short *panGreen = nullptr;
    unsigned short *panBlue = nullptr;

    if (poDS->m_nPhotometric == PHOTOMETRIC_PALETTE &&
        TIFFGetField(l_hTIFF, TIFFTAG_COLORMAP, &panRed, &panGreen, &panBlue))
    {

        poDS->m_poColorTable = new GDALColorTable();

        const int nColorCount = 1 << poDS->m_nBitsPerSample;

        for (int iColor = nColorCount - 1; iColor >= 0; iColor--)
        {
            const unsigned short divisor = 257;
            const GDALColorEntry oEntry = {
                static_cast<short>(panRed[iColor] / divisor),
                static_cast<short>(panGreen[iColor] / divisor),
                static_cast<short>(panBlue[iColor] / divisor),
                static_cast<short>(255)};

            poDS->m_poColorTable->SetColorEntry(iColor, &oEntry);
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Do we want to ensure all blocks get written out on close to     */
    /*      avoid sparse files?                                             */
    /* -------------------------------------------------------------------- */
    if (!CPLFetchBool(papszParamList, "SPARSE_OK", false))
        poDS->m_bFillEmptyTilesAtClosing = true;

    poDS->m_bWriteEmptyTiles =
        bStreaming || (poDS->m_nCompression != COMPRESSION_NONE &&
                       poDS->m_bFillEmptyTilesAtClosing);
    // Only required for people writing non-compressed striped files in the
    // right order and wanting all tstrips to be written in the same order
    // so that the end result can be memory mapped without knowledge of each
    // strip offset.
    if (CPLTestBool(CSLFetchNameValueDef(
            papszParamList, "WRITE_EMPTY_TILES_SYNCHRONOUSLY", "FALSE")) ||
        CPLTestBool(CSLFetchNameValueDef(
            papszParamList, "@WRITE_EMPTY_TILES_SYNCHRONOUSLY", "FALSE")))
    {
        poDS->m_bWriteEmptyTiles = true;
    }

    /* -------------------------------------------------------------------- */
    /*      Preserve creation options for consulting later (for instance    */
    /*      to decide if a TFW file should be written).                     */
    /* -------------------------------------------------------------------- */
    poDS->m_papszCreationOptions = CSLDuplicate(papszParamList);

    poDS->m_nZLevel = GTiffGetZLevel(papszParamList);
    poDS->m_nLZMAPreset = GTiffGetLZMAPreset(papszParamList);
    poDS->m_nZSTDLevel = GTiffGetZSTDPreset(papszParamList);
    poDS->m_nWebPLevel = GTiffGetWebPLevel(papszParamList);
    poDS->m_bWebPLossless = GTiffGetWebPLossless(papszParamList);
    if (poDS->m_nWebPLevel != 100 && poDS->m_bWebPLossless)
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "WEBP_LEVEL is specified, but WEBP_LOSSLESS=YES. "
                 "WEBP_LEVEL will be ignored.");
    }
    poDS->m_nJpegQuality = GTiffGetJpegQuality(papszParamList);
    poDS->m_nJpegTablesMode = GTiffGetJpegTablesMode(papszParamList);
    poDS->m_dfMaxZError = GTiffGetLERCMaxZError(papszParamList);
#if HAVE_JXL
    poDS->m_bJXLLossless = GTiffGetJXLLossless(papszParamList);
    poDS->m_nJXLEffort = GTiffGetJXLEffort(papszParamList);
    poDS->m_fJXLDistance = GTiffGetJXLDistance(papszParamList);
#endif
    poDS->InitCreationOrOpenOptions(true, papszParamList);

    /* -------------------------------------------------------------------- */
    /*      Create band information objects.                                */
    /* -------------------------------------------------------------------- */
    for (int iBand = 0; iBand < l_nBands; ++iBand)
    {
        if (poDS->m_nBitsPerSample == 8 ||
            (poDS->m_nBitsPerSample == 16 && eType != GDT_Float32) ||
            poDS->m_nBitsPerSample == 32 || poDS->m_nBitsPerSample == 64 ||
            poDS->m_nBitsPerSample == 128)
        {
            poDS->SetBand(iBand + 1, new GTiffRasterBand(poDS, iBand + 1));
        }
        else
        {
            poDS->SetBand(iBand + 1, new GTiffOddBitsBand(poDS, iBand + 1));
            poDS->GetRasterBand(iBand + 1)->SetMetadataItem(
                "NBITS", CPLString().Printf("%d", poDS->m_nBitsPerSample),
                "IMAGE_STRUCTURE");
        }
    }

    poDS->GetDiscardLsbOption(papszParamList);

    if (poDS->m_nPlanarConfig == PLANARCONFIG_CONTIG && l_nBands != 1)
        poDS->SetMetadataItem("INTERLEAVE", "PIXEL", "IMAGE_STRUCTURE");
    else
        poDS->SetMetadataItem("INTERLEAVE", "BAND", "IMAGE_STRUCTURE");

    poDS->oOvManager.Initialize(poDS, pszFilename);

    return poDS;
}

/************************************************************************/
/*                           CopyImageryAndMask()                       */
/************************************************************************/

CPLErr GTiffDataset::CopyImageryAndMask(GTiffDataset *poDstDS,
                                        GDALDataset *poSrcDS,
                                        GDALRasterBand *poSrcMaskBand,
                                        GDALProgressFunc pfnProgress,
                                        void *pProgressData)
{
    CPLErr eErr = CE_None;

    const auto eType = poDstDS->GetRasterBand(1)->GetRasterDataType();
    const int nDataTypeSize = GDALGetDataTypeSizeBytes(eType);
    const int l_nBands = poDstDS->GetRasterCount();
    void *pBlockBuffer =
        VSI_MALLOC3_VERBOSE(poDstDS->m_nBlockXSize, poDstDS->m_nBlockYSize,
                            l_nBands * nDataTypeSize);
    if (pBlockBuffer == nullptr)
    {
        eErr = CE_Failure;
    }
    const int nYSize = poDstDS->nRasterYSize;
    const int nXSize = poDstDS->nRasterXSize;
    const int nYBlocks = DIV_ROUND_UP(nYSize, poDstDS->m_nBlockYSize);
    const int nXBlocks = DIV_ROUND_UP(nXSize, poDstDS->m_nBlockXSize);
    const int nBlocks = nXBlocks * nYBlocks;

    CPLAssert(l_nBands == 1 || poDstDS->m_nPlanarConfig == PLANARCONFIG_CONTIG);

    const bool bIsOddBand =
        dynamic_cast<GTiffOddBitsBand *>(poDstDS->GetRasterBand(1)) != nullptr;

    if (poDstDS->m_poMaskDS)
    {
        CPLAssert(poDstDS->m_poMaskDS->m_nBlockXSize == poDstDS->m_nBlockXSize);
        CPLAssert(poDstDS->m_poMaskDS->m_nBlockYSize == poDstDS->m_nBlockYSize);
    }

    int iBlock = 0;
    for (int iY = 0, nYBlock = 0; iY < nYSize && eErr == CE_None;
         iY = ((nYSize - iY < poDstDS->m_nBlockYSize)
                   ? nYSize
                   : iY + poDstDS->m_nBlockYSize),
             nYBlock++)
    {
        const int nReqYSize = std::min(nYSize - iY, poDstDS->m_nBlockYSize);
        for (int iX = 0, nXBlock = 0; iX < nXSize && eErr == CE_None;
             iX = ((nXSize - iX < poDstDS->m_nBlockXSize)
                       ? nXSize
                       : iX + poDstDS->m_nBlockXSize),
                 nXBlock++)
        {
            const int nReqXSize = std::min(nXSize - iX, poDstDS->m_nBlockXSize);
            if (nReqXSize < poDstDS->m_nBlockXSize ||
                nReqYSize < poDstDS->m_nBlockYSize)
            {
                memset(pBlockBuffer, 0,
                       static_cast<size_t>(poDstDS->m_nBlockXSize) *
                           poDstDS->m_nBlockYSize * l_nBands * nDataTypeSize);
            }

            if (!bIsOddBand)
            {
                eErr = poSrcDS->RasterIO(
                    GF_Read, iX, iY, nReqXSize, nReqYSize, pBlockBuffer,
                    nReqXSize, nReqYSize, eType, l_nBands, nullptr,
                    nDataTypeSize * l_nBands,
                    poDstDS->m_nBlockXSize * nDataTypeSize * l_nBands,
                    nDataTypeSize, nullptr);
                if (eErr == CE_None)
                {
                    eErr = poDstDS->WriteEncodedTileOrStrip(
                        iBlock, pBlockBuffer, false);
                }
            }
            else
            {
                // In the odd bit case, this is a bit messy to ensure
                // the strile gets written synchronously.
                // We load the content of the n-1 bands in the cache,
                // and for the last band we invoke WriteBlock() directly
                // We also force FlushBlockBuf()
                std::vector<GDALRasterBlock *> apoLockedBlocks;
                for (int i = 0; eErr == CE_None && i < l_nBands - 1; i++)
                {
                    auto poBlock =
                        poDstDS->GetRasterBand(i + 1)->GetLockedBlockRef(
                            nXBlock, nYBlock, TRUE);
                    if (poBlock)
                    {
                        eErr = poSrcDS->GetRasterBand(i + 1)->RasterIO(
                            GF_Read, iX, iY, nReqXSize, nReqYSize,
                            poBlock->GetDataRef(), nReqXSize, nReqYSize, eType,
                            nDataTypeSize,
                            nDataTypeSize * poDstDS->m_nBlockXSize, nullptr);
                        poBlock->MarkDirty();
                        apoLockedBlocks.emplace_back(poBlock);
                    }
                    else
                    {
                        eErr = CE_Failure;
                    }
                }
                if (eErr == CE_None)
                {
                    eErr = poSrcDS->GetRasterBand(l_nBands)->RasterIO(
                        GF_Read, iX, iY, nReqXSize, nReqYSize, pBlockBuffer,
                        nReqXSize, nReqYSize, eType, nDataTypeSize,
                        nDataTypeSize * poDstDS->m_nBlockXSize, nullptr);
                }
                if (eErr == CE_None)
                {
                    // Avoid any attempt to load from disk
                    poDstDS->m_nLoadedBlock = iBlock;
                    eErr = poDstDS->GetRasterBand(l_nBands)->WriteBlock(
                        nXBlock, nYBlock, pBlockBuffer);
                    if (eErr == CE_None)
                        eErr = poDstDS->FlushBlockBuf();
                }
                for (auto poBlock : apoLockedBlocks)
                {
                    poBlock->MarkClean();
                    poBlock->DropLock();
                }
            }

            if (eErr == CE_None && poDstDS->m_poMaskDS)
            {
                if (nReqXSize < poDstDS->m_nBlockXSize ||
                    nReqYSize < poDstDS->m_nBlockYSize)
                {
                    memset(pBlockBuffer, 0,
                           static_cast<size_t>(poDstDS->m_nBlockXSize) *
                               poDstDS->m_nBlockYSize);
                }
                eErr = poSrcMaskBand->RasterIO(
                    GF_Read, iX, iY, nReqXSize, nReqYSize, pBlockBuffer,
                    nReqXSize, nReqYSize, GDT_Byte, 1, poDstDS->m_nBlockXSize,
                    nullptr);
                if (eErr == CE_None)
                {
                    // Avoid any attempt to load from disk
                    poDstDS->m_poMaskDS->m_nLoadedBlock = iBlock;
                    eErr = poDstDS->m_poMaskDS->GetRasterBand(1)->WriteBlock(
                        nXBlock, nYBlock, pBlockBuffer);
                    if (eErr == CE_None)
                        eErr = poDstDS->m_poMaskDS->FlushBlockBuf();
                }
            }
            if (poDstDS->m_bWriteError)
                eErr = CE_Failure;

            iBlock++;
            if (pfnProgress &&
                !pfnProgress(static_cast<double>(iBlock) / nBlocks, nullptr,
                             pProgressData))
            {
                eErr = CE_Failure;
            }
        }
    }
    poDstDS->FlushCache(false);  // mostly to wait for thread completion
    VSIFree(pBlockBuffer);

    return eErr;
}

/************************************************************************/
/*                             CreateCopy()                             */
/************************************************************************/

GDALDataset *GTiffDataset::CreateCopy(const char *pszFilename,
                                      GDALDataset *poSrcDS, int bStrict,
                                      char **papszOptions,
                                      GDALProgressFunc pfnProgress,
                                      void *pProgressData)

{
    if (poSrcDS->GetRasterCount() == 0)
    {
        ReportError(pszFilename, CE_Failure, CPLE_AppDefined,
                    "Unable to export GeoTIFF files with zero bands.");
        return nullptr;
    }

    GDALRasterBand *const poPBand = poSrcDS->GetRasterBand(1);
    const GDALDataType eType = poPBand->GetRasterDataType();

    /* -------------------------------------------------------------------- */
    /*      Check, whether all bands in input dataset has the same type.    */
    /* -------------------------------------------------------------------- */
    const int l_nBands = poSrcDS->GetRasterCount();
    for (int iBand = 2; iBand <= l_nBands; ++iBand)
    {
        if (eType != poSrcDS->GetRasterBand(iBand)->GetRasterDataType())
        {
            if (bStrict)
            {
                ReportError(
                    pszFilename, CE_Failure, CPLE_AppDefined,
                    "Unable to export GeoTIFF file with different datatypes "
                    "per different bands. All bands should have the same "
                    "types in TIFF.");
                return nullptr;
            }
            else
            {
                ReportError(
                    pszFilename, CE_Warning, CPLE_AppDefined,
                    "Unable to export GeoTIFF file with different datatypes "
                    "per different bands. All bands should have the same "
                    "types in TIFF.");
            }
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Capture the profile.                                            */
    /* -------------------------------------------------------------------- */
    const GTiffProfile eProfile =
        GetProfile(CSLFetchNameValue(papszOptions, "PROFILE"));

    const bool bGeoTIFF = eProfile != GTiffProfile::BASELINE;

    /* -------------------------------------------------------------------- */
    /*      Special handling for NBITS.  Copy from band metadata if found.  */
    /* -------------------------------------------------------------------- */
    char **papszCreateOptions = CSLDuplicate(papszOptions);

    if (poPBand->GetMetadataItem("NBITS", "IMAGE_STRUCTURE") != nullptr &&
        atoi(poPBand->GetMetadataItem("NBITS", "IMAGE_STRUCTURE")) > 0 &&
        CSLFetchNameValue(papszCreateOptions, "NBITS") == nullptr)
    {
        papszCreateOptions = CSLSetNameValue(
            papszCreateOptions, "NBITS",
            poPBand->GetMetadataItem("NBITS", "IMAGE_STRUCTURE"));
    }

    if (CSLFetchNameValue(papszOptions, "PIXELTYPE") == nullptr &&
        eType == GDT_Byte &&
        poPBand->GetMetadataItem("PIXELTYPE", "IMAGE_STRUCTURE"))
    {
        papszCreateOptions = CSLSetNameValue(
            papszCreateOptions, "PIXELTYPE",
            poPBand->GetMetadataItem("PIXELTYPE", "IMAGE_STRUCTURE"));
    }

    /* -------------------------------------------------------------------- */
    /*      Color profile.  Copy from band metadata if found.              */
    /* -------------------------------------------------------------------- */
    if (bGeoTIFF)
    {
        const char *pszOptionsMD[] = {"SOURCE_ICC_PROFILE",
                                      "SOURCE_PRIMARIES_RED",
                                      "SOURCE_PRIMARIES_GREEN",
                                      "SOURCE_PRIMARIES_BLUE",
                                      "SOURCE_WHITEPOINT",
                                      "TIFFTAG_TRANSFERFUNCTION_RED",
                                      "TIFFTAG_TRANSFERFUNCTION_GREEN",
                                      "TIFFTAG_TRANSFERFUNCTION_BLUE",
                                      "TIFFTAG_TRANSFERRANGE_BLACK",
                                      "TIFFTAG_TRANSFERRANGE_WHITE",
                                      nullptr};

        // Copy all the tags.  Options will override tags in the source.
        int i = 0;
        while (pszOptionsMD[i] != nullptr)
        {
            char const *pszMD =
                CSLFetchNameValue(papszOptions, pszOptionsMD[i]);
            if (pszMD == nullptr)
                pszMD =
                    poSrcDS->GetMetadataItem(pszOptionsMD[i], "COLOR_PROFILE");

            if ((pszMD != nullptr) && !EQUAL(pszMD, ""))
            {
                papszCreateOptions =
                    CSLSetNameValue(papszCreateOptions, pszOptionsMD[i], pszMD);

                // If an ICC profile exists, other tags are not needed.
                if (EQUAL(pszOptionsMD[i], "SOURCE_ICC_PROFILE"))
                    break;
            }

            ++i;
        }
    }

    double dfExtraSpaceForOverviews = 0;
    const bool bCopySrcOverviews =
        CPLFetchBool(papszCreateOptions, "COPY_SRC_OVERVIEWS", false);
    std::unique_ptr<GDALDataset> poOvrDS;
    int nSrcOverviews = 0;
    if (bCopySrcOverviews)
    {
        const char *pszOvrDS =
            CSLFetchNameValue(papszCreateOptions, "@OVERVIEW_DATASET");
        if (pszOvrDS)
        {
            // Empty string is used by COG driver to indicate that we want
            // to ignore source overviews.
            if (!EQUAL(pszOvrDS, ""))
            {
                poOvrDS.reset(GDALDataset::Open(pszOvrDS));
                if (!poOvrDS)
                {
                    CSLDestroy(papszCreateOptions);
                    return nullptr;
                }
                if (poOvrDS->GetRasterCount() != l_nBands)
                {
                    CSLDestroy(papszCreateOptions);
                    return nullptr;
                }
                nSrcOverviews =
                    poOvrDS->GetRasterBand(1)->GetOverviewCount() + 1;
            }
        }
        else
        {
            nSrcOverviews = poSrcDS->GetRasterBand(1)->GetOverviewCount();
        }

        // Limit number of overviews if specified
        const char *pszOverviewCount =
            CSLFetchNameValue(papszCreateOptions, "@OVERVIEW_COUNT");
        if (pszOverviewCount)
            nSrcOverviews =
                std::max(0, std::min(nSrcOverviews, atoi(pszOverviewCount)));

        if (nSrcOverviews)
        {
            for (int j = 1; j <= l_nBands; ++j)
            {
                const int nOtherBandOverviewCount =
                    poOvrDS ? poOvrDS->GetRasterBand(j)->GetOverviewCount() + 1
                            : poSrcDS->GetRasterBand(j)->GetOverviewCount();
                if (nOtherBandOverviewCount < nSrcOverviews)
                {
                    ReportError(
                        pszFilename, CE_Failure, CPLE_NotSupported,
                        "COPY_SRC_OVERVIEWS cannot be used when the bands have "
                        "not the same number of overview levels.");
                    CSLDestroy(papszCreateOptions);
                    return nullptr;
                }
                for (int i = 0; i < nSrcOverviews; ++i)
                {
                    GDALRasterBand *poOvrBand =
                        poOvrDS
                            ? (i == 0 ? poOvrDS->GetRasterBand(j)
                                      : poOvrDS->GetRasterBand(j)->GetOverview(
                                            i - 1))
                            : poSrcDS->GetRasterBand(j)->GetOverview(i);
                    if (poOvrBand == nullptr)
                    {
                        ReportError(
                            pszFilename, CE_Failure, CPLE_NotSupported,
                            "COPY_SRC_OVERVIEWS cannot be used when one "
                            "overview band is NULL.");
                        CSLDestroy(papszCreateOptions);
                        return nullptr;
                    }
                    GDALRasterBand *poOvrFirstBand =
                        poOvrDS
                            ? (i == 0 ? poOvrDS->GetRasterBand(1)
                                      : poOvrDS->GetRasterBand(1)->GetOverview(
                                            i - 1))
                            : poSrcDS->GetRasterBand(1)->GetOverview(i);
                    if (poOvrBand->GetXSize() != poOvrFirstBand->GetXSize() ||
                        poOvrBand->GetYSize() != poOvrFirstBand->GetYSize())
                    {
                        ReportError(
                            pszFilename, CE_Failure, CPLE_NotSupported,
                            "COPY_SRC_OVERVIEWS cannot be used when the "
                            "overview bands have not the same dimensions "
                            "among bands.");
                        CSLDestroy(papszCreateOptions);
                        return nullptr;
                    }
                }
            }

            for (int i = 0; i < nSrcOverviews; ++i)
            {
                GDALRasterBand *poOvrFirstBand =
                    poOvrDS
                        ? (i == 0
                               ? poOvrDS->GetRasterBand(1)
                               : poOvrDS->GetRasterBand(1)->GetOverview(i - 1))
                        : poSrcDS->GetRasterBand(1)->GetOverview(i);
                dfExtraSpaceForOverviews +=
                    static_cast<double>(poOvrFirstBand->GetXSize()) *
                    poOvrFirstBand->GetYSize();
            }
            dfExtraSpaceForOverviews *=
                l_nBands * GDALGetDataTypeSizeBytes(eType);
        }
        else
        {
            CPLDebug("GTiff", "No source overviews to copy");
        }
    }

/* -------------------------------------------------------------------- */
/*      Should we use optimized way of copying from an input JPEG       */
/*      dataset?                                                        */
/* -------------------------------------------------------------------- */

// TODO(schwehr): Refactor bDirectCopyFromJPEG to be a const.
#if defined(HAVE_LIBJPEG) || defined(JPEG_DIRECT_COPY)
    bool bDirectCopyFromJPEG = false;
#endif

    // Note: JPEG_DIRECT_COPY is not defined by default, because it is mainly
    // useful for debugging purposes.
#ifdef JPEG_DIRECT_COPY
    if (CPLFetchBool(papszCreateOptions, "JPEG_DIRECT_COPY", false) &&
        GTIFF_CanDirectCopyFromJPEG(poSrcDS, papszCreateOptions))
    {
        CPLDebug("GTiff", "Using special direct copy mode from a JPEG dataset");

        bDirectCopyFromJPEG = true;
    }
#endif

#ifdef HAVE_LIBJPEG
    bool bCopyFromJPEG = false;

    // When CreateCopy'ing() from a JPEG dataset, and asking for COMPRESS=JPEG,
    // use DCT coefficients (unless other options are incompatible, like
    // strip/tile dimensions, specifying JPEG_QUALITY option, incompatible
    // PHOTOMETRIC with the source colorspace, etc.) to avoid the lossy steps
    // involved by decompression/recompression.
    if (!bDirectCopyFromJPEG &&
        GTIFF_CanCopyFromJPEG(poSrcDS, papszCreateOptions))
    {
        CPLDebug("GTiff", "Using special copy mode from a JPEG dataset");

        bCopyFromJPEG = true;
    }
#endif

    /* -------------------------------------------------------------------- */
    /*      If the source is RGB, then set the PHOTOMETRIC=RGB value        */
    /* -------------------------------------------------------------------- */

    const bool bForcePhotometric =
        CSLFetchNameValue(papszOptions, "PHOTOMETRIC") != nullptr;

    if (l_nBands >= 3 && !bForcePhotometric &&
#ifdef HAVE_LIBJPEG
        !bCopyFromJPEG &&
#endif
        poSrcDS->GetRasterBand(1)->GetColorInterpretation() == GCI_RedBand &&
        poSrcDS->GetRasterBand(2)->GetColorInterpretation() == GCI_GreenBand &&
        poSrcDS->GetRasterBand(3)->GetColorInterpretation() == GCI_BlueBand)
    {
        papszCreateOptions =
            CSLSetNameValue(papszCreateOptions, "PHOTOMETRIC", "RGB");
    }

    /* -------------------------------------------------------------------- */
    /*      Create the file.                                                */
    /* -------------------------------------------------------------------- */
    VSILFILE *l_fpL = nullptr;
    CPLString l_osTmpFilename;

    const int nXSize = poSrcDS->GetRasterXSize();
    const int nYSize = poSrcDS->GetRasterYSize();
    TIFF *l_hTIFF = CreateLL(pszFilename, nXSize, nYSize, l_nBands, eType,
                             dfExtraSpaceForOverviews, papszCreateOptions,
                             &l_fpL, l_osTmpFilename);
    const bool bStreaming = !l_osTmpFilename.empty();

    CSLDestroy(papszCreateOptions);
    papszCreateOptions = nullptr;

    if (l_hTIFF == nullptr)
    {
        if (bStreaming)
            VSIUnlink(l_osTmpFilename);
        return nullptr;
    }

    uint16_t l_nPlanarConfig = 0;
    TIFFGetField(l_hTIFF, TIFFTAG_PLANARCONFIG, &l_nPlanarConfig);

    uint16_t l_nCompression = 0;

    if (!TIFFGetField(l_hTIFF, TIFFTAG_COMPRESSION, &(l_nCompression)))
        l_nCompression = COMPRESSION_NONE;

    /* -------------------------------------------------------------------- */
    /*      Set the alpha channel if we find one.                           */
    /* -------------------------------------------------------------------- */
    uint16_t *extraSamples = nullptr;
    uint16_t nExtraSamples = 0;
    if (TIFFGetField(l_hTIFF, TIFFTAG_EXTRASAMPLES, &nExtraSamples,
                     &extraSamples) &&
        nExtraSamples > 0)
    {
        // We need to allocate a new array as (current) libtiff
        // versions will not like that we reuse the array we got from
        // TIFFGetField().
        uint16_t *pasNewExtraSamples = static_cast<uint16_t *>(
            CPLMalloc(nExtraSamples * sizeof(uint16_t)));
        memcpy(pasNewExtraSamples, extraSamples,
               nExtraSamples * sizeof(uint16_t));
        uint16_t nAlpha = GTiffGetAlphaValue(
            CPLGetConfigOption("GTIFF_ALPHA",
                               CSLFetchNameValue(papszOptions, "ALPHA")),
            DEFAULT_ALPHA_TYPE);
        const int nBaseSamples = l_nBands - nExtraSamples;
        for (int iExtraBand = nBaseSamples + 1; iExtraBand <= l_nBands;
             iExtraBand++)
        {
            if (poSrcDS->GetRasterBand(iExtraBand)->GetColorInterpretation() ==
                GCI_AlphaBand)
            {
                pasNewExtraSamples[iExtraBand - nBaseSamples - 1] = nAlpha;
            }
        }
        TIFFSetField(l_hTIFF, TIFFTAG_EXTRASAMPLES, nExtraSamples,
                     pasNewExtraSamples);

        CPLFree(pasNewExtraSamples);
    }

    /* -------------------------------------------------------------------- */
    /*      If the output is jpeg compressed, and the input is RGB make     */
    /*      sure we note that.                                              */
    /* -------------------------------------------------------------------- */

    if (l_nCompression == COMPRESSION_JPEG)
    {
        if (l_nBands >= 3 &&
            (poSrcDS->GetRasterBand(1)->GetColorInterpretation() ==
             GCI_YCbCr_YBand) &&
            (poSrcDS->GetRasterBand(2)->GetColorInterpretation() ==
             GCI_YCbCr_CbBand) &&
            (poSrcDS->GetRasterBand(3)->GetColorInterpretation() ==
             GCI_YCbCr_CrBand))
        {
            // Do nothing.
        }
        else
        {
            // Assume RGB if it is not explicitly YCbCr.
            CPLDebug("GTiff", "Setting JPEGCOLORMODE_RGB");
            TIFFSetField(l_hTIFF, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB);
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Does the source image consist of one band, with a palette?      */
    /*      If so, copy over.                                               */
    /* -------------------------------------------------------------------- */
    if ((l_nBands == 1 || l_nBands == 2) &&
        poSrcDS->GetRasterBand(1)->GetColorTable() != nullptr &&
        eType == GDT_Byte)
    {
        unsigned short anTRed[256] = {0};
        unsigned short anTGreen[256] = {0};
        unsigned short anTBlue[256] = {0};
        GDALColorTable *poCT = poSrcDS->GetRasterBand(1)->GetColorTable();

        for (int iColor = 0; iColor < 256; ++iColor)
        {
            if (iColor < poCT->GetColorEntryCount())
            {
                GDALColorEntry sRGB = {0, 0, 0, 0};

                poCT->GetColorEntryAsRGB(iColor, &sRGB);

                anTRed[iColor] = static_cast<unsigned short>(257 * sRGB.c1);
                anTGreen[iColor] = static_cast<unsigned short>(257 * sRGB.c2);
                anTBlue[iColor] = static_cast<unsigned short>(257 * sRGB.c3);
            }
            else
            {
                anTRed[iColor] = 0;
                anTGreen[iColor] = 0;
                anTBlue[iColor] = 0;
            }
        }

        if (!bForcePhotometric)
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_PALETTE);
        TIFFSetField(l_hTIFF, TIFFTAG_COLORMAP, anTRed, anTGreen, anTBlue);
    }
    else if ((l_nBands == 1 || l_nBands == 2) &&
             poSrcDS->GetRasterBand(1)->GetColorTable() != nullptr &&
             eType == GDT_UInt16)
    {
        unsigned short *panTRed = static_cast<unsigned short *>(
            CPLMalloc(65536 * sizeof(unsigned short)));
        unsigned short *panTGreen = static_cast<unsigned short *>(
            CPLMalloc(65536 * sizeof(unsigned short)));
        unsigned short *panTBlue = static_cast<unsigned short *>(
            CPLMalloc(65536 * sizeof(unsigned short)));

        GDALColorTable *poCT = poSrcDS->GetRasterBand(1)->GetColorTable();

        for (int iColor = 0; iColor < 65536; ++iColor)
        {
            if (iColor < poCT->GetColorEntryCount())
            {
                GDALColorEntry sRGB = {0, 0, 0, 0};

                poCT->GetColorEntryAsRGB(iColor, &sRGB);

                panTRed[iColor] = static_cast<unsigned short>(257 * sRGB.c1);
                panTGreen[iColor] = static_cast<unsigned short>(257 * sRGB.c2);
                panTBlue[iColor] = static_cast<unsigned short>(257 * sRGB.c3);
            }
            else
            {
                panTRed[iColor] = 0;
                panTGreen[iColor] = 0;
                panTBlue[iColor] = 0;
            }
        }

        if (!bForcePhotometric)
            TIFFSetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_PALETTE);
        TIFFSetField(l_hTIFF, TIFFTAG_COLORMAP, panTRed, panTGreen, panTBlue);

        CPLFree(panTRed);
        CPLFree(panTGreen);
        CPLFree(panTBlue);
    }
    else if (poSrcDS->GetRasterBand(1)->GetColorTable() != nullptr)
        ReportError(
            pszFilename, CE_Failure, CPLE_AppDefined,
            "Unable to export color table to GeoTIFF file.  Color tables "
            "can only be written to 1 band or 2 bands Byte or "
            "UInt16 GeoTIFF files.");

    if (l_nCompression == COMPRESSION_JPEG)
    {
        uint16_t l_nPhotometric = 0;
        TIFFGetField(l_hTIFF, TIFFTAG_PHOTOMETRIC, &l_nPhotometric);
        // Check done in tif_jpeg.c later, but not with a very clear error
        // message
        if (l_nPhotometric == PHOTOMETRIC_PALETTE)
        {
            ReportError(pszFilename, CE_Failure, CPLE_NotSupported,
                        "JPEG compression not supported with paletted image");
            XTIFFClose(l_hTIFF);
            VSIUnlink(l_osTmpFilename);
            CPL_IGNORE_RET_VAL(VSIFCloseL(l_fpL));
            return nullptr;
        }
    }

    if (l_nBands == 2 &&
        poSrcDS->GetRasterBand(1)->GetColorTable() != nullptr &&
        (eType == GDT_Byte || eType == GDT_UInt16))
    {
        uint16_t v[1] = {EXTRASAMPLE_UNASSALPHA};

        TIFFSetField(l_hTIFF, TIFFTAG_EXTRASAMPLES, 1, v);
    }

    const int nMaskFlags = poSrcDS->GetRasterBand(1)->GetMaskFlags();
    bool bCreateMask = false;
    CPLString osHiddenStructuralMD;
    if ((l_nBands == 1 || l_nPlanarConfig == PLANARCONFIG_CONTIG) &&
        bCopySrcOverviews)
    {
        osHiddenStructuralMD += "LAYOUT=IFDS_BEFORE_DATA\n";
        osHiddenStructuralMD += "BLOCK_ORDER=ROW_MAJOR\n";
        osHiddenStructuralMD += "BLOCK_LEADER=SIZE_AS_UINT4\n";
        osHiddenStructuralMD += "BLOCK_TRAILER=LAST_4_BYTES_REPEATED\n";
        osHiddenStructuralMD +=
            "KNOWN_INCOMPATIBLE_EDITION=NO\n ";  // Final space intended, so
                                                 // this can be replaced by YES
    }
    if (!(nMaskFlags & (GMF_ALL_VALID | GMF_ALPHA | GMF_NODATA)) &&
        (nMaskFlags & GMF_PER_DATASET) && !bStreaming)
    {
        bCreateMask = true;
        if (GTiffDataset::MustCreateInternalMask() &&
            !osHiddenStructuralMD.empty())
        {
            osHiddenStructuralMD += "MASK_INTERLEAVED_WITH_IMAGERY=YES\n";
        }
    }
    if (!osHiddenStructuralMD.empty())
    {
        const int nHiddenMDSize = static_cast<int>(osHiddenStructuralMD.size());
        osHiddenStructuralMD =
            CPLOPrintf("GDAL_STRUCTURAL_METADATA_SIZE=%06d bytes\n",
                       nHiddenMDSize) +
            osHiddenStructuralMD;
        VSI_TIFFWrite(l_hTIFF, osHiddenStructuralMD.c_str(),
                      osHiddenStructuralMD.size());
    }

    // FIXME? libtiff writes extended tags in the order they are specified
    // and not in increasing order.

    /* -------------------------------------------------------------------- */
    /*      Transfer some TIFF specific metadata, if available.             */
    /*      The return value will tell us if we need to try again later with*/
    /*      PAM because the profile doesn't allow to write some metadata    */
    /*      as TIFF tag                                                     */
    /* -------------------------------------------------------------------- */
    const bool bHasWrittenMDInGeotiffTAG = GTiffDataset::WriteMetadata(
        poSrcDS, l_hTIFF, false, eProfile, pszFilename, papszOptions);

    /* -------------------------------------------------------------------- */
    /*      Write NoData value, if exist.                                   */
    /* -------------------------------------------------------------------- */
    if (eProfile == GTiffProfile::GDALGEOTIFF)
    {
        int bSuccess = FALSE;
        GDALRasterBand *poFirstBand = poSrcDS->GetRasterBand(1);
        if (poFirstBand->GetRasterDataType() == GDT_Int64)
        {
            const auto nNoData = poFirstBand->GetNoDataValueAsInt64(&bSuccess);
            if (bSuccess)
                GTiffDataset::WriteNoDataValue(l_hTIFF, nNoData);
        }
        else if (poFirstBand->GetRasterDataType() == GDT_UInt64)
        {
            const auto nNoData = poFirstBand->GetNoDataValueAsUInt64(&bSuccess);
            if (bSuccess)
                GTiffDataset::WriteNoDataValue(l_hTIFF, nNoData);
        }
        else
        {
            const auto dfNoData = poFirstBand->GetNoDataValue(&bSuccess);
            if (bSuccess)
                GTiffDataset::WriteNoDataValue(l_hTIFF, dfNoData);
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Are we addressing PixelIsPoint mode?                            */
    /* -------------------------------------------------------------------- */
    bool bPixelIsPoint = false;
    bool bPointGeoIgnore = false;

    if (poSrcDS->GetMetadataItem(GDALMD_AREA_OR_POINT) &&
        EQUAL(poSrcDS->GetMetadataItem(GDALMD_AREA_OR_POINT), GDALMD_AOP_POINT))
    {
        bPixelIsPoint = true;
        bPointGeoIgnore =
            CPLTestBool(CPLGetConfigOption("GTIFF_POINT_GEO_IGNORE", "FALSE"));
    }

    /* -------------------------------------------------------------------- */
    /*      Write affine transform if it is meaningful.                     */
    /* -------------------------------------------------------------------- */
    const OGRSpatialReference *l_poSRS = nullptr;
    double l_adfGeoTransform[6] = {0.0};

    if (poSrcDS->GetGeoTransform(l_adfGeoTransform) == CE_None)
    {
        if (bGeoTIFF)
        {
            l_poSRS = poSrcDS->GetSpatialRef();

            if (l_adfGeoTransform[2] == 0.0 && l_adfGeoTransform[4] == 0.0 &&
                l_adfGeoTransform[5] < 0.0)
            {
                double dfOffset = 0.0;
                {
                    // In the case the SRS has a vertical component and we have
                    // a single band, encode its scale/offset in the GeoTIFF
                    // tags
                    int bHasScale = FALSE;
                    double dfScale =
                        poSrcDS->GetRasterBand(1)->GetScale(&bHasScale);
                    int bHasOffset = FALSE;
                    dfOffset =
                        poSrcDS->GetRasterBand(1)->GetOffset(&bHasOffset);
                    const bool bApplyScaleOffset =
                        l_poSRS && l_poSRS->IsVertical() &&
                        poSrcDS->GetRasterCount() == 1;
                    if (bApplyScaleOffset && !bHasScale)
                        dfScale = 1.0;
                    if (!bApplyScaleOffset || !bHasOffset)
                        dfOffset = 0.0;
                    const double adfPixelScale[3] = {
                        l_adfGeoTransform[1], fabs(l_adfGeoTransform[5]),
                        bApplyScaleOffset ? dfScale : 0.0};

                    TIFFSetField(l_hTIFF, TIFFTAG_GEOPIXELSCALE, 3,
                                 adfPixelScale);
                }

                double adfTiePoints[6] = {0.0,
                                          0.0,
                                          0.0,
                                          l_adfGeoTransform[0],
                                          l_adfGeoTransform[3],
                                          dfOffset};

                if (bPixelIsPoint && !bPointGeoIgnore)
                {
                    adfTiePoints[3] +=
                        l_adfGeoTransform[1] * 0.5 + l_adfGeoTransform[2] * 0.5;
                    adfTiePoints[4] +=
                        l_adfGeoTransform[4] * 0.5 + l_adfGeoTransform[5] * 0.5;
                }

                TIFFSetField(l_hTIFF, TIFFTAG_GEOTIEPOINTS, 6, adfTiePoints);
            }
            else
            {
                double adfMatrix[16] = {0.0};

                adfMatrix[0] = l_adfGeoTransform[1];
                adfMatrix[1] = l_adfGeoTransform[2];
                adfMatrix[3] = l_adfGeoTransform[0];
                adfMatrix[4] = l_adfGeoTransform[4];
                adfMatrix[5] = l_adfGeoTransform[5];
                adfMatrix[7] = l_adfGeoTransform[3];
                adfMatrix[15] = 1.0;

                if (bPixelIsPoint && !bPointGeoIgnore)
                {
                    adfMatrix[3] +=
                        l_adfGeoTransform[1] * 0.5 + l_adfGeoTransform[2] * 0.5;
                    adfMatrix[7] +=
                        l_adfGeoTransform[4] * 0.5 + l_adfGeoTransform[5] * 0.5;
                }

                TIFFSetField(l_hTIFF, TIFFTAG_GEOTRANSMATRIX, 16, adfMatrix);
            }
        }

        /* --------------------------------------------------------------------
         */
        /*      Do we need a TFW file? */
        /* --------------------------------------------------------------------
         */
        if (CPLFetchBool(papszOptions, "TFW", false))
            GDALWriteWorldFile(pszFilename, "tfw", l_adfGeoTransform);
        else if (CPLFetchBool(papszOptions, "WORLDFILE", false))
            GDALWriteWorldFile(pszFilename, "wld", l_adfGeoTransform);
    }

    /* -------------------------------------------------------------------- */
    /*      Otherwise write tiepoints if they are available.                */
    /* -------------------------------------------------------------------- */
    else if (poSrcDS->GetGCPCount() > 0 && bGeoTIFF)
    {
        const GDAL_GCP *pasGCPs = poSrcDS->GetGCPs();
        double *padfTiePoints = static_cast<double *>(
            CPLMalloc(6 * sizeof(double) * poSrcDS->GetGCPCount()));

        for (int iGCP = 0; iGCP < poSrcDS->GetGCPCount(); ++iGCP)
        {

            padfTiePoints[iGCP * 6 + 0] = pasGCPs[iGCP].dfGCPPixel;
            padfTiePoints[iGCP * 6 + 1] = pasGCPs[iGCP].dfGCPLine;
            padfTiePoints[iGCP * 6 + 2] = 0;
            padfTiePoints[iGCP * 6 + 3] = pasGCPs[iGCP].dfGCPX;
            padfTiePoints[iGCP * 6 + 4] = pasGCPs[iGCP].dfGCPY;
            padfTiePoints[iGCP * 6 + 5] = pasGCPs[iGCP].dfGCPZ;

            if (bPixelIsPoint && !bPointGeoIgnore)
            {
                padfTiePoints[iGCP * 6 + 0] -= 0.5;
                padfTiePoints[iGCP * 6 + 1] -= 0.5;
            }
        }

        TIFFSetField(l_hTIFF, TIFFTAG_GEOTIEPOINTS, 6 * poSrcDS->GetGCPCount(),
                     padfTiePoints);
        CPLFree(padfTiePoints);

        l_poSRS = poSrcDS->GetGCPSpatialRef();

        if (CPLFetchBool(papszOptions, "TFW", false) ||
            CPLFetchBool(papszOptions, "WORLDFILE", false))
        {
            ReportError(
                pszFilename, CE_Warning, CPLE_AppDefined,
                "TFW=ON or WORLDFILE=ON creation options are ignored when "
                "GCPs are available");
        }
    }
    else
    {
        l_poSRS = poSrcDS->GetSpatialRef();
    }

    /* -------------------------------------------------------------------- */
    /*      Copy xml:XMP data                                               */
    /* -------------------------------------------------------------------- */
    char **papszXMP = poSrcDS->GetMetadata("xml:XMP");
    if (papszXMP != nullptr && *papszXMP != nullptr)
    {
        int nTagSize = static_cast<int>(strlen(*papszXMP));
        TIFFSetField(l_hTIFF, TIFFTAG_XMLPACKET, nTagSize, *papszXMP);
    }

    /* -------------------------------------------------------------------- */
    /*      Write the projection information, if possible.                  */
    /* -------------------------------------------------------------------- */
    const bool bHasProjection = l_poSRS != nullptr;
    bool bExportSRSToPAM = false;
    if ((bHasProjection || bPixelIsPoint) && bGeoTIFF)
    {
        GTIF *psGTIF = GTiffDatasetGTIFNew(l_hTIFF);

        if (bHasProjection)
        {
            if (IsSRSCompatibleOfGeoTIFF(l_poSRS))
            {
                GTIFSetFromOGISDefnEx(
                    psGTIF,
                    OGRSpatialReference::ToHandle(
                        const_cast<OGRSpatialReference *>(l_poSRS)),
                    GetGTIFFKeysFlavor(papszOptions),
                    GetGeoTIFFVersion(papszOptions));
            }
            else
            {
                bExportSRSToPAM = true;
            }
        }

        if (bPixelIsPoint)
        {
            GTIFKeySet(psGTIF, GTRasterTypeGeoKey, TYPE_SHORT, 1,
                       RasterPixelIsPoint);
        }

        GTIFWriteKeys(psGTIF);
        GTIFFree(psGTIF);
    }

    bool l_bDontReloadFirstBlock = false;

#ifdef HAVE_LIBJPEG
    if (bCopyFromJPEG)
    {
        GTIFF_CopyFromJPEG_WriteAdditionalTags(l_hTIFF, poSrcDS);
    }
#endif

/* -------------------------------------------------------------------- */
/*      Cleanup                                                         */
/* -------------------------------------------------------------------- */
#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
    if (bCopySrcOverviews)
    {
        TIFFDeferStrileArrayWriting(l_hTIFF);
    }
#endif
    TIFFWriteCheck(l_hTIFF, TIFFIsTiled(l_hTIFF), "GTiffCreateCopy()");
    TIFFWriteDirectory(l_hTIFF);
    if (bStreaming)
    {
        // We need to write twice the directory to be sure that custom
        // TIFF tags are correctly sorted and that padding bytes have been
        // added.
        TIFFSetDirectory(l_hTIFF, 0);
        TIFFWriteDirectory(l_hTIFF);

        if (VSIFSeekL(l_fpL, 0, SEEK_END) != 0)
            ReportError(pszFilename, CE_Failure, CPLE_FileIO, "Cannot seek");
        const int nSize = static_cast<int>(VSIFTellL(l_fpL));

        vsi_l_offset nDataLength = 0;
        VSIGetMemFileBuffer(l_osTmpFilename, &nDataLength, FALSE);
        TIFFSetDirectory(l_hTIFF, 0);
        GTiffFillStreamableOffsetAndCount(l_hTIFF, nSize);
        TIFFWriteDirectory(l_hTIFF);
    }
    TIFFSetDirectory(l_hTIFF,
                     static_cast<tdir_t>(TIFFNumberOfDirectories(l_hTIFF) - 1));
    const toff_t l_nDirOffset = TIFFCurrentDirOffset(l_hTIFF);
    TIFFFlush(l_hTIFF);
    XTIFFClose(l_hTIFF);

    VSIFSeekL(l_fpL, 0, SEEK_SET);

    // fpStreaming will assigned to the instance and not closed here.
    VSILFILE *fpStreaming = nullptr;
    if (bStreaming)
    {
        vsi_l_offset nDataLength = 0;
        void *pabyBuffer =
            VSIGetMemFileBuffer(l_osTmpFilename, &nDataLength, FALSE);
        fpStreaming = VSIFOpenL(pszFilename, "wb");
        if (fpStreaming == nullptr)
        {
            VSIUnlink(l_osTmpFilename);
            CPL_IGNORE_RET_VAL(VSIFCloseL(l_fpL));
            return nullptr;
        }
        if (static_cast<vsi_l_offset>(VSIFWriteL(pabyBuffer, 1,
                                                 static_cast<int>(nDataLength),
                                                 fpStreaming)) != nDataLength)
        {
            ReportError(pszFilename, CE_Failure, CPLE_FileIO,
                        "Could not write %d bytes",
                        static_cast<int>(nDataLength));
            CPL_IGNORE_RET_VAL(VSIFCloseL(fpStreaming));
            VSIUnlink(l_osTmpFilename);
            CPL_IGNORE_RET_VAL(VSIFCloseL(l_fpL));
            return nullptr;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Re-open as a dataset and copy over missing metadata using       */
    /*      PAM facilities.                                                 */
    /* -------------------------------------------------------------------- */
    l_hTIFF = VSI_TIFFOpen(bStreaming ? l_osTmpFilename.c_str() : pszFilename,
                           "r+", l_fpL);
    if (l_hTIFF == nullptr)
    {
        if (bStreaming)
            VSIUnlink(l_osTmpFilename);
        CPL_IGNORE_RET_VAL(VSIFCloseL(l_fpL));
        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Create a corresponding GDALDataset.                             */
    /* -------------------------------------------------------------------- */
    GTiffDataset *poDS = new GTiffDataset();
    poDS->SetDescription(pszFilename);
    poDS->eAccess = GA_Update;
    poDS->m_pszFilename = CPLStrdup(pszFilename);
    poDS->m_fpL = l_fpL;
    poDS->m_bIMDRPCMetadataLoaded = true;

    const bool bAppend = CPLFetchBool(papszOptions, "APPEND_SUBDATASET", false);
    if (poDS->OpenOffset(l_hTIFF,
                         bAppend ? l_nDirOffset : TIFFCurrentDirOffset(l_hTIFF),
                         GA_Update,
                         false,  // bAllowRGBAInterface
                         true    // bReadGeoTransform
                         ) != CE_None)
    {
        delete poDS;
        if (bStreaming)
            VSIUnlink(l_osTmpFilename);
        return nullptr;
    }
    poDS->oOvManager.Initialize(poDS, pszFilename);

    if (bStreaming)
    {
        VSIUnlink(l_osTmpFilename);
        poDS->m_fpToWrite = fpStreaming;
    }
    poDS->m_eProfile = eProfile;

    int nCloneInfoFlags = GCIF_PAM_DEFAULT & ~GCIF_MASK;

    // If we explicitly asked not to tag the alpha band as such, do not
    // reintroduce this alpha color interpretation in PAM.
    if (poSrcDS->GetRasterBand(l_nBands)->GetColorInterpretation() ==
            GCI_AlphaBand &&
        GTiffGetAlphaValue(
            CPLGetConfigOption("GTIFF_ALPHA",
                               CSLFetchNameValue(papszOptions, "ALPHA")),
            DEFAULT_ALPHA_TYPE) == EXTRASAMPLE_UNSPECIFIED)
    {
        nCloneInfoFlags &= ~GCIF_COLORINTERP;
    }
    // Ignore source band color interpretation if requesting PHOTOMETRIC=RGB
    else if (l_nBands >= 3 &&
             EQUAL(CSLFetchNameValueDef(papszOptions, "PHOTOMETRIC", ""),
                   "RGB"))
    {
        for (int i = 1; i <= 3; i++)
        {
            poDS->GetRasterBand(i)->SetColorInterpretation(
                static_cast<GDALColorInterp>(GCI_RedBand + (i - 1)));
        }
        nCloneInfoFlags &= ~GCIF_COLORINTERP;
        if (!(l_nBands == 4 &&
              CSLFetchNameValue(papszOptions, "ALPHA") != nullptr))
        {
            for (int i = 4; i <= l_nBands; i++)
            {
                poDS->GetRasterBand(i)->SetColorInterpretation(
                    poSrcDS->GetRasterBand(i)->GetColorInterpretation());
            }
        }
    }

    CPLString osOldGTIFF_REPORT_COMPD_CSVal(
        CPLGetConfigOption("GTIFF_REPORT_COMPD_CS", ""));
    CPLSetThreadLocalConfigOption("GTIFF_REPORT_COMPD_CS", "YES");
    poDS->CloneInfo(poSrcDS, nCloneInfoFlags);
    CPLSetThreadLocalConfigOption("GTIFF_REPORT_COMPD_CS",
                                  osOldGTIFF_REPORT_COMPD_CSVal.empty()
                                      ? nullptr
                                      : osOldGTIFF_REPORT_COMPD_CSVal.c_str());

    if ((!bGeoTIFF || bExportSRSToPAM) &&
        (poDS->GetPamFlags() & GPF_DISABLED) == 0)
    {
        // Copy georeferencing info to PAM if the profile is not GeoTIFF
        poDS->GDALPamDataset::SetSpatialRef(poDS->GetSpatialRef());
        double adfGeoTransform[6];
        if (poDS->GetGeoTransform(adfGeoTransform) == CE_None)
        {
            poDS->GDALPamDataset::SetGeoTransform(adfGeoTransform);
        }
        poDS->GDALPamDataset::SetGCPs(poDS->GetGCPCount(), poDS->GetGCPs(),
                                      poDS->GetGCPSpatialRef());
    }

    poDS->m_papszCreationOptions = CSLDuplicate(papszOptions);
    poDS->m_bDontReloadFirstBlock = l_bDontReloadFirstBlock;

    /* -------------------------------------------------------------------- */
    /*      CloneInfo() does not merge metadata, it just replaces it        */
    /*      totally.  So we have to merge it.                               */
    /* -------------------------------------------------------------------- */

    char **papszSRC_MD = poSrcDS->GetMetadata();
    char **papszDST_MD = CSLDuplicate(poDS->GetMetadata());

    papszDST_MD = CSLMerge(papszDST_MD, papszSRC_MD);

    poDS->SetMetadata(papszDST_MD);
    CSLDestroy(papszDST_MD);

    // Depending on the PHOTOMETRIC tag, the TIFF file may not have the same
    // band count as the source. Will fail later in GDALDatasetCopyWholeRaster
    // anyway.
    for (int nBand = 1;
         nBand <= std::min(poDS->GetRasterCount(), poSrcDS->GetRasterCount());
         ++nBand)
    {
        GDALRasterBand *poSrcBand = poSrcDS->GetRasterBand(nBand);
        GDALRasterBand *poDstBand = poDS->GetRasterBand(nBand);
        papszSRC_MD = poSrcBand->GetMetadata();
        papszDST_MD = CSLDuplicate(poDstBand->GetMetadata());

        papszDST_MD = CSLMerge(papszDST_MD, papszSRC_MD);

        poDstBand->SetMetadata(papszDST_MD);
        CSLDestroy(papszDST_MD);

        char **papszCatNames = poSrcBand->GetCategoryNames();
        if (nullptr != papszCatNames)
            poDstBand->SetCategoryNames(papszCatNames);
    }

    l_hTIFF = static_cast<TIFF *>(poDS->GetInternalHandle(nullptr));

    /* -------------------------------------------------------------------- */
    /*      Handle forcing xml:ESRI data to be written to PAM.              */
    /* -------------------------------------------------------------------- */
    if (CPLTestBool(CPLGetConfigOption("ESRI_XML_PAM", "NO")))
    {
        char **papszESRIMD = poSrcDS->GetMetadata("xml:ESRI");
        if (papszESRIMD)
        {
            poDS->SetMetadata(papszESRIMD, "xml:ESRI");
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Second chance: now that we have a PAM dataset, it is possible   */
    /*      to write metadata that we could not write as a TIFF tag.        */
    /* -------------------------------------------------------------------- */
    if (!bHasWrittenMDInGeotiffTAG && !bStreaming)
    {
        GTiffDataset::WriteMetadata(
            poDS, l_hTIFF, true, eProfile, pszFilename, papszOptions,
            true /* don't write RPC and IMD file again */);
    }

    if (!bStreaming)
        GTiffDataset::WriteRPC(poDS, l_hTIFF, true, eProfile, pszFilename,
                               papszOptions,
                               true /* write only in PAM AND if needed */);

    // Propagate ISIS3 or VICAR metadata, but only as PAM metadata.
    for (const char *pszMDD : {"json:ISIS3", "json:VICAR"})
    {
        char **papszMD = poSrcDS->GetMetadata(pszMDD);
        if (papszMD)
        {
            poDS->SetMetadata(papszMD, pszMDD);
            poDS->PushMetadataToPam();
        }
    }

    poDS->m_bWriteCOGLayout = bCopySrcOverviews;

    // To avoid unnecessary directory rewriting.
    poDS->m_bMetadataChanged = false;
    poDS->m_bGeoTIFFInfoChanged = false;
    poDS->m_bNoDataChanged = false;
    poDS->m_bForceUnsetGTOrGCPs = false;
    poDS->m_bForceUnsetProjection = false;
    poDS->m_bStreamingOut = bStreaming;

    // Don't try to load external metadata files (#6597).
    poDS->m_bIMDRPCMetadataLoaded = true;

    // We must re-set the compression level at this point, since it has been
    // lost a few lines above when closing the newly create TIFF file The
    // TIFFTAG_ZIPQUALITY & TIFFTAG_JPEGQUALITY are not store in the TIFF file.
    // They are just TIFF session parameters.

    poDS->m_nZLevel = GTiffGetZLevel(papszOptions);
    poDS->m_nLZMAPreset = GTiffGetLZMAPreset(papszOptions);
    poDS->m_nZSTDLevel = GTiffGetZSTDPreset(papszOptions);
    poDS->m_nWebPLevel = GTiffGetWebPLevel(papszOptions);
    poDS->m_bWebPLossless = GTiffGetWebPLossless(papszOptions);
    if (poDS->m_nWebPLevel != 100 && poDS->m_bWebPLossless)
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "WEBP_LEVEL is specified, but WEBP_LOSSLESS=YES. "
                 "WEBP_LEVEL will be ignored.");
    }
    poDS->m_nJpegQuality = GTiffGetJpegQuality(papszOptions);
    poDS->m_nJpegTablesMode = GTiffGetJpegTablesMode(papszOptions);
    poDS->GetDiscardLsbOption(papszOptions);
    poDS->m_dfMaxZError = GTiffGetLERCMaxZError(papszOptions);
#if HAVE_JXL
    poDS->m_bJXLLossless = GTiffGetJXLLossless(papszOptions);
    poDS->m_nJXLEffort = GTiffGetJXLEffort(papszOptions);
    poDS->m_fJXLDistance = GTiffGetJXLDistance(papszOptions);
#endif
    poDS->InitCreationOrOpenOptions(true, papszOptions);

    if (l_nCompression == COMPRESSION_ADOBE_DEFLATE ||
        l_nCompression == COMPRESSION_LERC)
    {
        GTiffSetDeflateSubCodec(l_hTIFF);

        if (poDS->m_nZLevel != -1)
        {
            TIFFSetField(l_hTIFF, TIFFTAG_ZIPQUALITY, poDS->m_nZLevel);
        }
    }
    if (l_nCompression == COMPRESSION_JPEG)
    {
        if (poDS->m_nJpegQuality != -1)
        {
            TIFFSetField(l_hTIFF, TIFFTAG_JPEGQUALITY, poDS->m_nJpegQuality);
        }
        TIFFSetField(l_hTIFF, TIFFTAG_JPEGTABLESMODE, poDS->m_nJpegTablesMode);
    }
    if (l_nCompression == COMPRESSION_LZMA)
    {
        if (poDS->m_nLZMAPreset != -1)
        {
            TIFFSetField(l_hTIFF, TIFFTAG_LZMAPRESET, poDS->m_nLZMAPreset);
        }
    }
    if (l_nCompression == COMPRESSION_ZSTD ||
        l_nCompression == COMPRESSION_LERC)
    {
        if (poDS->m_nZSTDLevel != -1)
        {
            TIFFSetField(l_hTIFF, TIFFTAG_ZSTD_LEVEL, poDS->m_nZSTDLevel);
        }
    }
    if (l_nCompression == COMPRESSION_LERC)
    {
        TIFFSetField(l_hTIFF, TIFFTAG_LERC_MAXZERROR, poDS->m_dfMaxZError);
    }
#if HAVE_JXL
    if (l_nCompression == COMPRESSION_JXL)
    {
        TIFFSetField(l_hTIFF, TIFFTAG_JXL_LOSSYNESS,
                     poDS->m_bJXLLossless ? JXL_LOSSLESS : JXL_LOSSY);
        TIFFSetField(l_hTIFF, TIFFTAG_JXL_EFFORT, poDS->m_nJXLEffort);
        TIFFSetField(l_hTIFF, TIFFTAG_JXL_DISTANCE, poDS->m_fJXLDistance);
    }
#endif
    if (l_nCompression == COMPRESSION_WEBP)
    {
        if (poDS->m_nWebPLevel != -1)
        {
            TIFFSetField(l_hTIFF, TIFFTAG_WEBP_LEVEL, poDS->m_nWebPLevel);
        }

        if (poDS->m_bWebPLossless)
        {
            TIFFSetField(l_hTIFF, TIFFTAG_WEBP_LOSSLESS, poDS->m_bWebPLossless);
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Do we want to ensure all blocks get written out on close to     */
    /*      avoid sparse files?                                             */
    /* -------------------------------------------------------------------- */
    if (!CPLFetchBool(papszOptions, "SPARSE_OK", false))
        poDS->m_bFillEmptyTilesAtClosing = true;

    poDS->m_bWriteEmptyTiles =
        (bCopySrcOverviews && poDS->m_bFillEmptyTilesAtClosing) || bStreaming ||
        (poDS->m_nCompression != COMPRESSION_NONE &&
         poDS->m_bFillEmptyTilesAtClosing);
    // Only required for people writing non-compressed striped files in the
    // rightorder and wanting all tstrips to be written in the same order
    // so that the end result can be memory mapped without knowledge of each
    // strip offset
    if (CPLTestBool(CSLFetchNameValueDef(
            papszOptions, "WRITE_EMPTY_TILES_SYNCHRONOUSLY", "FALSE")) ||
        CPLTestBool(CSLFetchNameValueDef(
            papszOptions, "@WRITE_EMPTY_TILES_SYNCHRONOUSLY", "FALSE")))
    {
        poDS->m_bWriteEmptyTiles = true;
    }

    // Precreate (internal) mask, so that the IBuildOverviews() below
    // has a chance to create also the overviews of the mask.
    CPLErr eErr = CE_None;

    if (bCreateMask)
    {
        eErr = poDS->CreateMaskBand(nMaskFlags);
        if (poDS->m_poMaskDS)
        {
            poDS->m_poMaskDS->m_bFillEmptyTilesAtClosing =
                poDS->m_bFillEmptyTilesAtClosing;
            poDS->m_poMaskDS->m_bWriteEmptyTiles = poDS->m_bWriteEmptyTiles;
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Create and then copy existing overviews if requested            */
    /*  We do it such that all the IFDs are at the beginning of the file,   */
    /*  and that the imagery data for the smallest overview is written      */
    /*  first, that way the file is more usable when embedded in a          */
    /*  compressed stream.                                                  */
    /* -------------------------------------------------------------------- */

    // For scaled progress due to overview copying.
    const int nBandsWidthMask = l_nBands + (bCreateMask ? 1 : 0);
    double dfTotalPixels =
        static_cast<double>(nXSize) * nYSize * nBandsWidthMask;
    double dfCurPixels = 0;

    if (eErr == CE_None && bCopySrcOverviews)
    {
        std::unique_ptr<GDALDataset> poMaskOvrDS;
        const char *pszMaskOvrDS =
            CSLFetchNameValue(papszOptions, "@MASK_OVERVIEW_DATASET");
        if (pszMaskOvrDS)
        {
            poMaskOvrDS.reset(GDALDataset::Open(pszMaskOvrDS));
            if (!poMaskOvrDS)
            {
                delete poDS;
                return nullptr;
            }
            if (poMaskOvrDS->GetRasterCount() != 1)
            {
                delete poDS;
                return nullptr;
            }
        }
        if (nSrcOverviews)
        {
            eErr = poDS->CreateOverviewsFromSrcOverviews(poSrcDS, poOvrDS.get(),
                                                         nSrcOverviews);

            if (eErr == CE_None &&
                (poMaskOvrDS != nullptr ||
                 (poSrcDS->GetRasterBand(1)->GetOverview(0) &&
                  poSrcDS->GetRasterBand(1)->GetOverview(0)->GetMaskFlags() ==
                      GMF_PER_DATASET)))
            {
                int nOvrBlockXSize = 0;
                int nOvrBlockYSize = 0;
                GTIFFGetOverviewBlockSize(
                    GDALRasterBand::ToHandle(poDS->GetRasterBand(1)),
                    &nOvrBlockXSize, &nOvrBlockYSize);
                eErr = poDS->CreateInternalMaskOverviews(nOvrBlockXSize,
                                                         nOvrBlockYSize);
            }
        }

#ifdef SUPPORTS_GET_OFFSET_BYTECOUNT
        TIFFForceStrileArrayWriting(poDS->m_hTIFF);

        if (poDS->m_poMaskDS)
        {
            TIFFForceStrileArrayWriting(poDS->m_poMaskDS->m_hTIFF);
        }

        for (int i = 0; i < poDS->m_nOverviewCount; i++)
        {
            TIFFForceStrileArrayWriting(poDS->m_papoOverviewDS[i]->m_hTIFF);

            if (poDS->m_papoOverviewDS[i]->m_poMaskDS)
            {
                TIFFForceStrileArrayWriting(
                    poDS->m_papoOverviewDS[i]->m_poMaskDS->m_hTIFF);
            }
        }
#endif

        if (eErr == CE_None && nSrcOverviews)
        {
            if (poDS->m_nOverviewCount != nSrcOverviews)
            {
                ReportError(
                    pszFilename, CE_Failure, CPLE_AppDefined,
                    "Did only manage to instantiate %d overview levels, "
                    "whereas source contains %d",
                    poDS->m_nOverviewCount, nSrcOverviews);
                eErr = CE_Failure;
            }

            for (int i = 0; eErr == CE_None && i < nSrcOverviews; ++i)
            {
                GDALRasterBand *poOvrBand =
                    poOvrDS
                        ? (i == 0
                               ? poOvrDS->GetRasterBand(1)
                               : poOvrDS->GetRasterBand(1)->GetOverview(i - 1))
                        : poSrcDS->GetRasterBand(1)->GetOverview(i);
                const double dfOvrPixels =
                    static_cast<double>(poOvrBand->GetXSize()) *
                    poOvrBand->GetYSize();
                dfTotalPixels += dfOvrPixels * l_nBands;
                if (poOvrBand->GetMaskFlags() == GMF_PER_DATASET ||
                    poMaskOvrDS != nullptr)
                {
                    dfTotalPixels += dfOvrPixels;
                }
                else if (i == 0 && poDS->GetRasterBand(1)->GetMaskFlags() ==
                                       GMF_PER_DATASET)
                {
                    ReportError(pszFilename, CE_Warning, CPLE_AppDefined,
                                "Source dataset has a mask band on full "
                                "resolution, overviews on the regular bands, "
                                "but lacks overviews on the mask band.");
                }
            }

            char *papszCopyWholeRasterOptions[2] = {nullptr, nullptr};
            if (l_nCompression != COMPRESSION_NONE)
                papszCopyWholeRasterOptions[0] =
                    const_cast<char *>("COMPRESSED=YES");
            // Now copy the imagery.
            // Begin with the smallest overview.
            for (int iOvrLevel = nSrcOverviews - 1;
                 eErr == CE_None && iOvrLevel >= 0; --iOvrLevel)
            {
                auto poDstDS = poDS->m_papoOverviewDS[iOvrLevel];

                // Create a fake dataset with the source overview level so that
                // GDALDatasetCopyWholeRaster can cope with it.
                GDALDataset *poSrcOvrDS =
                    poOvrDS
                        ? (iOvrLevel == 0 ? poOvrDS.get()
                                          : GDALCreateOverviewDataset(
                                                poOvrDS.get(), iOvrLevel - 1,
                                                /* bThisLevelOnly = */ true))
                        : GDALCreateOverviewDataset(
                              poSrcDS, iOvrLevel,
                              /* bThisLevelOnly = */ true);
                GDALRasterBand *poSrcOvrBand =
                    poOvrDS ? (iOvrLevel == 0
                                   ? poOvrDS->GetRasterBand(1)
                                   : poOvrDS->GetRasterBand(1)->GetOverview(
                                         iOvrLevel - 1))
                            : poSrcDS->GetRasterBand(1)->GetOverview(iOvrLevel);
                double dfNextCurPixels =
                    dfCurPixels +
                    static_cast<double>(poSrcOvrBand->GetXSize()) *
                        poSrcOvrBand->GetYSize() * l_nBands;

                poDstDS->m_bBlockOrderRowMajor = true;
                poDstDS->m_bLeaderSizeAsUInt4 = true;
                poDstDS->m_bTrailerRepeatedLast4BytesRepeated = true;
                poDstDS->m_bFillEmptyTilesAtClosing =
                    poDS->m_bFillEmptyTilesAtClosing;
                poDstDS->m_bWriteEmptyTiles = poDS->m_bWriteEmptyTiles;
                GDALRasterBand *poSrcMaskBand = nullptr;
                if (poDstDS->m_poMaskDS)
                {
                    poDstDS->m_poMaskDS->m_bBlockOrderRowMajor = true;
                    poDstDS->m_poMaskDS->m_bLeaderSizeAsUInt4 = true;
                    poDstDS->m_poMaskDS->m_bTrailerRepeatedLast4BytesRepeated =
                        true;
                    poDstDS->m_poMaskDS->m_bFillEmptyTilesAtClosing =
                        poDS->m_bFillEmptyTilesAtClosing;
                    poDstDS->m_poMaskDS->m_bWriteEmptyTiles =
                        poDS->m_bWriteEmptyTiles;

                    poSrcMaskBand =
                        poMaskOvrDS
                            ? (iOvrLevel == 0
                                   ? poMaskOvrDS->GetRasterBand(1)
                                   : poMaskOvrDS->GetRasterBand(1)->GetOverview(
                                         iOvrLevel - 1))
                            : poSrcOvrBand->GetMaskBand();
                }

                if (l_nBands == 1 ||
                    poDstDS->m_nPlanarConfig == PLANARCONFIG_CONTIG)
                {
                    if (poDstDS->m_poMaskDS)
                    {
                        dfNextCurPixels +=
                            static_cast<double>(poSrcOvrBand->GetXSize()) *
                            poSrcOvrBand->GetYSize();
                    }
                    void *pScaledData = GDALCreateScaledProgress(
                        dfCurPixels / dfTotalPixels,
                        dfNextCurPixels / dfTotalPixels, pfnProgress,
                        pProgressData);

                    eErr =
                        CopyImageryAndMask(poDstDS, poSrcOvrDS, poSrcMaskBand,
                                           GDALScaledProgress, pScaledData);

                    dfCurPixels = dfNextCurPixels;
                    GDALDestroyScaledProgress(pScaledData);
                }
                else
                {
                    void *pScaledData = GDALCreateScaledProgress(
                        dfCurPixels / dfTotalPixels,
                        dfNextCurPixels / dfTotalPixels, pfnProgress,
                        pProgressData);

                    eErr = GDALDatasetCopyWholeRaster(
                        GDALDataset::ToHandle(poSrcOvrDS),
                        GDALDataset::ToHandle(poDstDS),
                        papszCopyWholeRasterOptions, GDALScaledProgress,
                        pScaledData);

                    dfCurPixels = dfNextCurPixels;
                    GDALDestroyScaledProgress(pScaledData);

                    poDstDS->FlushCache(false);

                    // Copy mask of the overview.
                    if (eErr == CE_None &&
                        (poMaskOvrDS ||
                         poSrcOvrBand->GetMaskFlags() == GMF_PER_DATASET) &&
                        poDstDS->m_poMaskDS != nullptr)
                    {
                        dfNextCurPixels +=
                            static_cast<double>(poSrcOvrBand->GetXSize()) *
                            poSrcOvrBand->GetYSize();
                        pScaledData = GDALCreateScaledProgress(
                            dfCurPixels / dfTotalPixels,
                            dfNextCurPixels / dfTotalPixels, pfnProgress,
                            pProgressData);
                        eErr = GDALRasterBandCopyWholeRaster(
                            poSrcMaskBand,
                            poDstDS->m_poMaskDS->GetRasterBand(1),
                            papszCopyWholeRasterOptions, GDALScaledProgress,
                            pScaledData);
                        dfCurPixels = dfNextCurPixels;
                        GDALDestroyScaledProgress(pScaledData);
                        poDstDS->m_poMaskDS->FlushCache(false);
                    }
                }

                if (poSrcOvrDS != poOvrDS.get())
                    delete poSrcOvrDS;
                poSrcOvrDS = nullptr;
            }
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Copy actual imagery.                                            */
    /* -------------------------------------------------------------------- */
    double dfNextCurPixels =
        dfCurPixels + static_cast<double>(nXSize) * nYSize * l_nBands;
    void *pScaledData = GDALCreateScaledProgress(
        dfCurPixels / dfTotalPixels, dfNextCurPixels / dfTotalPixels,
        pfnProgress, pProgressData);

#if defined(HAVE_LIBJPEG) || defined(JPEG_DIRECT_COPY)
    bool bTryCopy = true;
#endif

#ifdef HAVE_LIBJPEG
    if (bCopyFromJPEG)
    {
        eErr = GTIFF_CopyFromJPEG(poDS, poSrcDS, pfnProgress, pProgressData,
                                  bTryCopy);

        // In case of failure in the decompression step, try normal copy.
        if (bTryCopy)
            eErr = CE_None;
    }
#endif

#ifdef JPEG_DIRECT_COPY
    if (bDirectCopyFromJPEG)
    {
        eErr = GTIFF_DirectCopyFromJPEG(poDS, poSrcDS, pfnProgress,
                                        pProgressData, bTryCopy);

        // In case of failure in the reading step, try normal copy.
        if (bTryCopy)
            eErr = CE_None;
    }
#endif

    bool bWriteMask = true;
    if (
#if defined(HAVE_LIBJPEG) || defined(JPEG_DIRECT_COPY)
        bTryCopy &&
#endif
        (poDS->m_bTreatAsSplit || poDS->m_bTreatAsSplitBitmap))
    {
        // For split bands, we use TIFFWriteScanline() interface.
        CPLAssert(poDS->m_nBitsPerSample == 8 || poDS->m_nBitsPerSample == 1);

        if (poDS->m_nPlanarConfig == PLANARCONFIG_CONTIG && poDS->nBands > 1)
        {
            GByte *pabyScanline = static_cast<GByte *>(
                VSI_MALLOC_VERBOSE(TIFFScanlineSize(l_hTIFF)));
            if (pabyScanline == nullptr)
                eErr = CE_Failure;
            for (int j = 0; j < nYSize && eErr == CE_None; ++j)
            {
                eErr = poSrcDS->RasterIO(GF_Read, 0, j, nXSize, 1, pabyScanline,
                                         nXSize, 1, GDT_Byte, l_nBands, nullptr,
                                         poDS->nBands, 0, 1, nullptr);
                if (eErr == CE_None &&
                    TIFFWriteScanline(l_hTIFF, pabyScanline, j, 0) == -1)
                {
                    ReportError(pszFilename, CE_Failure, CPLE_AppDefined,
                                "TIFFWriteScanline() failed.");
                    eErr = CE_Failure;
                }
                if (!GDALScaledProgress((j + 1) * 1.0 / nYSize, nullptr,
                                        pScaledData))
                    eErr = CE_Failure;
            }
            CPLFree(pabyScanline);
        }
        else
        {
            GByte *pabyScanline =
                static_cast<GByte *>(VSI_MALLOC_VERBOSE(nXSize));
            if (pabyScanline == nullptr)
                eErr = CE_Failure;
            else
                eErr = CE_None;
            for (int iBand = 1; iBand <= l_nBands && eErr == CE_None; ++iBand)
            {
                for (int j = 0; j < nYSize && eErr == CE_None; ++j)
                {
                    eErr = poSrcDS->GetRasterBand(iBand)->RasterIO(
                        GF_Read, 0, j, nXSize, 1, pabyScanline, nXSize, 1,
                        GDT_Byte, 0, 0, nullptr);
                    if (poDS->m_bTreatAsSplitBitmap)
                    {
                        for (int i = 0; i < nXSize; ++i)
                        {
                            const GByte byVal = pabyScanline[i];
                            if ((i & 0x7) == 0)
                                pabyScanline[i >> 3] = 0;
                            if (byVal)
                                pabyScanline[i >> 3] |= 0x80 >> (i & 0x7);
                        }
                    }
                    if (eErr == CE_None &&
                        TIFFWriteScanline(l_hTIFF, pabyScanline, j,
                                          static_cast<uint16_t>(iBand - 1)) ==
                            -1)
                    {
                        ReportError(pszFilename, CE_Failure, CPLE_AppDefined,
                                    "TIFFWriteScanline() failed.");
                        eErr = CE_Failure;
                    }
                    if (!GDALScaledProgress((j + 1 + (iBand - 1) * nYSize) *
                                                1.0 / (l_nBands * nYSize),
                                            nullptr, pScaledData))
                        eErr = CE_Failure;
                }
            }
            CPLFree(pabyScanline);
        }

        // Necessary to be able to read the file without re-opening.
        TIFFSizeProc pfnSizeProc = TIFFGetSizeProc(l_hTIFF);

        TIFFFlushData(l_hTIFF);

        toff_t nNewDirOffset = pfnSizeProc(TIFFClientdata(l_hTIFF));
        if ((nNewDirOffset % 2) == 1)
            ++nNewDirOffset;

        TIFFFlush(l_hTIFF);

        if (poDS->m_nDirOffset != TIFFCurrentDirOffset(l_hTIFF))
        {
            poDS->m_nDirOffset = nNewDirOffset;
            CPLDebug("GTiff", "directory moved during flush.");
        }
    }
    else if (
#if defined(HAVE_LIBJPEG) || defined(JPEG_DIRECT_COPY)
        bTryCopy &&
#endif
        eErr == CE_None)
    {
        const char *papszCopyWholeRasterOptions[3] = {nullptr, nullptr,
                                                      nullptr};
        int iNextOption = 0;
        papszCopyWholeRasterOptions[iNextOption++] = "SKIP_HOLES=YES";
        if (l_nCompression != COMPRESSION_NONE)
        {
            papszCopyWholeRasterOptions[iNextOption++] = "COMPRESSED=YES";
        }
        // For streaming with separate, we really want that bands are written
        // after each other, even if the source is pixel interleaved.
        else if (bStreaming && poDS->m_nPlanarConfig == PLANARCONFIG_SEPARATE)
        {
            papszCopyWholeRasterOptions[iNextOption++] = "INTERLEAVE=BAND";
        }

        if (bCopySrcOverviews &&
            (l_nBands == 1 || poDS->m_nPlanarConfig == PLANARCONFIG_CONTIG))
        {
            poDS->m_bBlockOrderRowMajor = true;
            poDS->m_bLeaderSizeAsUInt4 = true;
            poDS->m_bTrailerRepeatedLast4BytesRepeated = true;
            if (poDS->m_poMaskDS)
            {
                poDS->m_poMaskDS->m_bBlockOrderRowMajor = true;
                poDS->m_poMaskDS->m_bLeaderSizeAsUInt4 = true;
                poDS->m_poMaskDS->m_bTrailerRepeatedLast4BytesRepeated = true;
            }

            if (poDS->m_poMaskDS)
            {
                GDALDestroyScaledProgress(pScaledData);
                pScaledData =
                    GDALCreateScaledProgress(dfCurPixels / dfTotalPixels, 1.0,
                                             pfnProgress, pProgressData);
            }

            eErr = CopyImageryAndMask(poDS, poSrcDS,
                                      poSrcDS->GetRasterBand(1)->GetMaskBand(),
                                      GDALScaledProgress, pScaledData);
            if (poDS->m_poMaskDS)
            {
                bWriteMask = false;
            }
        }
        else
        {
            eErr = GDALDatasetCopyWholeRaster(
                /* (GDALDatasetH) */ poSrcDS,
                /* (GDALDatasetH) */ poDS, papszCopyWholeRasterOptions,
                GDALScaledProgress, pScaledData);
        }
    }

    GDALDestroyScaledProgress(pScaledData);

    if (eErr == CE_None && !bStreaming && bWriteMask)
    {
        pScaledData = GDALCreateScaledProgress(dfNextCurPixels / dfTotalPixels,
                                               1.0, pfnProgress, pProgressData);
        if (poDS->m_poMaskDS)
        {
            const char *l_papszOptions[2] = {"COMPRESSED=YES", nullptr};
            eErr = GDALRasterBandCopyWholeRaster(
                poSrcDS->GetRasterBand(1)->GetMaskBand(),
                poDS->GetRasterBand(1)->GetMaskBand(),
                const_cast<char **>(l_papszOptions), GDALScaledProgress,
                pScaledData);
        }
        else
        {
            eErr =
                GDALDriver::DefaultCopyMasks(poSrcDS, poDS, bStrict, nullptr,
                                             GDALScaledProgress, pScaledData);
        }
        GDALDestroyScaledProgress(pScaledData);
    }

    poDS->m_bWriteCOGLayout = false;

    if (eErr == CE_Failure)
    {
        delete poDS;
        poDS = nullptr;

        if (CPLTestBool(CPLGetConfigOption("GTIFF_DELETE_ON_ERROR", "YES")))
        {
            if (!bStreaming)
            {
                // Should really delete more carefully.
                VSIUnlink(pszFilename);
            }
        }
    }

    return poDS;
}

/************************************************************************/
/*                          GetSpatialRef()                             */
/************************************************************************/

const OGRSpatialReference *GTiffDataset::GetSpatialRef() const

{
    const_cast<GTiffDataset *>(this)->LoadGeoreferencingAndPamIfNeeded();
    if (m_nGCPCount == 0)
    {
        const_cast<GTiffDataset *>(this)->LookForProjection();
    }

    return m_nGCPCount == 0 && !m_oSRS.IsEmpty() ? &m_oSRS : nullptr;
}

/************************************************************************/
/*                           SetSpatialRef()                            */
/************************************************************************/

CPLErr GTiffDataset::SetSpatialRef(const OGRSpatialReference *poSRS)

{
    if (m_bStreamingOut && m_bCrystalized)
    {
        ReportError(CE_Failure, CPLE_NotSupported,
                    "Cannot modify projection at that point in "
                    "a streamed output file");
        return CE_Failure;
    }

    LoadGeoreferencingAndPamIfNeeded();
    LookForProjection();

    CPLErr eErr = CE_None;
    if (eAccess == GA_Update)
    {
        if ((m_eProfile == GTiffProfile::BASELINE) &&
            (GetPamFlags() & GPF_DISABLED) == 0)
        {
            eErr = GDALPamDataset::SetSpatialRef(poSRS);
        }
        else
        {
            if (GDALPamDataset::GetSpatialRef() != nullptr)
            {
                // Cancel any existing SRS from PAM file.
                GDALPamDataset::SetSpatialRef(nullptr);
            }
            m_bGeoTIFFInfoChanged = true;
        }
    }
    else
    {
        CPLDebug("GTIFF", "SetSpatialRef() goes to PAM instead of TIFF tags");
        eErr = GDALPamDataset::SetSpatialRef(poSRS);
    }

    if (eErr == CE_None)
    {
        if (poSRS == nullptr || poSRS->IsEmpty())
        {
            if (!m_oSRS.IsEmpty())
            {
                m_bForceUnsetProjection = true;
            }
            m_oSRS.Clear();
        }
        else
        {
            m_oSRS = *poSRS;
            m_oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
        }
    }

    return eErr;
}

/************************************************************************/
/*                          GetGeoTransform()                           */
/************************************************************************/

CPLErr GTiffDataset::GetGeoTransform(double *padfTransform)

{
    LoadGeoreferencingAndPamIfNeeded();

    memcpy(padfTransform, m_adfGeoTransform, sizeof(double) * 6);

    if (!m_bGeoTransformValid)
        return CE_Failure;

    // Same logic as in the .gtx driver, for the benefit of
    // GDALOpenVerticalShiftGrid() when used with PROJ-data's US geoids.
    if (CPLFetchBool(papszOpenOptions, "SHIFT_ORIGIN_IN_MINUS_180_PLUS_180",
                     false))
    {
        if (padfTransform[0] < -180.0 - padfTransform[1])
            padfTransform[0] += 360.0;
        else if (padfTransform[0] > 180.0)
            padfTransform[0] -= 360.0;
    }

    return CE_None;
}

/************************************************************************/
/*                          SetGeoTransform()                           */
/************************************************************************/

CPLErr GTiffDataset::SetGeoTransform(double *padfTransform)

{
    if (m_bStreamingOut && m_bCrystalized)
    {
        ReportError(CE_Failure, CPLE_NotSupported,
                    "Cannot modify geotransform at that point in a "
                    "streamed output file");
        return CE_Failure;
    }

    LoadGeoreferencingAndPamIfNeeded();

    CPLErr eErr = CE_None;
    if (eAccess == GA_Update)
    {
        if (m_nGCPCount > 0)
        {
            ReportError(CE_Warning, CPLE_AppDefined,
                        "GCPs previously set are going to be cleared "
                        "due to the setting of a geotransform.");
            m_bForceUnsetGTOrGCPs = true;
            GDALDeinitGCPs(m_nGCPCount, m_pasGCPList);
            CPLFree(m_pasGCPList);
            m_nGCPCount = 0;
            m_pasGCPList = nullptr;
        }
        else if (padfTransform[0] == 0.0 && padfTransform[1] == 0.0 &&
                 padfTransform[2] == 0.0 && padfTransform[3] == 0.0 &&
                 padfTransform[4] == 0.0 && padfTransform[5] == 0.0)
        {
            if (m_bGeoTransformValid)
            {
                m_bForceUnsetGTOrGCPs = true;
                m_bGeoTIFFInfoChanged = true;
            }
            m_bGeoTransformValid = false;
            memcpy(m_adfGeoTransform, padfTransform, sizeof(double) * 6);
            return CE_None;
        }

        if ((m_eProfile == GTiffProfile::BASELINE) &&
            !CPLFetchBool(m_papszCreationOptions, "TFW", false) &&
            !CPLFetchBool(m_papszCreationOptions, "WORLDFILE", false) &&
            (GetPamFlags() & GPF_DISABLED) == 0)
        {
            eErr = GDALPamDataset::SetGeoTransform(padfTransform);
        }
        else
        {
            // Cancel any existing geotransform from PAM file.
            GDALPamDataset::DeleteGeoTransform();
            m_bGeoTIFFInfoChanged = true;
        }
    }
    else
    {
        CPLDebug("GTIFF", "SetGeoTransform() goes to PAM instead of TIFF tags");
        eErr = GDALPamDataset::SetGeoTransform(padfTransform);
    }

    if (eErr == CE_None)
    {
        memcpy(m_adfGeoTransform, padfTransform, sizeof(double) * 6);
        m_bGeoTransformValid = true;
    }

    return eErr;
}

/************************************************************************/
/*                            GetGCPCount()                             */
/************************************************************************/

int GTiffDataset::GetGCPCount()

{
    LoadGeoreferencingAndPamIfNeeded();

    return m_nGCPCount;
}

/************************************************************************/
/*                          GetGCPSpatialRef()                          */
/************************************************************************/

const OGRSpatialReference *GTiffDataset::GetGCPSpatialRef() const

{
    const_cast<GTiffDataset *>(this)->LoadGeoreferencingAndPamIfNeeded();

    if (m_nGCPCount > 0)
    {
        const_cast<GTiffDataset *>(this)->LookForProjection();
    }
    return m_nGCPCount > 0 && !m_oSRS.IsEmpty() ? &m_oSRS : nullptr;
}

/************************************************************************/
/*                               GetGCPs()                              */
/************************************************************************/

const GDAL_GCP *GTiffDataset::GetGCPs()

{
    LoadGeoreferencingAndPamIfNeeded();

    return m_pasGCPList;
}

/************************************************************************/
/*                               SetGCPs()                              */
/************************************************************************/

CPLErr GTiffDataset::SetGCPs(int nGCPCountIn, const GDAL_GCP *pasGCPListIn,
                             const OGRSpatialReference *poGCPSRS)
{
    CPLErr eErr = CE_None;
    LoadGeoreferencingAndPamIfNeeded();
    LookForProjection();

    if (eAccess == GA_Update)
    {
        if (m_nGCPCount > 0 && nGCPCountIn == 0)
        {
            m_bForceUnsetGTOrGCPs = true;
        }
        else if (nGCPCountIn > 0 && m_bGeoTransformValid)
        {
            ReportError(CE_Warning, CPLE_AppDefined,
                        "A geotransform previously set is going to be cleared "
                        "due to the setting of GCPs.");
            m_adfGeoTransform[0] = 0.0;
            m_adfGeoTransform[1] = 1.0;
            m_adfGeoTransform[2] = 0.0;
            m_adfGeoTransform[3] = 0.0;
            m_adfGeoTransform[4] = 0.0;
            m_adfGeoTransform[5] = 1.0;
            m_bGeoTransformValid = false;
            m_bForceUnsetGTOrGCPs = true;
        }

        if ((m_eProfile == GTiffProfile::BASELINE) &&
            (GetPamFlags() & GPF_DISABLED) == 0)
        {
            eErr = GDALPamDataset::SetGCPs(nGCPCountIn, pasGCPListIn, poGCPSRS);
        }
        else
        {
            if (GDALPamDataset::GetGCPCount() > 0)
            {
                // Cancel any existing GCPs from PAM file.
                GDALPamDataset::SetGCPs(
                    0, nullptr,
                    static_cast<const OGRSpatialReference *>(nullptr));
            }
            m_bGeoTIFFInfoChanged = true;
        }
    }
    else
    {
        CPLDebug("GTIFF", "SetGCPs() goes to PAM instead of TIFF tags");
        eErr = GDALPamDataset::SetGCPs(nGCPCountIn, pasGCPListIn, poGCPSRS);
    }

    if (eErr == CE_None)
    {
        if (poGCPSRS == nullptr || poGCPSRS->IsEmpty())
        {
            if (!m_oSRS.IsEmpty())
            {
                m_bForceUnsetProjection = true;
            }
            m_oSRS.Clear();
        }
        else
        {
            m_oSRS = *poGCPSRS;
            m_oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
        }

        if (m_nGCPCount > 0)
        {
            GDALDeinitGCPs(m_nGCPCount, m_pasGCPList);
            CPLFree(m_pasGCPList);
        }

        m_nGCPCount = nGCPCountIn;
        m_pasGCPList = GDALDuplicateGCPs(m_nGCPCount, pasGCPListIn);
    }

    return eErr;
}

/************************************************************************/
/*                      GetMetadataDomainList()                         */
/************************************************************************/

char **GTiffDataset::GetMetadataDomainList()
{
    LoadGeoreferencingAndPamIfNeeded();

    char **papszDomainList = CSLDuplicate(m_oGTiffMDMD.GetDomainList());
    char **papszBaseList = GDALDataset::GetMetadataDomainList();

    const int nbBaseDomains = CSLCount(papszBaseList);

    for (int domainId = 0; domainId < nbBaseDomains; ++domainId)
    {
        if (CSLFindString(papszDomainList, papszBaseList[domainId]) < 0)
        {
            papszDomainList =
                CSLAddString(papszDomainList, papszBaseList[domainId]);
        }
    }

    CSLDestroy(papszBaseList);

    return BuildMetadataDomainList(papszDomainList, TRUE, "",
                                   "ProxyOverviewRequest", MD_DOMAIN_RPC,
                                   MD_DOMAIN_IMD, "SUBDATASETS", "EXIF",
                                   "xml:XMP", "COLOR_PROFILE", nullptr);
}

/************************************************************************/
/*                            GetMetadata()                             */
/************************************************************************/

char **GTiffDataset::GetMetadata(const char *pszDomain)

{
    if (pszDomain != nullptr && EQUAL(pszDomain, "IMAGE_STRUCTURE"))
    {
        GTiffDataset::GetMetadataItem("COMPRESSION_REVERSIBILITY", pszDomain);
    }
    else
    {
        LoadGeoreferencingAndPamIfNeeded();
    }

    if (pszDomain != nullptr && EQUAL(pszDomain, "ProxyOverviewRequest"))
        return GDALPamDataset::GetMetadata(pszDomain);

    if (pszDomain != nullptr && EQUAL(pszDomain, "DERIVED_SUBDATASETS"))
    {
        return GDALDataset::GetMetadata(pszDomain);
    }

    else if (pszDomain != nullptr && (EQUAL(pszDomain, MD_DOMAIN_RPC) ||
                                      EQUAL(pszDomain, MD_DOMAIN_IMD) ||
                                      EQUAL(pszDomain, MD_DOMAIN_IMAGERY)))
        LoadMetadata();

    else if (pszDomain != nullptr && EQUAL(pszDomain, "SUBDATASETS"))
        ScanDirectories();

    else if (pszDomain != nullptr && EQUAL(pszDomain, "EXIF"))
        LoadEXIFMetadata();

    else if (pszDomain != nullptr && EQUAL(pszDomain, "COLOR_PROFILE"))
        LoadICCProfile();

    else if (pszDomain == nullptr || EQUAL(pszDomain, ""))
        LoadMDAreaOrPoint();  // To set GDALMD_AREA_OR_POINT.

    return m_oGTiffMDMD.GetMetadata(pszDomain);
}

/************************************************************************/
/*                            SetMetadata()                             */
/************************************************************************/
CPLErr GTiffDataset::SetMetadata(char **papszMD, const char *pszDomain)

{
    LoadGeoreferencingAndPamIfNeeded();

    if (m_bStreamingOut && m_bCrystalized)
    {
        ReportError(
            CE_Failure, CPLE_NotSupported,
            "Cannot modify metadata at that point in a streamed output file");
        return CE_Failure;
    }

    CPLErr eErr = CE_None;
    if (eAccess == GA_Update)
    {
        if (pszDomain != nullptr && EQUAL(pszDomain, MD_DOMAIN_RPC))
        {
            // So that a subsequent GetMetadata() wouldn't override our new
            // values
            LoadMetadata();
            m_bForceUnsetRPC = (CSLCount(papszMD) == 0);
        }

        if ((papszMD != nullptr) && (pszDomain != nullptr) &&
            EQUAL(pszDomain, "COLOR_PROFILE"))
        {
            m_bColorProfileMetadataChanged = true;
        }
        else if (pszDomain == nullptr || !EQUAL(pszDomain, "_temporary_"))
        {
            m_bMetadataChanged = true;
            // Cancel any existing metadata from PAM file.
            if (GDALPamDataset::GetMetadata(pszDomain) != nullptr)
                GDALPamDataset::SetMetadata(nullptr, pszDomain);
        }

        if ((pszDomain == nullptr || EQUAL(pszDomain, "")) &&
            CSLFetchNameValue(papszMD, GDALMD_AREA_OR_POINT) != nullptr)
        {
            const char *pszPrevValue = GetMetadataItem(GDALMD_AREA_OR_POINT);
            const char *pszNewValue =
                CSLFetchNameValue(papszMD, GDALMD_AREA_OR_POINT);
            if (pszPrevValue == nullptr || pszNewValue == nullptr ||
                !EQUAL(pszPrevValue, pszNewValue))
            {
                LookForProjection();
                m_bGeoTIFFInfoChanged = true;
            }
        }

        if (pszDomain != nullptr && EQUAL(pszDomain, "xml:XMP"))
        {
            if (papszMD != nullptr && *papszMD != nullptr)
            {
                int nTagSize = static_cast<int>(strlen(*papszMD));
                TIFFSetField(m_hTIFF, TIFFTAG_XMLPACKET, nTagSize, *papszMD);
            }
            else
            {
                TIFFUnsetField(m_hTIFF, TIFFTAG_XMLPACKET);
            }
        }
    }
    else
    {
        CPLDebug(
            "GTIFF",
            "GTiffDataset::SetMetadata() goes to PAM instead of TIFF tags");
        eErr = GDALPamDataset::SetMetadata(papszMD, pszDomain);
    }

    if (eErr == CE_None)
    {
        eErr = m_oGTiffMDMD.SetMetadata(papszMD, pszDomain);
    }
    return eErr;
}

/************************************************************************/
/*                          GetMetadataItem()                           */
/************************************************************************/

const char *GTiffDataset::GetMetadataItem(const char *pszName,
                                          const char *pszDomain)

{
    if (pszDomain != nullptr && EQUAL(pszDomain, "IMAGE_STRUCTURE"))
    {
        if ((m_nCompression == COMPRESSION_WEBP ||
             m_nCompression == COMPRESSION_JXL) &&
            EQUAL(pszName, "COMPRESSION_REVERSIBILITY") &&
            m_oGTiffMDMD.GetMetadataItem("COMPRESSION_REVERSIBILITY",
                                         "IMAGE_STRUCTURE") == nullptr)
        {
            const char *pszDriverName =
                m_nCompression == COMPRESSION_WEBP ? "WEBP" : "JPEGXL";
            auto poTileDriver = GDALGetDriverByName(pszDriverName);
            if (poTileDriver)
            {
                vsi_l_offset nOffset = 0;
                vsi_l_offset nSize = 0;
                IsBlockAvailable(0, &nOffset, &nSize);
                if (nSize > 0)
                {
                    const std::string osSubfile(
                        CPLSPrintf("/vsisubfile/" CPL_FRMT_GUIB "_%d,%s",
                                   static_cast<GUIntBig>(nOffset),
                                   static_cast<int>(std::min(
                                       static_cast<vsi_l_offset>(1024), nSize)),
                                   m_pszFilename));
                    const char *const apszDrivers[] = {pszDriverName, nullptr};
                    auto poWebPDataset =
                        std::unique_ptr<GDALDataset>(GDALDataset::Open(
                            osSubfile.c_str(), GDAL_OF_RASTER, apszDrivers));
                    if (poWebPDataset)
                    {
                        const char *pszReversibility =
                            poWebPDataset->GetMetadataItem(
                                "COMPRESSION_REVERSIBILITY", "IMAGE_STRUCTURE");
                        if (pszReversibility)
                            m_oGTiffMDMD.SetMetadataItem(
                                "COMPRESSION_REVERSIBILITY", pszReversibility,
                                "IMAGE_STRUCTURE");
                    }
                }
            }
        }
    }
    else
    {
        LoadGeoreferencingAndPamIfNeeded();
    }

    if (pszDomain != nullptr && EQUAL(pszDomain, "ProxyOverviewRequest"))
    {
        return GDALPamDataset::GetMetadataItem(pszName, pszDomain);
    }
    else if (pszDomain != nullptr && (EQUAL(pszDomain, MD_DOMAIN_RPC) ||
                                      EQUAL(pszDomain, MD_DOMAIN_IMD) ||
                                      EQUAL(pszDomain, MD_DOMAIN_IMAGERY)))
    {
        LoadMetadata();
    }
    else if (pszDomain != nullptr && EQUAL(pszDomain, "SUBDATASETS"))
    {
        ScanDirectories();
    }
    else if (pszDomain != nullptr && EQUAL(pszDomain, "EXIF"))
    {
        LoadEXIFMetadata();
    }
    else if (pszDomain != nullptr && EQUAL(pszDomain, "COLOR_PROFILE"))
    {
        LoadICCProfile();
    }
    else if ((pszDomain == nullptr || EQUAL(pszDomain, "")) &&
             pszName != nullptr && EQUAL(pszName, GDALMD_AREA_OR_POINT))
    {
        LoadMDAreaOrPoint();  // To set GDALMD_AREA_OR_POINT.
    }
    else if (pszDomain != nullptr && EQUAL(pszDomain, "_DEBUG_") &&
             pszName != nullptr)
    {
#ifdef DEBUG_REACHED_VIRTUAL_MEM_IO
        if (EQUAL(pszName, "UNREACHED_VIRTUALMEMIO_CODE_PATH"))
        {
            CPLString osMissing;
            for (int i = 0;
                 i < static_cast<int>(CPL_ARRAYSIZE(anReachedVirtualMemIO));
                 ++i)
            {
                if (!anReachedVirtualMemIO[i])
                {
                    if (!osMissing.empty())
                        osMissing += ",";
                    osMissing += CPLSPrintf("%d", i);
                }
            }
            return (osMissing.size()) ? CPLSPrintf("%s", osMissing.c_str())
                                      : nullptr;
        }
        else
#endif
            if (EQUAL(pszName, "TIFFTAG_EXTRASAMPLES"))
        {
            CPLString osRet;
            uint16_t *v = nullptr;
            uint16_t count = 0;

            if (TIFFGetField(m_hTIFF, TIFFTAG_EXTRASAMPLES, &count, &v))
            {
                for (int i = 0; i < static_cast<int>(count); ++i)
                {
                    if (i > 0)
                        osRet += ",";
                    osRet += CPLSPrintf("%d", v[i]);
                }
            }
            return (osRet.size()) ? CPLSPrintf("%s", osRet.c_str()) : nullptr;
        }
        else if (EQUAL(pszName, "TIFFTAG_PHOTOMETRIC"))
        {
            return CPLSPrintf("%d", m_nPhotometric);
        }

        else if (EQUAL(pszName, "TIFFTAG_GDAL_METADATA"))
        {
            char *pszText = nullptr;
            if (!TIFFGetField(m_hTIFF, TIFFTAG_GDAL_METADATA, &pszText))
                return nullptr;

            return pszText;
        }
        else if (EQUAL(pszName, "HAS_USED_READ_ENCODED_API"))
        {
            return m_bHasUsedReadEncodedAPI ? "1" : "0";
        }
        else if (EQUAL(pszName, "WEBP_LOSSLESS"))
        {
            return m_bWebPLossless ? "1" : "0";
        }
        else if (EQUAL(pszName, "WEBP_LEVEL"))
        {
            return CPLSPrintf("%d", m_nWebPLevel);
        }
#if HAVE_JXL
        else if (EQUAL(pszName, "JXL_LOSSLESS"))
        {
            return m_bJXLLossless ? "1" : "0";
        }
        else if (EQUAL(pszName, "JXL_DISTANCE"))
        {
            return CPLSPrintf("%f", m_fJXLDistance);
        }
        else if (EQUAL(pszName, "JXL_EFFORT"))
        {
            return CPLSPrintf("%u", m_nJXLEffort);
        }
#endif
        return nullptr;
    }

    else if (pszDomain != nullptr && EQUAL(pszDomain, "TIFF") &&
             pszName != nullptr)
    {
        if (EQUAL(pszName, "GDAL_STRUCTURAL_METADATA"))
        {
            const auto nOffset = VSIFTellL(m_fpL);
            VSIFSeekL(m_fpL, 0, SEEK_SET);
            GByte abyData[1024];
            size_t nRead = VSIFReadL(abyData, 1, sizeof(abyData) - 1, m_fpL);
            abyData[nRead] = 0;
            VSIFSeekL(m_fpL, nOffset, SEEK_SET);
            if (nRead > 4)
            {
                const int nOffsetOfStructuralMetadata =
                    (abyData[2] == 0x2B || abyData[3] == 0x2B) ? 16 : 8;
                const int nSizePatternLen =
                    static_cast<int>(strlen("XXXXXX bytes\n"));
                if (nRead > nOffsetOfStructuralMetadata +
                                strlen("GDAL_STRUCTURAL_METADATA_SIZE=") +
                                nSizePatternLen &&
                    memcmp(abyData + nOffsetOfStructuralMetadata,
                           "GDAL_STRUCTURAL_METADATA_SIZE=",
                           strlen("GDAL_STRUCTURAL_METADATA_SIZE=")) == 0)
                {
                    char *pszStructuralMD = reinterpret_cast<char *>(
                        abyData + nOffsetOfStructuralMetadata);
                    const int nLenMD =
                        atoi(pszStructuralMD +
                             strlen("GDAL_STRUCTURAL_METADATA_SIZE="));
                    if (nOffsetOfStructuralMetadata +
                            strlen("GDAL_STRUCTURAL_METADATA_SIZE=") +
                            nSizePatternLen + nLenMD <=
                        nRead)
                    {
                        pszStructuralMD[strlen(
                                            "GDAL_STRUCTURAL_METADATA_SIZE=") +
                                        nSizePatternLen + nLenMD] = 0;
                        return CPLSPrintf("%s", pszStructuralMD);
                    }
                }
            }
            return nullptr;
        }
    }

    return m_oGTiffMDMD.GetMetadataItem(pszName, pszDomain);
}

/************************************************************************/
/*                          SetMetadataItem()                           */
/************************************************************************/

CPLErr GTiffDataset::SetMetadataItem(const char *pszName, const char *pszValue,
                                     const char *pszDomain)

{
    LoadGeoreferencingAndPamIfNeeded();

    if (m_bStreamingOut && m_bCrystalized)
    {
        ReportError(
            CE_Failure, CPLE_NotSupported,
            "Cannot modify metadata at that point in a streamed output file");
        return CE_Failure;
    }

    CPLErr eErr = CE_None;
    if (eAccess == GA_Update)
    {
        if ((pszDomain != nullptr) && EQUAL(pszDomain, "COLOR_PROFILE"))
        {
            m_bColorProfileMetadataChanged = true;
        }
        else if (pszDomain == nullptr || !EQUAL(pszDomain, "_temporary_"))
        {
            m_bMetadataChanged = true;
            // Cancel any existing metadata from PAM file.
            if (GDALPamDataset::GetMetadataItem(pszName, pszDomain) != nullptr)
                GDALPamDataset::SetMetadataItem(pszName, nullptr, pszDomain);
        }

        if ((pszDomain == nullptr || EQUAL(pszDomain, "")) &&
            pszName != nullptr && EQUAL(pszName, GDALMD_AREA_OR_POINT))
        {
            LookForProjection();
            m_bGeoTIFFInfoChanged = true;
        }
    }
    else
    {
        CPLDebug(
            "GTIFF",
            "GTiffDataset::SetMetadataItem() goes to PAM instead of TIFF tags");
        eErr = GDALPamDataset::SetMetadataItem(pszName, pszValue, pszDomain);
    }

    if (eErr == CE_None)
    {
        eErr = m_oGTiffMDMD.SetMetadataItem(pszName, pszValue, pszDomain);
    }

    return eErr;
}

/************************************************************************/
/*                         GetInternalHandle()                          */
/************************************************************************/

void *GTiffDataset::GetInternalHandle(const char * /* pszHandleName */)

{
    return m_hTIFF;
}

/************************************************************************/
/*                         LoadEXIFMetadata()                           */
/************************************************************************/

void GTiffDataset::LoadEXIFMetadata()
{
    if (m_bEXIFMetadataLoaded)
        return;
    m_bEXIFMetadataLoaded = true;

    VSILFILE *fp = VSI_TIFFGetVSILFile(TIFFClientdata(m_hTIFF));

    GByte abyHeader[2] = {0};
    if (VSIFSeekL(fp, 0, SEEK_SET) != 0 || VSIFReadL(abyHeader, 1, 2, fp) != 2)
        return;

    const bool bLittleEndian = abyHeader[0] == 'I' && abyHeader[1] == 'I';
    const bool bLeastSignificantBit = CPL_IS_LSB != 0;
    const bool bSwabflag = bLittleEndian != bLeastSignificantBit;  // != is XOR.

    char **papszMetadata = nullptr;
    toff_t nOffset = 0;  // TODO(b/28199387): Refactor to simplify casting.

    if (TIFFGetField(m_hTIFF, TIFFTAG_EXIFIFD, &nOffset))
    {
        int nExifOffset = static_cast<int>(nOffset);
        int nInterOffset = 0;
        int nGPSOffset = 0;
        EXIFExtractMetadata(papszMetadata, fp, static_cast<int>(nOffset),
                            bSwabflag, 0, nExifOffset, nInterOffset,
                            nGPSOffset);
    }

    if (TIFFGetField(m_hTIFF, TIFFTAG_GPSIFD, &nOffset))
    {
        int nExifOffset = 0;  // TODO(b/28199387): Refactor to simplify casting.
        int nInterOffset = 0;
        int nGPSOffset = static_cast<int>(nOffset);
        EXIFExtractMetadata(papszMetadata, fp, static_cast<int>(nOffset),
                            bSwabflag, 0, nExifOffset, nInterOffset,
                            nGPSOffset);
    }

    if (papszMetadata)
    {
        m_oGTiffMDMD.SetMetadata(papszMetadata, "EXIF");
        CSLDestroy(papszMetadata);
    }
}

/************************************************************************/
/*                           LoadMetadata()                             */
/************************************************************************/
void GTiffDataset::LoadMetadata()
{
    if (m_bIMDRPCMetadataLoaded)
        return;
    m_bIMDRPCMetadataLoaded = true;

    GDALMDReaderManager mdreadermanager;
    GDALMDReaderBase *mdreader = mdreadermanager.GetReader(
        m_pszFilename, oOvManager.GetSiblingFiles(), MDR_ANY);

    if (nullptr != mdreader)
    {
        mdreader->FillMetadata(&m_oGTiffMDMD);

        if (mdreader->GetMetadataDomain(MD_DOMAIN_RPC) == nullptr)
        {
            char **papszRPCMD = GTiffDatasetReadRPCTag(m_hTIFF);
            if (papszRPCMD)
            {
                m_oGTiffMDMD.SetMetadata(papszRPCMD, MD_DOMAIN_RPC);
                CSLDestroy(papszRPCMD);
            }
        }

        m_papszMetadataFiles = mdreader->GetMetadataFiles();
    }
    else
    {
        char **papszRPCMD = GTiffDatasetReadRPCTag(m_hTIFF);
        if (papszRPCMD)
        {
            m_oGTiffMDMD.SetMetadata(papszRPCMD, MD_DOMAIN_RPC);
            CSLDestroy(papszRPCMD);
        }
    }
}

/************************************************************************/
/*                            GetFileList()                             */
/************************************************************************/

char **GTiffDataset::GetFileList()

{
    LoadGeoreferencingAndPamIfNeeded();

    char **papszFileList = GDALPamDataset::GetFileList();

    LoadMetadata();
    if (nullptr != m_papszMetadataFiles)
    {
        for (int i = 0; m_papszMetadataFiles[i] != nullptr; ++i)
        {
            if (CSLFindString(papszFileList, m_papszMetadataFiles[i]) < 0)
            {
                papszFileList =
                    CSLAddString(papszFileList, m_papszMetadataFiles[i]);
            }
        }
    }

    if (m_pszGeorefFilename &&
        CSLFindString(papszFileList, m_pszGeorefFilename) == -1)
    {
        papszFileList = CSLAddString(papszFileList, m_pszGeorefFilename);
    }

    return papszFileList;
}

/************************************************************************/
/*                         CreateMaskBand()                             */
/************************************************************************/

CPLErr GTiffDataset::CreateMaskBand(int nFlagsIn)
{
    ScanDirectories();

    if (m_poMaskDS != nullptr)
    {
        ReportError(CE_Failure, CPLE_AppDefined,
                    "This TIFF dataset has already an internal mask band");
        return CE_Failure;
    }
    else if (MustCreateInternalMask())
    {
        if (nFlagsIn != GMF_PER_DATASET)
        {
            ReportError(CE_Failure, CPLE_AppDefined,
                        "The only flag value supported for internal mask is "
                        "GMF_PER_DATASET");
            return CE_Failure;
        }

        int l_nCompression = COMPRESSION_PACKBITS;
        if (strstr(GDALGetMetadataItem(GDALGetDriverByName("GTiff"),
                                       GDAL_DMD_CREATIONOPTIONLIST, nullptr),
                   "<Value>DEFLATE</Value>") != nullptr)
            l_nCompression = COMPRESSION_ADOBE_DEFLATE;

        /* --------------------------------------------------------------------
         */
        /*      If we don't have read access, then create the mask externally.
         */
        /* --------------------------------------------------------------------
         */
        if (GetAccess() != GA_Update)
        {
            ReportError(CE_Warning, CPLE_AppDefined,
                        "File open for read-only accessing, "
                        "creating mask externally.");

            return GDALPamDataset::CreateMaskBand(nFlagsIn);
        }

        if (m_bLayoutIFDSBeforeData && !m_bKnownIncompatibleEdition &&
            !m_bWriteKnownIncompatibleEdition)
        {
            ReportError(CE_Warning, CPLE_AppDefined,
                        "Adding a mask invalidates the "
                        "LAYOUT=IFDS_BEFORE_DATA property");
            m_bKnownIncompatibleEdition = true;
            m_bWriteKnownIncompatibleEdition = true;
        }

        bool bIsOverview = false;
        uint32_t nSubType = 0;
        if (TIFFGetField(m_hTIFF, TIFFTAG_SUBFILETYPE, &nSubType))
        {
            bIsOverview = (nSubType & FILETYPE_REDUCEDIMAGE) != 0;

            if ((nSubType & FILETYPE_MASK) != 0)
            {
                ReportError(CE_Failure, CPLE_AppDefined,
                            "Cannot create a mask on a TIFF mask IFD !");
                return CE_Failure;
            }
        }

        const int bIsTiled = TIFFIsTiled(m_hTIFF);

        FlushDirectory();

        const toff_t nOffset = GTIFFWriteDirectory(
            m_hTIFF,
            bIsOverview ? FILETYPE_REDUCEDIMAGE | FILETYPE_MASK : FILETYPE_MASK,
            nRasterXSize, nRasterYSize, 1, PLANARCONFIG_CONTIG, 1,
            m_nBlockXSize, m_nBlockYSize, bIsTiled, l_nCompression,
            PHOTOMETRIC_MASK, PREDICTOR_NONE, SAMPLEFORMAT_UINT, nullptr,
            nullptr, nullptr, 0, nullptr, "", nullptr, nullptr, nullptr,
            nullptr, m_bWriteCOGLayout);

        ReloadDirectory();

        if (nOffset == 0)
            return CE_Failure;

        m_poMaskDS = new GTiffDataset();
        m_poMaskDS->m_poBaseDS = this;
        m_poMaskDS->m_poImageryDS = this;
        m_poMaskDS->ShareLockWithParentDataset(this);
        m_poMaskDS->m_bPromoteTo8Bits = CPLTestBool(
            CPLGetConfigOption("GDAL_TIFF_INTERNAL_MASK_TO_8BIT", "YES"));
        if (m_poMaskDS->OpenOffset(VSI_TIFFOpenChild(m_hTIFF), nOffset,
                                   GA_Update) != CE_None)
        {
            delete m_poMaskDS;
            m_poMaskDS = nullptr;
            return CE_Failure;
        }

        return CE_None;
    }

    return GDALPamDataset::CreateMaskBand(nFlagsIn);
}

/************************************************************************/
/*                        MustCreateInternalMask()                      */
/************************************************************************/

bool GTiffDataset::MustCreateInternalMask()
{
    return CPLTestBool(CPLGetConfigOption("GDAL_TIFF_INTERNAL_MASK", "NO"));
}

/************************************************************************/
/*                         CreateMaskBand()                             */
/************************************************************************/

CPLErr GTiffRasterBand::CreateMaskBand(int nFlagsIn)
{
    m_poGDS->ScanDirectories();

    if (m_poGDS->m_poMaskDS != nullptr)
    {
        ReportError(CE_Failure, CPLE_AppDefined,
                    "This TIFF dataset has already an internal mask band");
        return CE_Failure;
    }

    if (CPLTestBool(CPLGetConfigOption("GDAL_TIFF_INTERNAL_MASK", "NO")))
    {
        return m_poGDS->CreateMaskBand(nFlagsIn);
    }

    return GDALPamRasterBand::CreateMaskBand(nFlagsIn);
}

/************************************************************************/
/*                            IsMaskBand()                              */
/************************************************************************/

bool GTiffRasterBand::IsMaskBand() const
{
    return (m_poGDS->m_poImageryDS != nullptr &&
            m_poGDS->m_poImageryDS->m_poMaskDS == m_poGDS) ||
           m_eBandInterp == GCI_AlphaBand ||
           m_poGDS->GetMetadataItem("INTERNAL_MASK_FLAGS_1") != nullptr;
}

/************************************************************************/
/*                         GetMaskValueRange()                          */
/************************************************************************/

GDALMaskValueRange GTiffRasterBand::GetMaskValueRange() const
{
    if (!IsMaskBand())
        return GMVR_UNKNOWN;
    if (m_poGDS->m_nBitsPerSample == 1)
        return m_poGDS->m_bPromoteTo8Bits ? GMVR_0_AND_255_ONLY
                                          : GMVR_0_AND_1_ONLY;
    return GMVR_UNKNOWN;
}

/************************************************************************/
/*                        GetRawBinaryLayout()                          */
/************************************************************************/

bool GTiffDataset::GetRawBinaryLayout(GDALDataset::RawBinaryLayout &sLayout)
{
    if (eAccess == GA_Update)
    {
        FlushCache(false);
        Crystalize();
    }

    if (m_nCompression != COMPRESSION_NONE)
        return false;
    if (!CPLIsPowerOfTwo(m_nBitsPerSample) || m_nBitsPerSample < 8)
        return false;
    const auto eDT = GetRasterBand(1)->GetRasterDataType();
    if (GDALDataTypeIsComplex(eDT))
        return false;

    toff_t *panByteCounts = nullptr;
    toff_t *panOffsets = nullptr;
    const bool bIsTiled = CPL_TO_BOOL(TIFFIsTiled(m_hTIFF));

    if (!((bIsTiled &&
           TIFFGetField(m_hTIFF, TIFFTAG_TILEBYTECOUNTS, &panByteCounts) &&
           TIFFGetField(m_hTIFF, TIFFTAG_TILEOFFSETS, &panOffsets)) ||
          (!bIsTiled &&
           TIFFGetField(m_hTIFF, TIFFTAG_STRIPBYTECOUNTS, &panByteCounts) &&
           TIFFGetField(m_hTIFF, TIFFTAG_STRIPOFFSETS, &panOffsets))))
    {
        return false;
    }

    const int nDTSize = GDALGetDataTypeSizeBytes(eDT);
    vsi_l_offset nImgOffset = panOffsets[0];
    GIntBig nPixelOffset = (m_nPlanarConfig == PLANARCONFIG_CONTIG)
                               ? static_cast<GIntBig>(nDTSize) * nBands
                               : nDTSize;
    GIntBig nLineOffset = nPixelOffset * nRasterXSize;
    GIntBig nBandOffset =
        (m_nPlanarConfig == PLANARCONFIG_CONTIG && nBands > 1) ? nDTSize : 0;
    RawBinaryLayout::Interleaving eInterleaving =
        (nBands == 1) ? RawBinaryLayout::Interleaving::UNKNOWN
        : (m_nPlanarConfig == PLANARCONFIG_CONTIG)
            ? RawBinaryLayout::Interleaving::BIP
            : RawBinaryLayout::Interleaving::BSQ;
    if (bIsTiled)
    {
        // Only a single block tiled file with same dimension as the raster
        // might be acceptable
        if (m_nBlockXSize != nRasterXSize || m_nBlockYSize != nRasterYSize)
            return false;
        if (nBands > 1 && m_nPlanarConfig != PLANARCONFIG_CONTIG)
        {
            nBandOffset = static_cast<GIntBig>(panOffsets[1]) -
                          static_cast<GIntBig>(panOffsets[0]);
            for (int i = 2; i < nBands; i++)
            {
                if (static_cast<GIntBig>(panOffsets[i]) -
                        static_cast<GIntBig>(panOffsets[i - 1]) !=
                    nBandOffset)
                    return false;
            }
        }
    }
    else
    {
        const int nStrips = DIV_ROUND_UP(nRasterYSize, m_nRowsPerStrip);
        if (nBands == 1 || m_nPlanarConfig == PLANARCONFIG_CONTIG)
        {
            vsi_l_offset nLastStripEnd = panOffsets[0] + panByteCounts[0];
            for (int iStrip = 1; iStrip < nStrips; iStrip++)
            {
                if (nLastStripEnd != panOffsets[iStrip])
                    return false;
                nLastStripEnd = panOffsets[iStrip] + panByteCounts[iStrip];
            }
        }
        else
        {
            // Note: we could potentially have BIL order with m_nRowsPerStrip ==
            // 1 and if strips are ordered strip_line_1_band_1, ...,
            // strip_line_1_band_N, strip_line2_band1, ... strip_line2_band_N,
            // etc.... but that'd be faily exotic ! So only detect BSQ layout
            // here
            nBandOffset = static_cast<GIntBig>(panOffsets[nStrips]) -
                          static_cast<GIntBig>(panOffsets[0]);
            for (int i = 0; i < nBands; i++)
            {
                uint32_t iStripOffset = nStrips * i;
                vsi_l_offset nLastStripEnd =
                    panOffsets[iStripOffset] + panByteCounts[iStripOffset];
                for (int iStrip = 1; iStrip < nStrips; iStrip++)
                {
                    if (nLastStripEnd != panOffsets[iStripOffset + iStrip])
                        return false;
                    nLastStripEnd = panOffsets[iStripOffset + iStrip] +
                                    panByteCounts[iStripOffset + iStrip];
                }
                if (i >= 2 && static_cast<GIntBig>(panOffsets[iStripOffset]) -
                                      static_cast<GIntBig>(
                                          panOffsets[iStripOffset - nStrips]) !=
                                  nBandOffset)
                {
                    return false;
                }
            }
        }
    }

    sLayout.osRawFilename = m_pszFilename;
    sLayout.eInterleaving = eInterleaving;
    sLayout.eDataType = eDT;
#ifdef CPL_LSB
    sLayout.bLittleEndianOrder = !TIFFIsByteSwapped(m_hTIFF);
#else
    sLayout.bLittleEndianOrder = TIFFIsByteSwapped(m_hTIFF);
#endif
    sLayout.nImageOffset = nImgOffset;
    sLayout.nPixelOffset = nPixelOffset;
    sLayout.nLineOffset = nLineOffset;
    sLayout.nBandOffset = nBandOffset;

    return true;
}

/************************************************************************/
/*                       PrepareTIFFErrorFormat()                       */
/*                                                                      */
/*      sometimes the "module" has stuff in it that has special         */
/*      meaning in a printf() style format, so we try to escape it.     */
/*      For now we hope the only thing we have to escape is %'s.        */
/************************************************************************/

static char *PrepareTIFFErrorFormat(const char *module, const char *fmt)

{
    const size_t nModuleSize = strlen(module);
    const size_t nModFmtSize = nModuleSize * 2 + strlen(fmt) + 2;
    char *pszModFmt = static_cast<char *>(CPLMalloc(nModFmtSize));

    size_t iOut = 0;  // Used after for.

    for (size_t iIn = 0; iIn < nModuleSize; ++iIn)
    {
        if (module[iIn] == '%')
        {
            CPLAssert(iOut < nModFmtSize - 2);
            pszModFmt[iOut++] = '%';
            pszModFmt[iOut++] = '%';
        }
        else
        {
            CPLAssert(iOut < nModFmtSize - 1);
            pszModFmt[iOut++] = module[iIn];
        }
    }
    CPLAssert(iOut < nModFmtSize);
    pszModFmt[iOut] = '\0';
    strcat(pszModFmt, ":");
    strcat(pszModFmt, fmt);

    return pszModFmt;
}

/************************************************************************/
/*                        GTiffWarningHandler()                         */
/************************************************************************/
static void GTiffWarningHandler(const char *module, const char *fmt, va_list ap)
{
    if (gnThreadLocalLibtiffError > 0)
    {
        gnThreadLocalLibtiffError++;
        if (gnThreadLocalLibtiffError > 10)
            return;
    }

    if (strstr(fmt, "nknown field") != nullptr)
        return;

    char *pszModFmt = PrepareTIFFErrorFormat(module, fmt);
    if (strstr(fmt, "does not end in null byte") != nullptr)
    {
        CPLString osMsg;
        osMsg.vPrintf(pszModFmt, ap);
        CPLDebug("GTiff", "%s", osMsg.c_str());
    }
    else
    {
        CPLErrorV(CE_Warning, CPLE_AppDefined, pszModFmt, ap);
    }
    CPLFree(pszModFmt);
}

/************************************************************************/
/*                         GTiffErrorHandler()                          */
/************************************************************************/
static void GTiffErrorHandler(const char *module, const char *fmt, va_list ap)
{
    if (gnThreadLocalLibtiffError > 0)
    {
        gnThreadLocalLibtiffError++;
        if (gnThreadLocalLibtiffError > 10)
            return;
    }

    if (strcmp(fmt, "Maximum TIFF file size exceeded") == 0)
    {
        // Ideally there would be a thread-safe way of setting this flag,
        // but we cannot really use the extended error handler, since the
        // handler is for all TIFF handles, and not necessarily the ones of
        // this driver.
        if (bGlobalInExternalOvr)
            fmt = "Maximum TIFF file size exceeded. "
                  "Use --config BIGTIFF_OVERVIEW YES configuration option.";
        else
            fmt = "Maximum TIFF file size exceeded. "
                  "Use BIGTIFF=YES creation option.";
    }

    char *pszModFmt = PrepareTIFFErrorFormat(module, fmt);
    CPLErrorV(CE_Failure, CPLE_AppDefined, pszModFmt, ap);
    CPLFree(pszModFmt);
}

/************************************************************************/
/*                          GTiffTagExtender()                          */
/*                                                                      */
/*      Install tags specially known to GDAL.                           */
/************************************************************************/

static TIFFExtendProc _ParentExtender = nullptr;

static void GTiffTagExtender(TIFF *tif)

{
    const TIFFFieldInfo xtiffFieldInfo[] = {
        {TIFFTAG_GDAL_METADATA, -1, -1, TIFF_ASCII, FIELD_CUSTOM, TRUE, FALSE,
         const_cast<char *>("GDALMetadata")},
        {TIFFTAG_GDAL_NODATA, -1, -1, TIFF_ASCII, FIELD_CUSTOM, TRUE, FALSE,
         const_cast<char *>("GDALNoDataValue")},
        {TIFFTAG_RPCCOEFFICIENT, -1, -1, TIFF_DOUBLE, FIELD_CUSTOM, TRUE, TRUE,
         const_cast<char *>("RPCCoefficient")},
        {TIFFTAG_TIFF_RSID, -1, -1, TIFF_ASCII, FIELD_CUSTOM, TRUE, FALSE,
         const_cast<char *>("TIFF_RSID")},
        {TIFFTAG_GEO_METADATA, TIFF_VARIABLE2, TIFF_VARIABLE2, TIFF_BYTE,
         FIELD_CUSTOM, TRUE, TRUE, const_cast<char *>("GEO_METADATA")}};

    if (_ParentExtender)
        (*_ParentExtender)(tif);

    TIFFMergeFieldInfo(tif, xtiffFieldInfo,
                       sizeof(xtiffFieldInfo) / sizeof(xtiffFieldInfo[0]));
}

/************************************************************************/
/*                          GTiffOneTimeInit()                          */
/*                                                                      */
/*      This is stuff that is initialized for the TIFF library just     */
/*      once.  We deliberately defer the initialization till the        */
/*      first time we are likely to call into libtiff to avoid          */
/*      unnecessary paging in of the library for GDAL apps that         */
/*      don't use it.                                                   */
/************************************************************************/

static std::mutex oDeleteMutex;
#ifdef HAVE_JXL
static TIFFCodec *pJXLCodec = nullptr;
#endif

int GTiffOneTimeInit()

{
    std::lock_guard<std::mutex> oLock(oDeleteMutex);

#ifdef HAVE_JXL
    if (pJXLCodec == nullptr)
    {
        pJXLCodec = TIFFRegisterCODEC(COMPRESSION_JXL, "JXL", TIFFInitJXL);
    }
#endif

    static bool bOneTimeInitDone = false;
    if (bOneTimeInitDone)
        return TRUE;

    bOneTimeInitDone = true;

    _ParentExtender = TIFFSetTagExtender(GTiffTagExtender);

    TIFFSetWarningHandler(GTiffWarningHandler);
    TIFFSetErrorHandler(GTiffErrorHandler);

    LibgeotiffOneTimeInit();

    return TRUE;
}

/************************************************************************/
/*                        GDALDeregister_GTiff()                        */
/************************************************************************/

static void GDALDeregister_GTiff(GDALDriver *)

{
#ifdef HAVE_JXL
    if (pJXLCodec)
        TIFFUnRegisterCODEC(pJXLCodec);
    pJXLCodec = nullptr;
#endif
}

/************************************************************************/
/*                   GTIFFGetCompressionMethod()                        */
/************************************************************************/

int GTIFFGetCompressionMethod(const char *pszValue, const char *pszVariableName)
{
    int nCompression = COMPRESSION_NONE;
    bool bFoundMatch = false;
    for (const auto &entry : asCompressionNames)
    {
        if (entry.bWriteSupported && EQUAL(entry.pszText, pszValue))
        {
            bFoundMatch = true;
            nCompression = entry.nCode;
            break;
        }
    }

    if (!bFoundMatch)
    {
        CPLError(CE_Warning, CPLE_IllegalArg,
                 "%s=%s value not recognised, ignoring.", pszVariableName,
                 pszValue);
    }

    if (nCompression != COMPRESSION_NONE &&
        !TIFFIsCODECConfigured(static_cast<uint16_t>(nCompression)))
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot create TIFF file due to missing codec for %s.",
                 pszValue);
        return -1;
    }

    return nCompression;
}

/************************************************************************/
/*                     GTiffGetCompressValues()                         */
/************************************************************************/

CPLString GTiffGetCompressValues(bool &bHasLZW, bool &bHasDEFLATE,
                                 bool &bHasLZMA, bool &bHasZSTD, bool &bHasJPEG,
                                 bool &bHasWebP, bool &bHasLERC, bool bForCOG)
{
    bHasLZW = false;
    bHasDEFLATE = false;
    bHasLZMA = false;
    bHasZSTD = false;
    bHasJPEG = false;
    bHasWebP = false;
    bHasLERC = false;

    /* -------------------------------------------------------------------- */
    /*      Determine which compression codecs are available that we        */
    /*      want to advertise.  If we are using an old libtiff we won't     */
    /*      be able to find out so we just assume all are available.        */
    /* -------------------------------------------------------------------- */
    CPLString osCompressValues = "       <Value>NONE</Value>";

    TIFFCodec *codecs = TIFFGetConfiguredCODECs();

    for (TIFFCodec *c = codecs; c->name; ++c)
    {
        if (c->scheme == COMPRESSION_PACKBITS && !bForCOG)
        {
            osCompressValues += "       <Value>PACKBITS</Value>";
        }
        else if (c->scheme == COMPRESSION_JPEG)
        {
            bHasJPEG = true;
            osCompressValues += "       <Value>JPEG</Value>";
        }
        else if (c->scheme == COMPRESSION_LZW)
        {
            bHasLZW = true;
            osCompressValues += "       <Value>LZW</Value>";
        }
        else if (c->scheme == COMPRESSION_ADOBE_DEFLATE)
        {
            bHasDEFLATE = true;
            osCompressValues += "       <Value>DEFLATE</Value>";
        }
        else if (c->scheme == COMPRESSION_CCITTRLE && !bForCOG)
        {
            osCompressValues += "       <Value>CCITTRLE</Value>";
        }
        else if (c->scheme == COMPRESSION_CCITTFAX3 && !bForCOG)
        {
            osCompressValues += "       <Value>CCITTFAX3</Value>";
        }
        else if (c->scheme == COMPRESSION_CCITTFAX4 && !bForCOG)
        {
            osCompressValues += "       <Value>CCITTFAX4</Value>";
        }
        else if (c->scheme == COMPRESSION_LZMA)
        {
            bHasLZMA = true;
            osCompressValues += "       <Value>LZMA</Value>";
        }
        else if (c->scheme == COMPRESSION_ZSTD)
        {
            bHasZSTD = true;
            osCompressValues += "       <Value>ZSTD</Value>";
        }
        else if (c->scheme == COMPRESSION_WEBP)
        {
            bHasWebP = true;
            osCompressValues += "       <Value>WEBP</Value>";
        }
        else if (c->scheme == COMPRESSION_LERC)
        {
            bHasLERC = true;
        }
    }
    if (bHasLERC)
    {
        osCompressValues += "       <Value>LERC</Value>"
                            "       <Value>LERC_DEFLATE</Value>";
        if (bHasZSTD)
        {
            osCompressValues += "       <Value>LERC_ZSTD</Value>";
        }
    }
#ifdef HAVE_JXL
    osCompressValues += "       <Value>JXL</Value>";
#endif
    _TIFFfree(codecs);

    return osCompressValues;
}

/************************************************************************/
/*                          GDALRegister_GTiff()                        */
/************************************************************************/

void GDALRegister_GTiff()

{
    if (GDALGetDriverByName("GTiff") != nullptr)
        return;

    CPLString osOptions;

    bool bHasLZW = false;
    bool bHasDEFLATE = false;
    bool bHasLZMA = false;
    bool bHasZSTD = false;
    bool bHasJPEG = false;
    bool bHasWebP = false;
    bool bHasLERC = false;
    CPLString osCompressValues(GTiffGetCompressValues(
        bHasLZW, bHasDEFLATE, bHasLZMA, bHasZSTD, bHasJPEG, bHasWebP, bHasLERC,
        false /* bForCOG */));

    GDALDriver *poDriver = new GDALDriver();

    /* -------------------------------------------------------------------- */
    /*      Build full creation option list.                                */
    /* -------------------------------------------------------------------- */
    osOptions = "<CreationOptionList>"
                "   <Option name='COMPRESS' type='string-select'>";
    osOptions += osCompressValues;
    osOptions += "   </Option>";
    if (bHasLZW || bHasDEFLATE || bHasZSTD)
        osOptions += ""
                     "   <Option name='PREDICTOR' type='int' "
                     "description='Predictor Type (1=default, 2=horizontal "
                     "differencing, 3=floating point prediction)'/>";
    osOptions +=
        ""
        "   <Option name='DISCARD_LSB' type='string' description='Number of "
        "least-significant bits to set to clear as a single value or "
        "comma-separated list of values for per-band values'/>";
    if (bHasJPEG)
    {
        osOptions +=
            ""
            "   <Option name='JPEG_QUALITY' type='int' description='JPEG "
            "quality 1-100' default='75'/>"
            "   <Option name='JPEGTABLESMODE' type='int' description='Content "
            "of JPEGTABLES tag. 0=no JPEGTABLES tag, 1=Quantization tables "
            "only, 2=Huffman tables only, 3=Both' default='1'/>";
#ifdef JPEG_DIRECT_COPY
        osOptions +=
            ""
            "   <Option name='JPEG_DIRECT_COPY' type='boolean' description='To "
            "copy without any decompression/recompression a JPEG source file' "
            "default='NO'/>";
#endif
    }
    if (bHasDEFLATE)
    {
#ifdef LIBDEFLATE_SUPPORT
        osOptions += ""
                     "   <Option name='ZLEVEL' type='int' description='DEFLATE "
                     "compression level 1-12' default='6'/>";
#else
        osOptions += ""
                     "   <Option name='ZLEVEL' type='int' description='DEFLATE "
                     "compression level 1-9' default='6'/>";
#endif
    }
    if (bHasLZMA)
        osOptions +=
            ""
            "   <Option name='LZMA_PRESET' type='int' description='LZMA "
            "compression level 0(fast)-9(slow)' default='6'/>";
    if (bHasZSTD)
        osOptions +=
            ""
            "   <Option name='ZSTD_LEVEL' type='int' description='ZSTD "
            "compression level 1(fast)-22(slow)' default='9'/>";
    if (bHasLERC)
        osOptions +=
            ""
            "   <Option name='MAX_Z_ERROR' type='float' description='Maximum "
            "error for LERC compression' default='0'/>";
    if (bHasWebP)
    {
        osOptions +=
            ""
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
            "   <Option name='WEBP_LOSSLESS' type='boolean' "
            "description='Whether lossless compression should be used' "
            "default='FALSE'/>"
#endif
            "   <Option name='WEBP_LEVEL' type='int' description='WEBP quality "
            "level. Low values result in higher compression ratios' "
            "default='" XSTRINGIFY(DEFAULT_WEBP_LEVEL) "'/>";
    }
#ifdef HAVE_JXL
    osOptions +=
        ""
        "   <Option name='JXL_LOSSLESS' type='boolean' description='Whether "
        "JPEGXL compression should be lossless' default='YES'/>"
        "   <Option name='JXL_EFFORT' type='int' description='Level of effort "
        "1(fast)-9(slow)' default='5'/>"
        "   <Option name='JXL_DISTANCE' type='float' description='Distance "
        "level for lossy compression (0=mathematically lossless, 1.0=visually "
        "lossless, usual range [0.5,3])' default='1.0' min='0.1' max='15.0'/>";
#endif
    osOptions +=
        ""
        "   <Option name='NUM_THREADS' type='string' description='Number of "
        "worker threads for compression. Can be set to ALL_CPUS' default='1'/>"
        "   <Option name='NBITS' type='int' description='BITS for sub-byte "
        "files (1-7), sub-uint16_t (9-15), sub-uint32_t (17-31), or float32 "
        "(16)'/>"
        "   <Option name='INTERLEAVE' type='string-select' default='PIXEL'>"
        "       <Value>BAND</Value>"
        "       <Value>PIXEL</Value>"
        "   </Option>"
        "   <Option name='TILED' type='boolean' description='Switch to tiled "
        "format'/>"
        "   <Option name='TFW' type='boolean' description='Write out world "
        "file'/>"
        "   <Option name='RPB' type='boolean' description='Write out .RPB "
        "(RPC) file'/>"
        "   <Option name='RPCTXT' type='boolean' description='Write out "
        "_RPC.TXT file'/>"
        "   <Option name='BLOCKXSIZE' type='int' description='Tile Width'/>"
        "   <Option name='BLOCKYSIZE' type='int' description='Tile/Strip "
        "Height'/>"
        "   <Option name='PHOTOMETRIC' type='string-select'>"
        "       <Value>MINISBLACK</Value>"
        "       <Value>MINISWHITE</Value>"
        "       <Value>PALETTE</Value>"
        "       <Value>RGB</Value>"
        "       <Value>CMYK</Value>"
        "       <Value>YCBCR</Value>"
        "       <Value>CIELAB</Value>"
        "       <Value>ICCLAB</Value>"
        "       <Value>ITULAB</Value>"
        "   </Option>"
        "   <Option name='SPARSE_OK' type='boolean' description='Should empty "
        "blocks be omitted on disk?' default='FALSE'/>"
        "   <Option name='ALPHA' type='string-select' description='Mark first "
        "extrasample as being alpha'>"
        "       <Value>NON-PREMULTIPLIED</Value>"
        "       <Value>PREMULTIPLIED</Value>"
        "       <Value>UNSPECIFIED</Value>"
        "       <Value aliasOf='NON-PREMULTIPLIED'>YES</Value>"
        "       <Value aliasOf='UNSPECIFIED'>NO</Value>"
        "   </Option>"
        "   <Option name='PROFILE' type='string-select' default='GDALGeoTIFF'>"
        "       <Value>GDALGeoTIFF</Value>"
        "       <Value>GeoTIFF</Value>"
        "       <Value>BASELINE</Value>"
        "   </Option>"
        "   <Option name='PIXELTYPE' type='string-select'>"
        "       <Value>DEFAULT</Value>"
        "       <Value>SIGNEDBYTE</Value>"
        "   </Option>"
        "   <Option name='BIGTIFF' type='string-select' description='Force "
        "creation of BigTIFF file'>"
        "     <Value>YES</Value>"
        "     <Value>NO</Value>"
        "     <Value>IF_NEEDED</Value>"
        "     <Value>IF_SAFER</Value>"
        "   </Option>"
        "   <Option name='ENDIANNESS' type='string-select' default='NATIVE' "
        "description='Force endianness of created file. For DEBUG purpose "
        "mostly'>"
        "       <Value>NATIVE</Value>"
        "       <Value>INVERTED</Value>"
        "       <Value>LITTLE</Value>"
        "       <Value>BIG</Value>"
        "   </Option>"
        "   <Option name='COPY_SRC_OVERVIEWS' type='boolean' default='NO' "
        "description='Force copy of overviews of source dataset "
        "(CreateCopy())'/>"
        "   <Option name='SOURCE_ICC_PROFILE' type='string' description='ICC "
        "profile'/>"
        "   <Option name='SOURCE_PRIMARIES_RED' type='string' "
        "description='x,y,1.0 (xyY) red chromaticity'/>"
        "   <Option name='SOURCE_PRIMARIES_GREEN' type='string' "
        "description='x,y,1.0 (xyY) green chromaticity'/>"
        "   <Option name='SOURCE_PRIMARIES_BLUE' type='string' "
        "description='x,y,1.0 (xyY) blue chromaticity'/>"
        "   <Option name='SOURCE_WHITEPOINT' type='string' "
        "description='x,y,1.0 (xyY) whitepoint'/>"
        "   <Option name='TIFFTAG_TRANSFERFUNCTION_RED' type='string' "
        "description='Transfer function for red'/>"
        "   <Option name='TIFFTAG_TRANSFERFUNCTION_GREEN' type='string' "
        "description='Transfer function for green'/>"
        "   <Option name='TIFFTAG_TRANSFERFUNCTION_BLUE' type='string' "
        "description='Transfer function for blue'/>"
        "   <Option name='TIFFTAG_TRANSFERRANGE_BLACK' type='string' "
        "description='Transfer range for black'/>"
        "   <Option name='TIFFTAG_TRANSFERRANGE_WHITE' type='string' "
        "description='Transfer range for white'/>"
        "   <Option name='STREAMABLE_OUTPUT' type='boolean' default='NO' "
        "description='Enforce a mode compatible with a streamable file'/>"
        "   <Option name='GEOTIFF_KEYS_FLAVOR' type='string-select' "
        "default='STANDARD' description='Which flavor of GeoTIFF keys must be "
        "used'>"
        "       <Value>STANDARD</Value>"
        "       <Value>ESRI_PE</Value>"
        "   </Option>"
#if LIBGEOTIFF_VERSION >= 1600
        "   <Option name='GEOTIFF_VERSION' type='string-select' default='AUTO' "
        "description='Which version of GeoTIFF must be used'>"
        "       <Value>AUTO</Value>"
        "       <Value>1.0</Value>"
        "       <Value>1.1</Value>"
        "   </Option>"
#endif
        "</CreationOptionList>";

    /* -------------------------------------------------------------------- */
    /*      Set the driver details.                                         */
    /* -------------------------------------------------------------------- */
    poDriver->SetDescription("GTiff");
    poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
    poDriver->SetMetadataItem(GDAL_DMD_LONGNAME, "GeoTIFF");
    poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/raster/gtiff.html");
    poDriver->SetMetadataItem(GDAL_DMD_MIMETYPE, "image/tiff");
    poDriver->SetMetadataItem(GDAL_DMD_EXTENSION, "tif");
    poDriver->SetMetadataItem(GDAL_DMD_EXTENSIONS, "tif tiff");
    poDriver->SetMetadataItem(GDAL_DMD_CREATIONDATATYPES,
                              "Byte UInt16 Int16 UInt32 Int32 Float32 "
                              "Float64 CInt16 CInt32 CFloat32 CFloat64");
    poDriver->SetMetadataItem(GDAL_DMD_CREATIONOPTIONLIST, osOptions);
    poDriver->SetMetadataItem(
        GDAL_DMD_OPENOPTIONLIST,
        "<OpenOptionList>"
        "   <Option name='NUM_THREADS' type='string' description='Number of "
        "worker threads for compression. Can be set to ALL_CPUS' default='1'/>"
        "   <Option name='GEOTIFF_KEYS_FLAVOR' type='string-select' "
        "default='STANDARD' description='Which flavor of GeoTIFF keys must be "
        "used (for writing)'>"
        "       <Value>STANDARD</Value>"
        "       <Value>ESRI_PE</Value>"
        "   </Option>"
        "   <Option name='GEOREF_SOURCES' type='string' description='Comma "
        "separated list made with values INTERNAL/TABFILE/WORLDFILE/PAM/NONE "
        "that describe the priority order for georeferencing' "
        "default='PAM,INTERNAL,TABFILE,WORLDFILE'/>"
        "   <Option name='SPARSE_OK' type='boolean' description='Should empty "
        "blocks be omitted on disk?' default='FALSE'/>"
        "</OpenOptionList>");
    poDriver->SetMetadataItem(GDAL_DMD_SUBDATASETS, "YES");
    poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");

#ifdef INTERNAL_LIBTIFF
    poDriver->SetMetadataItem("LIBTIFF", "INTERNAL");
#else
    poDriver->SetMetadataItem("LIBTIFF", TIFFLIB_VERSION_STR);
#endif

    poDriver->SetMetadataItem("LIBGEOTIFF", XSTRINGIFY(LIBGEOTIFF_VERSION));

    poDriver->SetMetadataItem(GDAL_DCAP_COORDINATE_EPOCH, "YES");

    poDriver->pfnOpen = GTiffDataset::Open;
    poDriver->pfnCreate = GTiffDataset::Create;
    poDriver->pfnCreateCopy = GTiffDataset::CreateCopy;
    poDriver->pfnUnloadDriver = GDALDeregister_GTiff;
    poDriver->pfnIdentify = GTiffDataset::Identify;

    GetGDALDriverManager()->RegisterDriver(poDriver);
}
