/******************************************************************************
 *
 * Project:  GDAL WEBP Driver
 * Purpose:  Implement GDAL WEBP Support based on libwebp
 * Author:   Even Rouault, <even dot rouault at spatialys.com>
 *
 ******************************************************************************
 * Copyright (c) 2011-2013, Even Rouault <even dot rouault at spatialys.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_string.h"
#include "gdal_frmts.h"
#include "gdal_pam.h"

#include "webp_headers.h"

CPL_CVSID("$Id$")

/************************************************************************/
/* ==================================================================== */
/*                               WEBPDataset                            */
/* ==================================================================== */
/************************************************************************/

class WEBPRasterBand;

class WEBPDataset final: public GDALPamDataset
{
    friend class WEBPRasterBand;

    VSILFILE*   fpImage;
    GByte* pabyUncompressed;
    int    bHasBeenUncompressed;
    CPLErr eUncompressErrRet;
    CPLErr Uncompress();

    int    bHasReadXMPMetadata;

  public:
                 WEBPDataset();
    virtual ~WEBPDataset();

    virtual CPLErr      IRasterIO( GDALRWFlag, int, int, int, int,
                                   void *, int, int, GDALDataType,
                                   int, int *,
                                   GSpacing nPixelSpace, GSpacing nLineSpace,
                                   GSpacing nBandSpace,
                                   GDALRasterIOExtraArg* psExtraArg) override;

    virtual char      **GetMetadataDomainList() override;
    virtual char  **GetMetadata( const char * pszDomain = "" ) override;

    static GDALDataset *Open( GDALOpenInfo * );
    static int          Identify( GDALOpenInfo * );
    static GDALDataset* CreateCopy( const char * pszFilename,
                                    GDALDataset *poSrcDS,
                                    int bStrict, char ** papszOptions,
                                    GDALProgressFunc pfnProgress,
                                    void * pProgressData );
};

/************************************************************************/
/* ==================================================================== */
/*                            WEBPRasterBand                            */
/* ==================================================================== */
/************************************************************************/

class WEBPRasterBand final: public GDALPamRasterBand
{
    friend class WEBPDataset;

  public:
                   WEBPRasterBand( WEBPDataset *, int );

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

/************************************************************************/
/*                          WEBPRasterBand()                            */
/************************************************************************/

WEBPRasterBand::WEBPRasterBand( WEBPDataset *poDSIn, int )
{
    poDS = poDSIn;

    eDataType = GDT_Byte;

    nBlockXSize = poDSIn->nRasterXSize;
    nBlockYSize = 1;
}

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

CPLErr WEBPRasterBand::IReadBlock( CPL_UNUSED int nBlockXOff, int nBlockYOff,
                                   void * pImage )
{
    WEBPDataset* poGDS = reinterpret_cast<WEBPDataset *>( poDS );

    if( poGDS->Uncompress() != CE_None )
        return CE_Failure;

    GByte* pabyUncompressed =
        &poGDS->pabyUncompressed[nBlockYOff * nRasterXSize  * poGDS->nBands
                                 + nBand - 1];
    for( int i = 0; i < nRasterXSize; i++ )
        reinterpret_cast<GByte*>( pImage )[i]
            = pabyUncompressed[poGDS->nBands * i];

    return CE_None;
}

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

GDALColorInterp WEBPRasterBand::GetColorInterpretation()

{
    if ( nBand == 1 )
        return GCI_RedBand;

    else if( nBand == 2 )
        return GCI_GreenBand;

    else if ( nBand == 3 )
        return GCI_BlueBand;

    return GCI_AlphaBand;
}

/************************************************************************/
/* ==================================================================== */
/*                             WEBPDataset                               */
/* ==================================================================== */
/************************************************************************/

/************************************************************************/
/*                            WEBPDataset()                              */
/************************************************************************/

WEBPDataset::WEBPDataset() :
    fpImage(nullptr),
    pabyUncompressed(nullptr),
    bHasBeenUncompressed(FALSE),
    eUncompressErrRet(CE_None),
    bHasReadXMPMetadata(FALSE)
{}

/************************************************************************/
/*                           ~WEBPDataset()                             */
/************************************************************************/

WEBPDataset::~WEBPDataset()

{
    FlushCache(true);
    if (fpImage)
        VSIFCloseL(fpImage);
    VSIFree(pabyUncompressed);
}

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

char **WEBPDataset::GetMetadataDomainList()
{
    return BuildMetadataDomainList(GDALPamDataset::GetMetadataDomainList(),
                                   TRUE,
                                   "xml:XMP", nullptr);
}

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

char  **WEBPDataset::GetMetadata( const char * pszDomain )
{
    if( (pszDomain != nullptr && EQUAL(pszDomain, "xml:XMP") )
        && !bHasReadXMPMetadata)
    {
        bHasReadXMPMetadata = TRUE;

        VSIFSeekL(fpImage, 12, SEEK_SET);

        bool bFirst = true;
        while( true )
        {
            char szHeader[5];
            GUInt32 nChunkSize;

            if (VSIFReadL(szHeader, 1, 4, fpImage) != 4 ||
                VSIFReadL(&nChunkSize, 1, 4, fpImage) != 4)
                break;

            szHeader[4] = '\0';
            CPL_LSBPTR32(&nChunkSize);

            if (bFirst)
            {
                if (strcmp(szHeader, "VP8X") != 0 || nChunkSize < 10)
                    break;

                int l_nFlags;
                if (VSIFReadL(&l_nFlags, 1, 4, fpImage) != 4)
                    break;
                CPL_LSBPTR32(&l_nFlags);
                if ((l_nFlags & 8) == 0)
                    break;

                VSIFSeekL(fpImage, nChunkSize - 4, SEEK_CUR);

                bFirst = false;
            }
            else if (strcmp(szHeader, "META") == 0)
            {
                if (nChunkSize > 1024 * 1024)
                    break;

                char* pszXMP
                    = reinterpret_cast<char*>( VSIMalloc(nChunkSize + 1) );
                if (pszXMP == nullptr)
                    break;

                if( static_cast<GUInt32>( VSIFReadL(pszXMP, 1, nChunkSize,
                                                    fpImage) )
                    != nChunkSize )
                {
                    VSIFree(pszXMP);
                    break;
                }
                pszXMP[nChunkSize] = '\0';

                /* Avoid setting the PAM dirty bit just for that */
                const int nOldPamFlags = nPamFlags;

                char *apszMDList[2] = { pszXMP, nullptr };
                SetMetadata(apszMDList, "xml:XMP");

                nPamFlags = nOldPamFlags;

                VSIFree(pszXMP);
                break;
            }
            else
                VSIFSeekL(fpImage, nChunkSize, SEEK_CUR);
        }
    }

    return GDALPamDataset::GetMetadata(pszDomain);
}

/************************************************************************/
/*                            Uncompress()                              */
/************************************************************************/

CPLErr WEBPDataset::Uncompress()
{
    if (bHasBeenUncompressed)
        return eUncompressErrRet;

    bHasBeenUncompressed = TRUE;
    eUncompressErrRet = CE_Failure;

    // To avoid excessive memory allocation attempts
    // Normally WebP images are no larger than 16383x16383*4 ~= 1 GB
    if( nRasterXSize > INT_MAX / (nRasterYSize * nBands) )
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "Too large image");
        return CE_Failure;
    }

    pabyUncompressed = reinterpret_cast<GByte*>(
        VSIMalloc3(nRasterXSize, nRasterYSize, nBands ) );
    if (pabyUncompressed == nullptr)
        return CE_Failure;

    VSIFSeekL(fpImage, 0, SEEK_END);
    vsi_l_offset nSizeLarge = VSIFTellL(fpImage);
    if( nSizeLarge != static_cast<vsi_l_offset>( static_cast<uint32_t>( nSizeLarge ) ) )
        return CE_Failure;
    VSIFSeekL(fpImage, 0, SEEK_SET);
    uint32_t nSize = static_cast<uint32_t>( nSizeLarge );
    uint8_t* pabyCompressed = reinterpret_cast<uint8_t*>( VSIMalloc(nSize) );
    if (pabyCompressed == nullptr)
        return CE_Failure;
    VSIFReadL(pabyCompressed, 1, nSize, fpImage);
    uint8_t* pRet;

    if (nBands == 4)
        pRet = WebPDecodeRGBAInto(
            pabyCompressed,
            static_cast<uint32_t>( nSize ),
            static_cast<uint8_t*>( pabyUncompressed),
            nRasterXSize * nRasterYSize * nBands,
            nRasterXSize * nBands );
    else
        pRet = WebPDecodeRGBInto(
            pabyCompressed,
            static_cast<uint32_t>( nSize ),
            static_cast<uint8_t*>( pabyUncompressed ),
            nRasterXSize * nRasterYSize * nBands,
            nRasterXSize * nBands );

    VSIFree(pabyCompressed);
    if (pRet == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                    "WebPDecodeRGBInto() failed");
        return CE_Failure;
    }
    eUncompressErrRet = CE_None;

    return CE_None;
}

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

CPLErr WEBPDataset::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 )

{
    if((eRWFlag == GF_Read) &&
       (nBandCount == nBands) &&
       (nXOff == 0) &&
       (nYOff == 0) &&
       (nXSize == nBufXSize) && (nXSize == nRasterXSize) &&
       (nYSize == nBufYSize) && (nYSize == nRasterYSize) &&
       (eBufType == GDT_Byte) &&
       (pData != nullptr) &&
       (panBandMap[0] == 1) && (panBandMap[1] == 2) && (panBandMap[2] == 3) &&
       (nBands == 3 || panBandMap[3] == 4))
    {
        if( Uncompress() != CE_None )
            return CE_Failure;
        if( nPixelSpace == nBands && nLineSpace == (nPixelSpace*nXSize) &&
            nBandSpace == 1 )
        {
            memcpy(pData, pabyUncompressed, nBands * nXSize * nYSize);
        }
        else
        {
            for(int y = 0; y < nYSize; ++y)
            {
                GByte* pabyScanline = pabyUncompressed + y * nBands * nXSize;
                for(int x = 0; x < nXSize; ++x)
                {
                    for(int iBand=0;iBand<nBands;iBand++)
                        reinterpret_cast<GByte *>(
                            pData )[(y * nLineSpace) + (x * nPixelSpace) +
                                    iBand * nBandSpace]
                            = pabyScanline[x * nBands + iBand];
                }
            }
        }

        return CE_None;
    }

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

/************************************************************************/
/*                              Identify()                              */
/************************************************************************/

int WEBPDataset::Identify( GDALOpenInfo * poOpenInfo )

{
    int    nHeaderBytes = poOpenInfo->nHeaderBytes;

    GByte  *pabyHeader = poOpenInfo->pabyHeader;

    if( nHeaderBytes < 20 )
        return FALSE;

    return memcmp(pabyHeader, "RIFF", 4) == 0 &&
           memcmp(pabyHeader + 8, "WEBP", 4) == 0 &&
           (memcmp(pabyHeader + 12, "VP8 ", 4) == 0 ||
            memcmp(pabyHeader + 12, "VP8L", 4) == 0 ||
            memcmp(pabyHeader + 12, "VP8X", 4) == 0);
}

/************************************************************************/
/*                                Open()                                */
/************************************************************************/

GDALDataset *WEBPDataset::Open( GDALOpenInfo * poOpenInfo )

{
    if( !Identify( poOpenInfo ) || poOpenInfo->fpL == nullptr )
        return nullptr;

    int nWidth, nHeight;
    if( !WebPGetInfo(reinterpret_cast<const uint8_t*>( poOpenInfo->pabyHeader ),
                     static_cast<uint32_t>( poOpenInfo->nHeaderBytes ),
                     &nWidth, &nHeight))
        return nullptr;

    int nBands = 3;

#if WEBP_DECODER_ABI_VERSION >= 0x0002
    WebPDecoderConfig config;
    if( !WebPInitDecoderConfig(&config) )
        return nullptr;

    const bool bOK
        = WebPGetFeatures(poOpenInfo->pabyHeader,
                          poOpenInfo->nHeaderBytes, &config.input)
        == VP8_STATUS_OK;

    if (config.input.has_alpha)
        nBands = 4;

     WebPFreeDecBuffer(&config.output);

    if (!bOK)
        return nullptr;

#endif

    if( poOpenInfo->eAccess == GA_Update )
    {
        CPLError( CE_Failure, CPLE_NotSupported,
                  "The WEBP driver does not support update access to existing"
                  " datasets.\n" );
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Create a corresponding GDALDataset.                             */
/* -------------------------------------------------------------------- */
    WEBPDataset *poDS = new WEBPDataset();
    poDS->nRasterXSize = nWidth;
    poDS->nRasterYSize = nHeight;
    poDS->fpImage = poOpenInfo->fpL;
    poOpenInfo->fpL = nullptr;

/* -------------------------------------------------------------------- */
/*      Create band information objects.                                */
/* -------------------------------------------------------------------- */
    for( int iBand = 0; iBand < nBands; iBand++ )
        poDS->SetBand( iBand+1, new WEBPRasterBand( poDS, iBand+1 ) );

/* -------------------------------------------------------------------- */
/*      Initialize any PAM information.                                 */
/* -------------------------------------------------------------------- */
    poDS->SetDescription( poOpenInfo->pszFilename );

    poDS->TryLoadXML( poOpenInfo->GetSiblingFiles() );

/* -------------------------------------------------------------------- */
/*      Open overviews.                                                 */
/* -------------------------------------------------------------------- */
    poDS->oOvManager.Initialize(
        poDS, poOpenInfo->pszFilename, poOpenInfo->GetSiblingFiles() );

    return poDS;
}

/************************************************************************/
/*                              WebPUserData                            */
/************************************************************************/

typedef struct
{
    VSILFILE         *fp;
    GDALProgressFunc  pfnProgress;
    void             *pProgressData;
} WebPUserData;

/************************************************************************/
/*                         WEBPDatasetWriter()                          */
/************************************************************************/

static
int WEBPDatasetWriter(const uint8_t* data, size_t data_size,
                      const WebPPicture* const picture)
{
    WebPUserData* pUserData
        = reinterpret_cast<WebPUserData *>( picture->custom_ptr );
    return VSIFWriteL(data, 1, data_size, pUserData->fp) == data_size;
}

/************************************************************************/
/*                        WEBPDatasetProgressHook()                     */
/************************************************************************/

#if WEBP_ENCODER_ABI_VERSION >= 0x0100
static
int WEBPDatasetProgressHook(int percent, const WebPPicture* const picture)
{
    WebPUserData* pUserData
      = reinterpret_cast<WebPUserData *>( picture->custom_ptr );
    return pUserData->pfnProgress(
        percent / 100.0, nullptr, pUserData->pProgressData );
}
#endif

/************************************************************************/
/*                              CreateCopy()                            */
/************************************************************************/

GDALDataset *
WEBPDataset::CreateCopy( const char * pszFilename, GDALDataset *poSrcDS,
                        int bStrict, char ** papszOptions,
                        GDALProgressFunc pfnProgress, void * pProgressData )

{

/* -------------------------------------------------------------------- */
/*      WEBP library initialization                                     */
/* -------------------------------------------------------------------- */

    WebPPicture sPicture;
    if (!WebPPictureInit(&sPicture))
    {
        CPLError(CE_Failure, CPLE_AppDefined, "WebPPictureInit() failed");
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Some some rudimentary checks                                    */
/* -------------------------------------------------------------------- */

    const int nXSize = poSrcDS->GetRasterXSize();
    const int nYSize = poSrcDS->GetRasterYSize();
    if( nXSize > 16383 || nYSize > 16383 )
    {
        CPLError( CE_Failure, CPLE_NotSupported,
                  "WEBP maximum image dimensions are 16383 x 16383.");

        return nullptr;
    }

    const int nBands = poSrcDS->GetRasterCount();
    if( nBands != 3
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
        && nBands != 4
#endif
        )
    {
        CPLError( CE_Failure, CPLE_NotSupported,
                  "WEBP driver doesn't support %d bands. Must be 3 (RGB) "
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
                  "or 4 (RGBA) "
#endif
                  "bands.",
                  nBands );

        return nullptr;
    }

    const GDALDataType eDT = poSrcDS->GetRasterBand(1)->GetRasterDataType();

    if( eDT != GDT_Byte )
    {
        CPLError( (bStrict) ? CE_Failure : CE_Warning, CPLE_NotSupported,
                  "WEBP driver doesn't support data type %s. "
                  "Only eight bit byte bands supported.",
                  GDALGetDataTypeName(
                      poSrcDS->GetRasterBand(1)->GetRasterDataType()) );

        if (bStrict)
            return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      What options has the user selected?                             */
/* -------------------------------------------------------------------- */
    float fQuality = 75.0f;
    const char* pszQUALITY = CSLFetchNameValue(papszOptions, "QUALITY");
    if( pszQUALITY != nullptr )
    {
        fQuality = static_cast<float>( CPLAtof(pszQUALITY) );
        if( fQuality < 0.0f || fQuality > 100.0f )
        {
            CPLError( CE_Failure, CPLE_IllegalArg,
                      "%s=%s is not a legal value.", "QUALITY", pszQUALITY);
            return nullptr;
        }
    }

    WebPPreset nPreset = WEBP_PRESET_DEFAULT;
    const char* pszPRESET = CSLFetchNameValueDef(
        papszOptions, "PRESET", "DEFAULT" );
    if (EQUAL(pszPRESET, "DEFAULT"))
        nPreset = WEBP_PRESET_DEFAULT;
    else if (EQUAL(pszPRESET, "PICTURE"))
        nPreset = WEBP_PRESET_PICTURE;
    else if (EQUAL(pszPRESET, "PHOTO"))
        nPreset = WEBP_PRESET_PHOTO;
    else if (EQUAL(pszPRESET, "PICTURE"))
        nPreset = WEBP_PRESET_PICTURE;
    else if (EQUAL(pszPRESET, "DRAWING"))
        nPreset = WEBP_PRESET_DRAWING;
    else if (EQUAL(pszPRESET, "ICON"))
        nPreset = WEBP_PRESET_ICON;
    else if (EQUAL(pszPRESET, "TEXT"))
        nPreset = WEBP_PRESET_TEXT;
    else
    {
        CPLError( CE_Failure, CPLE_IllegalArg,
                  "%s=%s is not a legal value.", "PRESET", pszPRESET );
        return nullptr;
    }

    WebPConfig sConfig;
    if (!WebPConfigInitInternal(&sConfig, nPreset, fQuality,
                                WEBP_ENCODER_ABI_VERSION))
    {
        CPLError(CE_Failure, CPLE_AppDefined, "WebPConfigInit() failed");
        return nullptr;
    }

    // TODO: Get rid of this macro in a reasonable way.
#define FETCH_AND_SET_OPTION_INT(name, fieldname, minval, maxval) \
{ \
    const char* pszVal = CSLFetchNameValue(papszOptions, name); \
    if (pszVal != nullptr) \
    { \
        sConfig.fieldname = atoi(pszVal); \
        if (sConfig.fieldname < minval || sConfig.fieldname > maxval) \
        { \
            CPLError( CE_Failure, CPLE_IllegalArg, \
                      "%s=%s is not a legal value.", name, pszVal ); \
            return nullptr; \
        } \
    } \
}

    FETCH_AND_SET_OPTION_INT("TARGETSIZE", target_size, 0, INT_MAX-1);

    const char* pszPSNR = CSLFetchNameValue(papszOptions, "PSNR");
    if (pszPSNR)
    {
        sConfig.target_PSNR = static_cast<float>(CPLAtof(pszPSNR));
        if (sConfig.target_PSNR < 0)
        {
            CPLError( CE_Failure, CPLE_IllegalArg,
                      "PSNR=%s is not a legal value.", pszPSNR );
            return nullptr;
        }
    }

    FETCH_AND_SET_OPTION_INT("METHOD", method, 0, 6);
    FETCH_AND_SET_OPTION_INT("SEGMENTS", segments, 1, 4);
    FETCH_AND_SET_OPTION_INT("SNS_STRENGTH", sns_strength, 0, 100);
    FETCH_AND_SET_OPTION_INT("FILTER_STRENGTH", filter_strength, 0, 100);
    FETCH_AND_SET_OPTION_INT("FILTER_SHARPNESS", filter_sharpness, 0, 7);
    FETCH_AND_SET_OPTION_INT("FILTER_TYPE", filter_type, 0, 1);
    FETCH_AND_SET_OPTION_INT("AUTOFILTER", autofilter, 0, 1);
    FETCH_AND_SET_OPTION_INT("PASS", pass, 1, 10);
    FETCH_AND_SET_OPTION_INT("PREPROCESSING", preprocessing, 0, 1);
    FETCH_AND_SET_OPTION_INT("PARTITIONS", partitions, 0, 3);
#if WEBP_ENCODER_ABI_VERSION >= 0x0002
    FETCH_AND_SET_OPTION_INT("PARTITION_LIMIT", partition_limit, 0, 100);
#endif
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
    sConfig.lossless = CPLFetchBool(papszOptions, "LOSSLESS", false);
    if (sConfig.lossless)
        sPicture.use_argb = 1;
#endif
#if WEBP_ENCODER_ABI_VERSION >= 0x0209
    FETCH_AND_SET_OPTION_INT("EXACT", exact, 0, 1);
#endif

    if (!WebPValidateConfig(&sConfig))
    {
        CPLError(CE_Failure, CPLE_AppDefined, "WebPValidateConfig() failed");
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Allocate memory                                                 */
/* -------------------------------------------------------------------- */
    GByte *pabyBuffer = reinterpret_cast<GByte *>(
        VSIMalloc( nBands * nXSize * nYSize ) );
    if (pabyBuffer == nullptr)
    {
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Create the dataset.                                             */
/* -------------------------------------------------------------------- */
    VSILFILE *fpImage = VSIFOpenL( pszFilename, "wb" );
    if( fpImage == nullptr )
    {
        CPLError( CE_Failure, CPLE_OpenFailed,
                  "Unable to create WEBP file %s.\n",
                  pszFilename );
        VSIFree(pabyBuffer);
        return nullptr;
    }

    WebPUserData sUserData;
    sUserData.fp = fpImage;
    sUserData.pfnProgress = pfnProgress ? pfnProgress : GDALDummyProgress;
    sUserData.pProgressData = pProgressData;

/* -------------------------------------------------------------------- */
/*      WEBP library settings                                           */
/* -------------------------------------------------------------------- */

    sPicture.width = nXSize;
    sPicture.height = nYSize;
    sPicture.writer = WEBPDatasetWriter;
    sPicture.custom_ptr = &sUserData;
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
    sPicture.progress_hook = WEBPDatasetProgressHook;
#endif
    if (!WebPPictureAlloc(&sPicture))
    {
        CPLError(CE_Failure, CPLE_AppDefined, "WebPPictureAlloc() failed");
        VSIFree(pabyBuffer);
        VSIFCloseL( fpImage );
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Acquire source imagery.                                         */
/* -------------------------------------------------------------------- */
    CPLErr eErr = poSrcDS->RasterIO( GF_Read, 0, 0, nXSize, nYSize,
                                     pabyBuffer, nXSize, nYSize, GDT_Byte,
                                     nBands, nullptr,
                                     nBands, nBands * nXSize, 1, nullptr );

/* -------------------------------------------------------------------- */
/*      Import and write to file                                        */
/* -------------------------------------------------------------------- */
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
    if (eErr == CE_None && nBands == 4)
    {
        if (!WebPPictureImportRGBA(&sPicture, pabyBuffer, nBands * nXSize))
        {
            CPLError( CE_Failure, CPLE_AppDefined,
                      "WebPPictureImportRGBA() failed" );
            eErr = CE_Failure;
        }
    }
    else
#endif
    if (eErr == CE_None &&
        !WebPPictureImportRGB(&sPicture, pabyBuffer, nBands * nXSize))
    {
        CPLError(CE_Failure, CPLE_AppDefined, "WebPPictureImportRGB() failed");
        eErr = CE_Failure;
    }

    if (eErr == CE_None && !WebPEncode(&sConfig, &sPicture))
    {
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
        const char* pszErrorMsg = nullptr;
        switch(sPicture.error_code)
        {
            case VP8_ENC_ERROR_OUT_OF_MEMORY:
                pszErrorMsg = "Out of memory"; break;
            case VP8_ENC_ERROR_BITSTREAM_OUT_OF_MEMORY:
                pszErrorMsg = "Out of memory while flushing bits"; break;
            case VP8_ENC_ERROR_NULL_PARAMETER:
                pszErrorMsg = "A pointer parameter is NULL"; break;
            case VP8_ENC_ERROR_INVALID_CONFIGURATION:
                pszErrorMsg = "Configuration is invalid"; break;
            case VP8_ENC_ERROR_BAD_DIMENSION:
                pszErrorMsg = "Picture has invalid width/height"; break;
            case VP8_ENC_ERROR_PARTITION0_OVERFLOW:
                pszErrorMsg = "Partition is bigger than 512k. Try using less "
                    "SEGMENTS, or increase PARTITION_LIMIT value";
                break;
            case VP8_ENC_ERROR_PARTITION_OVERFLOW:
                pszErrorMsg = "Partition is bigger than 16M";
                break;
            case VP8_ENC_ERROR_BAD_WRITE:
                pszErrorMsg = "Error while flushing bytes"; break;
            case VP8_ENC_ERROR_FILE_TOO_BIG:
                pszErrorMsg = "File is bigger than 4G"; break;
            case VP8_ENC_ERROR_USER_ABORT:
                pszErrorMsg = "User interrupted";
                break;
            default:
                CPLError(CE_Failure, CPLE_AppDefined,
                         "WebPEncode returned an unknown error code: %d",
                         sPicture.error_code);
                pszErrorMsg = "Unknown WebP error type.";
                break;
        }
        CPLError(CE_Failure, CPLE_AppDefined,
                 "WebPEncode() failed : %s", pszErrorMsg);
#else
        CPLError(CE_Failure, CPLE_AppDefined, "WebPEncode() failed");
#endif
        eErr = CE_Failure;
    }

/* -------------------------------------------------------------------- */
/*      Cleanup and close.                                              */
/* -------------------------------------------------------------------- */
    CPLFree( pabyBuffer );

    WebPPictureFree(&sPicture);

    VSIFCloseL( fpImage );

    if( eErr != CE_None )
    {
        VSIUnlink( pszFilename );
        return nullptr;
    }

/* -------------------------------------------------------------------- */
/*      Re-open dataset, and copy any auxiliary pam information.        */
/* -------------------------------------------------------------------- */
    GDALOpenInfo oOpenInfo(pszFilename, GA_ReadOnly);

    /* If writing to stdout, we can't reopen it, so return */
    /* a fake dataset to make the caller happy */
    CPLPushErrorHandler(CPLQuietErrorHandler);
    WEBPDataset *poDS
        = reinterpret_cast<WEBPDataset*>( WEBPDataset::Open( &oOpenInfo ) );
    CPLPopErrorHandler();
    if( poDS )
    {
        poDS->CloneInfo( poSrcDS, GCIF_PAM_DEFAULT );
        return poDS;
    }

    return nullptr;
}

/************************************************************************/
/*                         GDALRegister_WEBP()                          */
/************************************************************************/

void GDALRegister_WEBP()

{
    if( GDALGetDriverByName( "WEBP" ) != nullptr )
        return;

    GDALDriver *poDriver = new GDALDriver();

    poDriver->SetDescription( "WEBP" );
    poDriver->SetMetadataItem( GDAL_DCAP_RASTER, "YES" );
    poDriver->SetMetadataItem( GDAL_DMD_LONGNAME, "WEBP" );
    poDriver->SetMetadataItem( GDAL_DMD_HELPTOPIC, "drivers/raster/webp.html" );
    poDriver->SetMetadataItem( GDAL_DMD_EXTENSION, "webp" );
    poDriver->SetMetadataItem( GDAL_DMD_MIMETYPE, "image/webp" );
    poDriver->SetMetadataItem( GDAL_DMD_CREATIONDATATYPES, "Byte" );

    poDriver->SetMetadataItem( GDAL_DMD_CREATIONOPTIONLIST,
"<CreationOptionList>\n"
"   <Option name='QUALITY' type='float' description='good=100, bad=0' default='75'/>\n"
#if WEBP_ENCODER_ABI_VERSION >= 0x0100
"   <Option name='LOSSLESS' type='boolean' description='Whether lossless compression should be used' default='FALSE'/>\n"
#endif
"   <Option name='PRESET' type='string-select' description='kind of image' default='DEFAULT'>\n"
"       <Value>DEFAULT</Value>\n"
"       <Value>PICTURE</Value>\n"
"       <Value>PHOTO</Value>\n"
"       <Value>DRAWING</Value>\n"
"       <Value>ICON</Value>\n"
"       <Value>TEXT</Value>\n"
"   </Option>\n"
"   <Option name='TARGETSIZE' type='int' description='if non-zero, desired target size in bytes. Has precedence over QUALITY'/>\n"
"   <Option name='PSNR' type='float' description='if non-zero, minimal distortion to to achieve. Has precedence over TARGETSIZE'/>\n"
"   <Option name='METHOD' type='int' description='quality/speed trade-off. fast=0, slower-better=6' default='4'/>\n"
"   <Option name='SEGMENTS' type='int' description='maximum number of segments [1-4]' default='4'/>\n"
"   <Option name='SNS_STRENGTH' type='int' description='Spatial Noise Shaping. off=0, maximum=100' default='50'/>\n"
"   <Option name='FILTER_STRENGTH' type='int' description='Filter strength. off=0, strongest=100' default='20'/>\n"
"   <Option name='FILTER_SHARPNESS' type='int' description='Filter sharpness. off=0, least sharp=7' default='0'/>\n"
"   <Option name='FILTER_TYPE' type='int' description='Filtering type. simple=0, strong=1' default='0'/>\n"
"   <Option name='AUTOFILTER' type='int' description=\"Auto adjust filter's strength. off=0, on=1\" default='0'/>\n"
"   <Option name='PASS' type='int' description='Number of entropy analysis passes [1-10]' default='1'/>\n"
"   <Option name='PREPROCESSING' type='int' description='Preprocessing filter. none=0, segment-smooth=1' default='0'/>\n"
"   <Option name='PARTITIONS' type='int' description='log2(number of token partitions) in [0..3]' default='0'/>\n"
#if WEBP_ENCODER_ABI_VERSION >= 0x0002
"   <Option name='PARTITION_LIMIT' type='int' description='quality degradation allowed to fit the 512k limit on prediction modes coding (0=no degradation, 100=full)' default='0'/>\n"
#endif
#if WEBP_ENCODER_ABI_VERSION >= 0x0209
"   <Option name='EXACT' type='int' description='preserve the exact RGB values under transparent area. off=0, on=1' default='0'/>\n"
#endif
"</CreationOptionList>\n" );

    poDriver->SetMetadataItem( GDAL_DCAP_VIRTUALIO, "YES" );

    poDriver->pfnIdentify = WEBPDataset::Identify;
    poDriver->pfnOpen = WEBPDataset::Open;
    poDriver->pfnCreateCopy = WEBPDataset::CreateCopy;

    GetGDALDriverManager()->RegisterDriver( poDriver );
}
