/******************************************************************************
 *
 * Project:  CALS driver
 * Purpose:  CALS driver
 * Author:   Even Rouault, <even dot rouault at spatialys dot com>
 *
 ******************************************************************************
 * Copyright (c) 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 "gdal_frmts.h"
#include "gdal_pam.h"
#include "gdal_priv.h"

#include "tiff.h"

CPL_CVSID("$Id: calsdataset.cpp 36682 2016-12-04 20:34:45Z rouault $");

/************************************************************************/
/* ==================================================================== */
/*                            CALSDataset                               */
/* ==================================================================== */
/************************************************************************/

class CALSDataset : public GDALPamDataset
{
    friend class CALSRasterBand;

    CPLString    osTIFFHeaderFilename;
    CPLString    osSparseFilename;
    GDALDataset* poUnderlyingDS;

    static void WriteLEInt16( VSILFILE* fp, GInt16 nVal );
    static void WriteLEInt32( VSILFILE* fp, GInt32 nVal );
    static void WriteTIFFTAG( VSILFILE* fp, GInt16 nTagName, GInt16 nTagType,
                              GInt32 nTagValue );

  public:
                 CALSDataset() : poUnderlyingDS(NULL) {}
                ~CALSDataset();

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

/************************************************************************/
/* ==================================================================== */
/*                          CALSRasterBand                              */
/* ==================================================================== */
/************************************************************************/

class CALSRasterBand: public GDALPamRasterBand
{
    GDALRasterBand* poUnderlyingBand;

  public:
    explicit CALSRasterBand( CALSDataset* poDSIn )
    {
        poDS = poDSIn;
        poUnderlyingBand = poDSIn->poUnderlyingDS->GetRasterBand(1);
        poUnderlyingBand->GetBlockSize(&nBlockXSize, &nBlockYSize);
        nBand = 1;
        eDataType = GDT_Byte;
    }

    virtual CPLErr IReadBlock( int nBlockXOff, int nBlockYOff, void * pData ) override
    {
        return poUnderlyingBand->ReadBlock(nBlockXOff, nBlockYOff, pData);
    }

    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
    {
        return poUnderlyingBand->RasterIO(
            eRWFlag, nXOff, nYOff, nXSize, nYSize,
            pData, nBufXSize, nBufYSize, eBufType,
            nPixelSpace, nLineSpace, psExtraArg ) ;
    }

    virtual GDALColorTable* GetColorTable() override
    {
        return poUnderlyingBand->GetColorTable();
    }

    virtual GDALColorInterp GetColorInterpretation() override
    {
        return GCI_PaletteIndex;
    }

    virtual char** GetMetadata(const char* pszDomain) override
    {
        return poUnderlyingBand->GetMetadata(pszDomain);
    }

    virtual const char* GetMetadataItem(const char* pszKey, const char* pszDomain) override
    {
        return poUnderlyingBand->GetMetadataItem(pszKey, pszDomain);
    }
};

/************************************************************************/
/* ==================================================================== */
/*                          CALSWrapperSrcBand                          */
/* ==================================================================== */
/************************************************************************/

class CALSWrapperSrcBand: public GDALPamRasterBand
{
        GDALDataset* poSrcDS;
        bool bInvertValues;

    public:
        explicit CALSWrapperSrcBand( GDALDataset* poSrcDSIn )
        {
            poSrcDS = poSrcDSIn;
            SetMetadataItem("NBITS", "1", "IMAGE_STRUCTURE");
            poSrcDS->GetRasterBand(1)->GetBlockSize(&nBlockXSize, &nBlockYSize);
            eDataType = GDT_Byte;
            bInvertValues = true;
            GDALColorTable* poCT = poSrcDS->GetRasterBand(1)->GetColorTable();
            if( poCT != NULL && poCT->GetColorEntryCount() >= 2 )
            {
                const GDALColorEntry* psEntry1 = poCT->GetColorEntry(0);
                const GDALColorEntry* psEntry2 = poCT->GetColorEntry(1);
                if( psEntry1->c1 == 255 &&
                    psEntry1->c2 == 255 &&
                    psEntry1->c3 == 255 &&
                    psEntry2->c1 == 0 &&
                    psEntry2->c2 == 0 &&
                    psEntry2->c3 == 0 )
                {
                    bInvertValues = false;
                }
            }
        }

        virtual CPLErr IReadBlock( int /* nBlockXOff */,
                                   int /* nBlockYOff */,
                                   void * /* pData */ ) override
        {
            // Should not be called.
            return CE_Failure;
        }

        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
        {
            const CPLErr eErr =
                poSrcDS->GetRasterBand(1)->RasterIO(
                    eRWFlag, nXOff, nYOff, nXSize, nYSize,
                    pData, nBufXSize, nBufYSize, eBufType,
                    nPixelSpace, nLineSpace, psExtraArg ) ;
            if( bInvertValues )
            {
                for( int j = 0; j < nBufYSize; j++ )
                {
                    for( int i = 0; i < nBufXSize; i++ )
                        ((GByte*)pData)[j * nLineSpace + i * nPixelSpace] =
                            1 - ((GByte*)pData)[j * nLineSpace +
                                                i * nPixelSpace];
                }
            }
            return eErr;
        }
};

/************************************************************************/
/* ==================================================================== */
/*                          CALSWrapperSrcDataset                       */
/* ==================================================================== */
/************************************************************************/

class CALSWrapperSrcDataset: public GDALPamDataset
{
    public:
        CALSWrapperSrcDataset( GDALDataset* poSrcDS, const char* pszPadding )
        {
            nRasterXSize = poSrcDS->GetRasterXSize();
            nRasterYSize = poSrcDS->GetRasterYSize();
            SetBand(1, new CALSWrapperSrcBand(poSrcDS));
            SetMetadataItem("TIFFTAG_DOCUMENTNAME", pszPadding);
        }
};

/************************************************************************/
/* ==================================================================== */
/*                            CALSDataset                               */
/* ==================================================================== */
/************************************************************************/

/************************************************************************/
/*                            ~CALSDataset()                            */
/************************************************************************/

CALSDataset::~CALSDataset()

{
    delete poUnderlyingDS;
    if( !osTIFFHeaderFilename.empty() )
        VSIUnlink(osTIFFHeaderFilename);
    if( !osSparseFilename.empty() )
        VSIUnlink(osSparseFilename);
}

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

int CALSDataset::Identify( GDALOpenInfo * poOpenInfo )

{
    // If in the ingested bytes we found neither srcdocid: or rtype: 1, give up
    if( poOpenInfo->nHeaderBytes == 0 ||
        (strstr( (const char*) poOpenInfo->pabyHeader, "srcdocid:") == NULL &&
         strstr( (const char*) poOpenInfo->pabyHeader, "rtype: 1") == NULL) )
        return FALSE;

    // If we found srcdocid: try to ingest up to 2048 bytes
    if( strstr( (const char*) poOpenInfo->pabyHeader, "srcdocid:") &&
        !poOpenInfo->TryToIngest(2048) )
        return FALSE;

    return strstr((const char*) poOpenInfo->pabyHeader, "rtype: 1") != NULL &&
           strstr((const char*) poOpenInfo->pabyHeader, "rorient:") != NULL &&
           strstr((const char*) poOpenInfo->pabyHeader, "rpelcnt:") != NULL;
}

/************************************************************************/
/*                           WriteLEInt16()                             */
/************************************************************************/

void CALSDataset::WriteLEInt16( VSILFILE* fp, GInt16 nVal )
{
    CPL_LSBPTR16(&nVal);
    VSIFWriteL(&nVal, 1, 2, fp);
}

/************************************************************************/
/*                            WriteLEInt32()                            */
/************************************************************************/

void CALSDataset::WriteLEInt32( VSILFILE* fp, GInt32 nVal )
{
    CPL_LSBPTR32(&nVal);
    VSIFWriteL(&nVal, 1, 4, fp);
}

/************************************************************************/
/*                            WriteTIFFTAG()                            */
/************************************************************************/

void CALSDataset::WriteTIFFTAG( VSILFILE* fp, GInt16 nTagName, GInt16 nTagType,
                                GInt32 nTagValue )
{
    WriteLEInt16(fp, nTagName);
    WriteLEInt16(fp, nTagType);
    WriteLEInt32(fp, 1);
    WriteLEInt32(fp, nTagValue);
}

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

GDALDataset *CALSDataset::Open( GDALOpenInfo * poOpenInfo )

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

    const char* pszRPelCnt =
        strstr((const char*) poOpenInfo->pabyHeader, "rpelcnt:");
    int nXSize = 0;
    int nYSize = 0;
    if( sscanf(pszRPelCnt+strlen("rpelcnt:"),"%d,%d",&nXSize,&nYSize) != 2 ||
        nXSize <= 0 || nYSize <= 0 )
        return NULL;

    const char* pszOrient =
        strstr((const char*) poOpenInfo->pabyHeader, "rorient:");
    int nAngle1, nAngle2;
    if( sscanf(pszOrient+strlen("rorient:"),"%d,%d",&nAngle1,&nAngle2) != 2 )
        return NULL;

    const char* pszDensity =
        strstr((const char*) poOpenInfo->pabyHeader, "rdensty:");
    int nDensity = 0;
    if( pszDensity )
        sscanf(pszDensity+strlen("rdensty:"), "%d", &nDensity);

    VSIFSeekL(poOpenInfo->fpL, 0, SEEK_END);
    int nFAX4BlobSize = static_cast<int>(VSIFTellL(poOpenInfo->fpL)) - 2048;
    if( nFAX4BlobSize < 0 )
        return NULL;

    CALSDataset* poDS = new CALSDataset();
    poDS->nRasterXSize = nXSize;
    poDS->nRasterYSize = nYSize;

    // Create a TIFF header for a single-strip CCITTFAX4 file.
    poDS->osTIFFHeaderFilename = CPLSPrintf("/vsimem/cals/header_%p.tiff", poDS);
    VSILFILE* fp = VSIFOpenL(poDS->osTIFFHeaderFilename, "wb");
    const int nTagCount = 10;
    const int nHeaderSize = 4 + 4 + 2 + nTagCount * 12 + 4;
    WriteLEInt16(fp, TIFF_LITTLEENDIAN);  // TIFF little-endian signature.
    WriteLEInt16(fp, 42);  // TIFF classic.

    WriteLEInt32(fp, 8);  // Offset of IFD0.

    WriteLEInt16(fp, nTagCount);  // Number of entries.

    WriteTIFFTAG(fp, TIFFTAG_IMAGEWIDTH, TIFF_LONG, nXSize);
    WriteTIFFTAG(fp, TIFFTAG_IMAGELENGTH, TIFF_LONG, nYSize);
    WriteTIFFTAG(fp, TIFFTAG_BITSPERSAMPLE, TIFF_SHORT, 1);
    WriteTIFFTAG(fp, TIFFTAG_COMPRESSION, TIFF_SHORT, COMPRESSION_CCITTFAX4);
    WriteTIFFTAG(fp, TIFFTAG_PHOTOMETRIC, TIFF_SHORT, PHOTOMETRIC_MINISWHITE);
    WriteTIFFTAG(fp, TIFFTAG_STRIPOFFSETS, TIFF_LONG, nHeaderSize);
    WriteTIFFTAG(fp, TIFFTAG_SAMPLESPERPIXEL, TIFF_SHORT, 1);
    WriteTIFFTAG(fp, TIFFTAG_ROWSPERSTRIP, TIFF_LONG, nYSize);
    WriteTIFFTAG(fp, TIFFTAG_STRIPBYTECOUNTS, TIFF_LONG, nFAX4BlobSize);
    WriteTIFFTAG(fp, TIFFTAG_PLANARCONFIG, TIFF_SHORT, PLANARCONFIG_CONTIG);

    WriteLEInt32(fp, 0);  // Offset of next IFD.

    VSIFCloseL(fp);

    // Create a /vsisparse/ description file assembling the TIFF header
    // with the FAX4 codestream that starts at offset 2048 of the CALS file.
    poDS->osSparseFilename = CPLSPrintf("/vsimem/cals/sparse_%p.xml", poDS);
    fp = VSIFOpenL(poDS->osSparseFilename, "wb");
    CPLAssert(fp);
    VSIFPrintfL(fp, "<VSISparseFile>"
                    "<Length>%d</Length>"
                    "<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>%d</SourceOffset>"
                      "<RegionLength>%d</RegionLength>"
                    "</SubfileRegion>"
                    "</VSISparseFile>",
                nHeaderSize + nFAX4BlobSize,
                poDS->osTIFFHeaderFilename.c_str(),
                nHeaderSize,
                poOpenInfo->pszFilename,
                nHeaderSize,
                2048,
                nFAX4BlobSize);
    VSIFCloseL(fp);

    poDS->poUnderlyingDS = (GDALDataset*) GDALOpenEx(
        CPLSPrintf("/vsisparse/%s", poDS->osSparseFilename.c_str()),
        GDAL_OF_RASTER | GDAL_OF_INTERNAL, NULL, NULL, NULL);
    if( poDS->poUnderlyingDS == NULL )
    {
        delete poDS;
        return NULL;
    }

    if( nAngle1 != 0 || nAngle2 != 270 )
    {
        poDS->SetMetadataItem("PIXEL_PATH", CPLSPrintf("%d", nAngle1));
        poDS->SetMetadataItem("LINE_PROGRESSION", CPLSPrintf("%d", nAngle2));
    }

    if( nDensity != 0 )
    {
        poDS->SetMetadataItem("TIFFTAG_XRESOLUTION", CPLSPrintf("%d", nDensity));
        poDS->SetMetadataItem("TIFFTAG_YRESOLUTION", CPLSPrintf("%d", nDensity));
        poDS->SetMetadataItem("TIFFTAG_RESOLUTIONUNIT", "2 (pixels/inch)");
    }

    poDS->SetBand(1, new CALSRasterBand(poDS));

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

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

    return poDS;
}

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

GDALDataset *CALSDataset::CreateCopy( const char *pszFilename,
                                      GDALDataset *poSrcDS,
                                      int bStrict,
                                      char ** /* papszOptionsUnused */,
                                      GDALProgressFunc pfnProgress,
                                      void *pProgressData )
{
    if( poSrcDS->GetRasterCount() == 0 ||
        (bStrict && poSrcDS->GetRasterCount() != 1) )
    {
        CPLError( CE_Failure, CPLE_NotSupported,
                  "CALS driver only supports single band raster.");
        return NULL;
    }
    if( poSrcDS->GetRasterBand(1)->
            GetMetadataItem("NBITS", "IMAGE_STRUCTURE") == NULL ||
        !EQUAL(poSrcDS->GetRasterBand(1)->
                   GetMetadataItem("NBITS", "IMAGE_STRUCTURE"), "1") )
    {
        CPLError( bStrict ? CE_Failure : CE_Warning, CPLE_NotSupported,
                  "CALS driver only supports 1-bit.");
        if( bStrict )
            return NULL;
    }

    if( poSrcDS->GetRasterXSize() > 999999 ||
        poSrcDS->GetRasterYSize() > 999999 )
    {
        CPLError(
            CE_Failure, CPLE_NotSupported,
            "CALS driver only supports datasets with dimension <= 999999.");
        return NULL;
    }

    GDALDriver* poGTiffDrv =
        static_cast<GDALDriver *>(GDALGetDriverByName("GTiff"));
    if( poGTiffDrv == NULL )
    {
        CPLError( CE_Failure, CPLE_NotSupported,
                  "CALS driver needs GTiff driver." );
        return NULL;
    }

    // Write a in-memory TIFF with just the TIFF header to figure out
    // how large it will be.
    CPLString osTmpFilename(CPLSPrintf("/vsimem/cals/tmp_%p", poSrcDS));
    char** papszOptions = NULL;
    papszOptions = CSLSetNameValue(papszOptions, "COMPRESS", "CCITTFAX4");
    papszOptions = CSLSetNameValue(papszOptions, "NBITS", "1");
    papszOptions = CSLSetNameValue(papszOptions, "BLOCKYSIZE",
                                   CPLSPrintf("%d", poSrcDS->GetRasterYSize()));
    papszOptions = CSLSetNameValue(papszOptions, "SPARSE_OK", "YES");
    GDALDataset* poDS = poGTiffDrv->Create(osTmpFilename,
                                           poSrcDS->GetRasterXSize(),
                                           poSrcDS->GetRasterYSize(),
                                           1, GDT_Byte,
                                           papszOptions);
    if( poDS == NULL )
    {
        // Should not happen normally (except if CCITTFAX4 not available).
        CSLDestroy(papszOptions);
        return NULL;
    }
    const char INITIAL_PADDING[] = "12345";
     // To adjust padding.
    poDS->SetMetadataItem("TIFFTAG_DOCUMENTNAME", INITIAL_PADDING);
    GDALClose(poDS);
    VSIStatBufL sStat;
    if( VSIStatL(osTmpFilename, &sStat) != 0 )
    {
        // Shoudln't happen really. Just to make Coverity happy.
        CSLDestroy(papszOptions);
        return NULL;
    }
    int nTIFFHeaderSize = static_cast<int>(sStat.st_size);
    VSIUnlink(osTmpFilename);

    // Redo the same thing, but this time write it to the output file
    // and use a variable TIFF tag (TIFFTAG_DOCUMENTNAME) to enlarge the
    // header + the variable TIFF tag so that they are 2048 bytes large.
    char szBuffer[2048+1] = {};
    memset(szBuffer, 'X', 2048 - nTIFFHeaderSize + strlen(INITIAL_PADDING));
    szBuffer[2048 - nTIFFHeaderSize + strlen(INITIAL_PADDING)] = 0;
    GDALDataset* poTmpDS = new CALSWrapperSrcDataset(poSrcDS, szBuffer);
    poDS = poGTiffDrv->CreateCopy(pszFilename, poTmpDS, FALSE, papszOptions,
                                  pfnProgress, pProgressData );
    delete poTmpDS;
    CSLDestroy(papszOptions);
    if( poDS == NULL )
        return NULL;
    delete poDS;

    // Now replace the TIFF header by the CALS header.
    VSILFILE* fp = VSIFOpenL(pszFilename, "rb+");
    if( fp == NULL )
        return NULL; // Shoudln't happen normally.
    memset(szBuffer, ' ', 2048);
    CPLString osField;
    osField = "srcdocid: NONE";
    // cppcheck-suppress redundantCopy
    memcpy(szBuffer, osField, osField.size());

    osField = "dstdocid: NONE";
    memcpy(szBuffer + 128, osField, osField.size());

    osField = "txtfilid: NONE";
    memcpy(szBuffer + 128*2, osField, osField.size());

    osField = "figid: NONE";
    memcpy(szBuffer + 128*3, osField, osField.size());

    osField = "srcgph: NONE";
    memcpy(szBuffer + 128*4, osField, osField.size());

    osField = "doccls: NONE";
    memcpy(szBuffer + 128*5, osField, osField.size());

    osField = "rtype: 1";
    memcpy(szBuffer + 128*6, osField, osField.size());

    int nAngle1 = 0;
    int nAngle2 = 270;
    const char* pszPixelPath = poSrcDS->GetMetadataItem("PIXEL_PATH");
    const char* pszLineProgression = poSrcDS->GetMetadataItem("LINE_PROGRESSION");
    if( pszPixelPath && pszLineProgression )
    {
        nAngle1 = atoi(pszPixelPath);
        nAngle2 = atoi(pszLineProgression);
    }
    osField = CPLSPrintf("rorient: %03d,%03d", nAngle1, nAngle2);
    memcpy(szBuffer + 128*7, osField, osField.size());

    osField = CPLSPrintf("rpelcnt: %06d,%06d",
                         poSrcDS->GetRasterXSize(),
                         poSrcDS->GetRasterYSize());
    memcpy(szBuffer + 128*8, osField, osField.size());

    int nDensity = 200;
    const char* pszXRes = poSrcDS->GetMetadataItem("TIFFTAG_XRESOLUTION");
    const char* pszYRes = poSrcDS->GetMetadataItem("TIFFTAG_YRESOLUTION");
    const char* pszResUnit = poSrcDS->GetMetadataItem("TIFFTAG_RESOLUTIONUNIT");
    if( pszXRes && pszYRes && pszResUnit && EQUAL(pszXRes, pszYRes) &&
        atoi(pszResUnit) == 2 )
    {
        nDensity = atoi(pszXRes);
        if( nDensity < 1 || nDensity > 9999 )
            nDensity = 200;
    }
    osField = CPLSPrintf("rdensty: %04d", nDensity);
    memcpy(szBuffer + 128*9, osField, osField.size());

    osField = "notes: NONE";
    memcpy(szBuffer + 128*10, osField, osField.size());
    VSIFWriteL(szBuffer, 1, 2048, fp);
    VSIFCloseL(fp);

    GDALOpenInfo oOpenInfo(pszFilename, GA_ReadOnly, NULL);
    return Open(&oOpenInfo);
}

/************************************************************************/
/*                        GDALRegister_CALS()                           */
/************************************************************************/

void GDALRegister_CALS()

{
    if( GDALGetDriverByName( "CALS" ) != NULL )
        return;

    GDALDriver *poDriver = new GDALDriver();

    poDriver->SetDescription( "CALS" );
    poDriver->SetMetadataItem( GDAL_DCAP_RASTER, "YES" );
    poDriver->SetMetadataItem( GDAL_DMD_LONGNAME,
                               "CALS (Type 1)" );
    poDriver->SetMetadataItem( GDAL_DMD_HELPTOPIC,
                               "frmt_cals.html" );

    poDriver->SetMetadataItem( GDAL_DMD_EXTENSIONS, ".cal .ct1");

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

    GetGDALDriverManager()->RegisterDriver( poDriver );
}
