/******************************************************************************
 * $Id: pdfdataset.cpp 28978 2015-04-23 09:14:09Z rouault $
 *
 * Project:  PDF driver
 * Purpose:  GDALDataset driver for PDF dataset.
 * Author:   Even Rouault, <even dot rouault at mines dash paris dot org>
 *
 ******************************************************************************
 * Copyright (c) 2010-2014, Even Rouault <even dot rouault at mines-paris dot org>
 *
 * 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_pdf.h"

#include "cpl_vsi_virtual.h"
#include "cpl_string.h"
#include "ogr_spatialref.h"
#include "ogr_geometry.h"
#include "cpl_spawn.h"

#ifdef HAVE_POPPLER
#include "cpl_multiproc.h"
#include "pdfio.h"
#include <goo/GooList.h>
#endif // HAVE_POPPLER

#include "pdfcreatecopy.h"
#include <set>

#define GDAL_DEFAULT_DPI 150.0

/* g++ -fPIC -g -Wall frmts/pdf/pdfdataset.cpp -shared -o gdal_PDF.so -Iport -Igcore -Iogr -L. -lgdal -lpoppler -I/usr/include/poppler */

CPL_CVSID("$Id: pdfdataset.cpp 28978 2015-04-23 09:14:09Z rouault $");

CPL_C_START
void    GDALRegister_PDF(void);
CPL_C_END

#if defined(HAVE_POPPLER) || defined(HAVE_PODOFO)
static const char* pszOpenOptionList =
"<OpenOptionList>"
"  <Option name='RENDERING_OPTIONS' type='string-select' description='Which graphical elements to render' default='RASTER,VECTOR,TEXT' alt_config_option='GDAL_PDF_RENDERING_OPTIONS'>"
"     <Value>RASTER,VECTOR,TEXT</Value>\n"
"     <Value>RASTER,VECTOR</Value>\n"
"     <Value>RASTER,TEXT</Value>\n"
"     <Value>RASTER</Value>\n"
"     <Value>VECTOR,TEXT</Value>\n"
"     <Value>VECTOR</Value>\n"
"     <Value>TEXT</Value>\n"
"  </Option>"
"  <Option name='DPI' type='float' description='Resolution in Dot Per Inch' default='72' alt_config_option='GDAL_PDF_DPI'/>"
"  <Option name='USER_PWD' type='string' description='Password' alt_config_option='PDF_USER_PWD'/>"
#if defined(HAVE_POPPLER) && defined(HAVE_PODOFO)
"  <Option name='PDF_LIB' type='string-select' description='Which underlying PDF library to use' default='POPPLER' alt_config_option='GDAL_PDF_LIB'>"
"     <Value>POPPLER</Value>\n"
"     <Value>PODOFO</Value>\n"
"  </Option>"
#endif // HAVE_POPPLER && HAVE_PODOFO
"  <Option name='LAYERS' type='string' description='List of layers (comma separated) to turn ON (or ALL to turn all layers ON)' alt_config_option='GDAL_PDF_LAYERS'/>"
"  <Option name='LAYERS_OFF' type='string' description='List of layers (comma separated) to turn OFF' alt_config_option='GDAL_PDF_LAYERS_OFF'/>"
"  <Option name='BANDS' type='string-select' description='Number of raster bands' default='3' alt_config_option='GDAL_PDF_BANDS'>"
"     <Value>3</Value>\n"
"     <Value>4</Value>\n"
"  </Option>"
"  <Option name='NEATLINE' type='string' description='The name of the neatline to select' alt_config_option='GDAL_PDF_NEATLINE'/>"
"</OpenOptionList>";

static double Get(GDALPDFObject* poObj, int nIndice = -1);

#ifdef HAVE_POPPLER

static CPLMutex* hGlobalParamsMutex = NULL;

/************************************************************************/
/*                          ObjectAutoFree                              */
/************************************************************************/

class ObjectAutoFree : public Object
{
public:
    ObjectAutoFree() {}
    ~ObjectAutoFree() { free(); }
};


/************************************************************************/
/*                         GDALPDFOutputDev                             */
/************************************************************************/

class GDALPDFOutputDev : public SplashOutputDev
{
    private:
        int bEnableVector;
        int bEnableText;
        int bEnableBitmap;

        void skipBytes(Stream *str,
                       int width, int height,
                       int nComps, int nBits)
        {
            int nVals = width * nComps;
            int nLineSize = (nVals * nBits + 7) >> 3;
            int nBytes = nLineSize * height;
            for (int i = 0; i < nBytes; i++)
            {
                if( str->getChar() == EOF)
                    break;
            }
        }

    public:
        GDALPDFOutputDev(SplashColorMode colorModeA, int bitmapRowPadA,
                         GBool reverseVideoA, SplashColorPtr paperColorA) :
                SplashOutputDev(colorModeA, bitmapRowPadA,
                                reverseVideoA, paperColorA),
                bEnableVector(TRUE),
                bEnableText(TRUE),
                bEnableBitmap(TRUE) {}

        void SetEnableVector(int bFlag) { bEnableVector = bFlag; }
        void SetEnableText(int bFlag) { bEnableText = bFlag; }
        void SetEnableBitmap(int bFlag) { bEnableBitmap = bFlag; }

        virtual void startPage(int pageNum, GfxState *state
#ifdef POPPLER_0_23_OR_LATER
                               ,XRef* xref
#endif
        )
        {
            SplashOutputDev::startPage(pageNum, state
#ifdef POPPLER_0_23_OR_LATER
                                       ,xref
#endif
            );
            SplashBitmap* poBitmap = getBitmap();
            memset(poBitmap->getDataPtr(), 255, poBitmap->getRowSize() * poBitmap->getHeight());
        }

        virtual void stroke(GfxState * state)
        {
            if (bEnableVector)
                SplashOutputDev::stroke(state);
        }

        virtual void fill(GfxState * state)
        {
            if (bEnableVector)
                SplashOutputDev::fill(state);
        }

        virtual void eoFill(GfxState * state)
        {
            if (bEnableVector)
                SplashOutputDev::eoFill(state);
        }

        virtual void drawChar(GfxState *state, double x, double y,
                              double dx, double dy,
                              double originX, double originY,
                              CharCode code, int nBytes, Unicode *u, int uLen)
        {
            if (bEnableText)
                SplashOutputDev::drawChar(state, x, y, dx, dy,
                                          originX, originY,
                                          code, nBytes, u, uLen);
        }

        virtual void beginTextObject(GfxState *state)
        {
            if (bEnableText)
                SplashOutputDev::beginTextObject(state);
        }

#ifndef POPPLER_0_23_OR_LATER
        virtual GBool deviceHasTextClip(GfxState *state)
        {
            if (bEnableText)
                return SplashOutputDev::deviceHasTextClip(state);
            return gFalse;
        }
#endif

        virtual void endTextObject(GfxState *state)
        {
            if (bEnableText)
                SplashOutputDev::endTextObject(state);
        }

        virtual void drawImageMask(GfxState *state, Object *ref, Stream *str,
                                   int width, int height, GBool invert,
                                   GBool interpolate, GBool inlineImg)
        {
            if (bEnableBitmap)
                SplashOutputDev::drawImageMask(state, ref, str,
                                               width, height, invert,
                                               interpolate, inlineImg);
            else
            {
                str->reset();
                if (inlineImg)
                {
                    skipBytes(str, width, height, 1, 1);
                }
                str->close();
            }
        }

#ifdef POPPLER_0_20_OR_LATER
        virtual void setSoftMaskFromImageMask(GfxState *state,
                            Object *ref, Stream *str,
                            int width, int height, GBool invert,
                            GBool inlineImg, double *baseMatrix)
        {
            if (bEnableBitmap)
                SplashOutputDev::setSoftMaskFromImageMask(state, ref, str,
                                               width, height, invert,
                                               inlineImg, baseMatrix);
            else
                str->close();
        }

        virtual void unsetSoftMaskFromImageMask(GfxState *state, double *baseMatrix)
        {
            if (bEnableBitmap)
                SplashOutputDev::unsetSoftMaskFromImageMask(state, baseMatrix);
        }
#endif

        virtual void drawImage(GfxState *state, Object *ref, Stream *str,
                               int width, int height, GfxImageColorMap *colorMap,
                               GBool interpolate, int *maskColors, GBool inlineImg)
        {
            if (bEnableBitmap)
                SplashOutputDev::drawImage(state, ref, str,
                                           width, height, colorMap,
                                           interpolate, maskColors, inlineImg);
            else
            {
                str->reset();
                if (inlineImg)
                {
                    skipBytes(str, width, height,
                              colorMap->getNumPixelComps(),
                              colorMap->getBits());
                }
                str->close();
            }
        }

        virtual void drawMaskedImage(GfxState *state, Object *ref, Stream *str,
                                     int width, int height,
                                     GfxImageColorMap *colorMap,
                                     GBool interpolate,
                                     Stream *maskStr, int maskWidth, int maskHeight,
                                     GBool maskInvert, GBool maskInterpolate)
        {
            if (bEnableBitmap)
                SplashOutputDev::drawMaskedImage(state, ref, str,
                                                 width, height, colorMap,
                                                 interpolate,
                                                 maskStr, maskWidth, maskHeight,
                                                 maskInvert, maskInterpolate);
            else
                str->close();
        }

        virtual void drawSoftMaskedImage(GfxState *state, Object *ref, Stream *str,
                                         int width, int height,
                                         GfxImageColorMap *colorMap,
                                         GBool interpolate,
                                         Stream *maskStr,
                                         int maskWidth, int maskHeight,
                                         GfxImageColorMap *maskColorMap,
                                         GBool maskInterpolate)
        {
            if (bEnableBitmap)
            {
                if( maskColorMap->getBits() <= 0 ) /* workaround poppler bug (robustness) */
                {
                    str->close();
                    return;
                }
                SplashOutputDev::drawSoftMaskedImage(state, ref, str,
                                                     width, height, colorMap,
                                                     interpolate,
                                                     maskStr, maskWidth, maskHeight,
                                                     maskColorMap, maskInterpolate);
            }
            else
                str->close();
        }
};

#endif

/************************************************************************/
/*                         Dump routines                                */
/************************************************************************/

class GDALPDFDumper
{
    private:
        FILE* f;
        int   nDepthLimit;
        std::set< int > aoSetObjectExplored;
        int   bDumpParent;

        void DumpSimplified(GDALPDFObject* poObj);

    public:
        GDALPDFDumper(FILE* fIn, int nDepthLimitIn = -1) : f(fIn), nDepthLimit(nDepthLimitIn)
        {
            bDumpParent = CSLTestBoolean(CPLGetConfigOption("PDF_DUMP_PARENT", "FALSE"));
        }

        void Dump(GDALPDFObject* poObj, int nDepth = 0);
        void Dump(GDALPDFDictionary* poDict, int nDepth = 0);
        void Dump(GDALPDFArray* poArray, int nDepth = 0);
};

void GDALPDFDumper::Dump(GDALPDFArray* poArray, int nDepth)
{
    if (nDepthLimit >= 0 && nDepth > nDepthLimit)
        return;

    int nLength = poArray->GetLength();
    int i;
    CPLString osIndent;
    for(i=0;i<nDepth;i++)
        osIndent += " ";
    for(i=0;i<nLength;i++)
    {
        fprintf(f, "%sItem[%d]:", osIndent.c_str(), i);
        GDALPDFObject* poObj = NULL;
        if ((poObj = poArray->Get(i)) != NULL)
        {
            if (poObj->GetType() == PDFObjectType_String ||
                poObj->GetType() == PDFObjectType_Null ||
                poObj->GetType() == PDFObjectType_Bool ||
                poObj->GetType() == PDFObjectType_Int ||
                poObj->GetType() == PDFObjectType_Real ||
                poObj->GetType() == PDFObjectType_Name)
            {
                fprintf(f, " ");
                DumpSimplified(poObj);
                fprintf(f, "\n");
            }
            else
            {
                fprintf(f, "\n");
                Dump( poObj, nDepth+1);
            }
        }
    }
}

void GDALPDFDumper::DumpSimplified(GDALPDFObject* poObj)
{
    switch(poObj->GetType())
    {
        case PDFObjectType_String:
            fprintf(f, "%s (string)", poObj->GetString().c_str());
            break;

        case PDFObjectType_Null:
            fprintf(f, "null");
            break;

        case PDFObjectType_Bool:
            fprintf(f, "%s (bool)", poObj->GetBool() ? "true" : "false");
            break;

        case PDFObjectType_Int:
            fprintf(f, "%d (int)", poObj->GetInt());
            break;

        case PDFObjectType_Real:
            fprintf(f, "%f (real)", poObj->GetReal());
            break;

        case PDFObjectType_Name:
            fprintf(f, "%s (name)", poObj->GetName().c_str());
            break;

        default:
            fprintf(f, "unknown !");
            break;
    }
}

void GDALPDFDumper::Dump(GDALPDFObject* poObj, int nDepth)
{
    if (nDepthLimit >= 0 && nDepth > nDepthLimit)
        return;

    int i;
    CPLString osIndent;
    for(i=0;i<nDepth;i++)
        osIndent += " ";
    fprintf(f, "%sType = %s",
            osIndent.c_str(), poObj->GetTypeName());
    int nRefNum = poObj->GetRefNum();
    if (nRefNum != 0)
        fprintf(f, ", Num = %d, Gen = %d",
                nRefNum, poObj->GetRefGen());
    fprintf(f, "\n");

    if (nRefNum != 0)
    {
        if (aoSetObjectExplored.find(nRefNum) != aoSetObjectExplored.end())
            return;
        aoSetObjectExplored.insert(nRefNum);
    }

    switch(poObj->GetType())
    {
        case PDFObjectType_Array:
            Dump(poObj->GetArray(), nDepth+1);
            break;

        case PDFObjectType_Dictionary:
            Dump(poObj->GetDictionary(), nDepth+1);
            break;

        case PDFObjectType_String:
        case PDFObjectType_Null:
        case PDFObjectType_Bool:
        case PDFObjectType_Int:
        case PDFObjectType_Real:
        case PDFObjectType_Name:
            fprintf(f, "%s", osIndent.c_str());
            DumpSimplified(poObj);
            fprintf(f, "\n");
            break;

        default:
            fprintf(f, "%s", osIndent.c_str());
            fprintf(f, "unknown !\n");
            break;
    }

    GDALPDFStream* poStream = poObj->GetStream();
    if (poStream != NULL)
    {
        fprintf(f, "%sHas stream (%d bytes)\n", osIndent.c_str(), poStream->GetLength());
    }
}

void GDALPDFDumper::Dump(GDALPDFDictionary* poDict, int nDepth)
{
    if (nDepthLimit >= 0 && nDepth > nDepthLimit)
        return;

    std::map<CPLString, GDALPDFObject*>& oMap = poDict->GetValues();
    std::map<CPLString, GDALPDFObject*>::iterator oIter = oMap.begin();
    std::map<CPLString, GDALPDFObject*>::iterator oEnd = oMap.end();
    int i;
    CPLString osIndent;
    for(i=0;i<nDepth;i++)
        osIndent += " ";
    for(i=0;oIter != oEnd;++oIter, i++)
    {
        const char* pszKey = oIter->first.c_str();
        fprintf(f, "%sItem[%d] : %s", osIndent.c_str(), i, pszKey);
        GDALPDFObject* poObj = oIter->second;
        if (strcmp(pszKey, "Parent") == 0 && !bDumpParent)
        {
            if (poObj->GetRefNum())
                fprintf(f, ", Num = %d, Gen = %d",
                        poObj->GetRefNum(), poObj->GetRefGen());
            fprintf(f, "\n");
            continue;
        }
        if (poObj != NULL)
        {
            if (poObj->GetType() == PDFObjectType_String ||
                poObj->GetType() == PDFObjectType_Null ||
                poObj->GetType() == PDFObjectType_Bool ||
                poObj->GetType() == PDFObjectType_Int ||
                poObj->GetType() == PDFObjectType_Real ||
                poObj->GetType() == PDFObjectType_Name)
            {
                fprintf(f, " = ");
                DumpSimplified(poObj);
                fprintf(f, "\n");
            }
            else
            {
                fprintf(f, "\n");
                Dump(poObj, nDepth+1);
            }
        }
    }
}

/************************************************************************/
/*                         PDFRasterBand()                              */
/************************************************************************/

PDFRasterBand::PDFRasterBand( PDFDataset *poDS, int nBand )

{
    this->poDS = poDS;
    this->nBand = nBand;

    eDataType = GDT_Byte;

    if( poDS->nBlockXSize )
    {
        nBlockXSize = poDS->nBlockXSize;
        nBlockYSize = poDS->nBlockYSize;
    }
    else if( poDS->GetRasterXSize() < 64 * 1024 * 1024 / poDS->GetRasterYSize() )
    {
        nBlockXSize = poDS->GetRasterXSize();
        nBlockYSize = 1;
    }
    else
    {
        nBlockXSize = MIN(1024, poDS->GetRasterXSize());
        nBlockYSize = MIN(1024, poDS->GetRasterYSize());
        poDS->SetMetadataItem( "INTERLEAVE", "PIXEL", "IMAGE_STRUCTURE" );
    }
}

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

GDALColorInterp PDFRasterBand::GetColorInterpretation()
{
    PDFDataset *poGDS = (PDFDataset *) poDS;
    if (poGDS->nBands == 1)
        return GCI_GrayIndex;
    else
        return (GDALColorInterp)(GCI_RedBand + (nBand - 1));
}

/************************************************************************/
/*                         IReadBlockFromTile()                         */
/************************************************************************/

CPLErr PDFRasterBand::IReadBlockFromTile( int nBlockXOff, int nBlockYOff,
                                          void * pImage )

{
    PDFDataset *poGDS = (PDFDataset *) poDS;

    int nReqXSize = nBlockXSize;
    int nReqYSize = nBlockYSize;
    if( (nBlockXOff + 1) * nBlockXSize > nRasterXSize )
        nReqXSize = nRasterXSize - nBlockXOff * nBlockXSize;
    if( (nBlockYOff + 1) * nBlockYSize > nRasterYSize )
        nReqYSize = nRasterYSize - nBlockYOff * nBlockYSize;

    int nXBlocks = DIV_ROUND_UP(nRasterXSize, nBlockXSize);
    int iTile = poGDS->aiTiles[nBlockYOff * nXBlocks + nBlockXOff];
    GDALPDFTileDesc& sTile = poGDS->asTiles[iTile];
    GDALPDFObject* poImage = sTile.poImage;

    if( iTile < 0 )
    {
        memset(pImage, 0, nBlockXSize * nBlockYSize);
        return CE_None;
    }

    if( nBand == 4 )
    {
        GDALPDFDictionary* poImageDict = poImage->GetDictionary();
        GDALPDFObject* poSMask = poImageDict->Get("SMask");
        if( poSMask != NULL && poSMask->GetType() == PDFObjectType_Dictionary )
        {
            GDALPDFDictionary* poSMaskDict = poSMask->GetDictionary();
            GDALPDFObject* poWidth = poSMaskDict->Get("Width");
            GDALPDFObject* poHeight = poSMaskDict->Get("Height");
            GDALPDFObject* poColorSpace = poSMaskDict->Get("ColorSpace");
            GDALPDFObject* poBitsPerComponent = poSMaskDict->Get("BitsPerComponent");
            int nBits = 0;
            if( poBitsPerComponent )
                nBits = (int)Get(poBitsPerComponent);
            if (poWidth && Get(poWidth) == nReqXSize &&
                poHeight && Get(poHeight) == nReqYSize &&
                poColorSpace && poColorSpace->GetType() == PDFObjectType_Name &&
                poColorSpace->GetName() == "DeviceGray" &&
                (nBits == 1 || nBits == 8) )
            {
                GDALPDFStream* poStream = poSMask->GetStream();
                GByte* pabyStream = NULL;

                if( poStream == NULL )
                    return CE_Failure;

                pabyStream = (GByte*) poStream->GetBytes();
                if( pabyStream == NULL )
                    return CE_Failure;

                int nReqXSize1 = (nReqXSize + 7) / 8;
                if( (nBits == 8 && poStream->GetLength() != nReqXSize * nReqYSize) ||
                    (nBits == 1 && poStream->GetLength() != nReqXSize1 * nReqYSize) )
                {
                    VSIFree(pabyStream);
                    return CE_Failure;
                }

                GByte* pabyData = (GByte*) pImage;
                if( nReqXSize != nBlockXSize || nReqYSize != nBlockYSize )
                {
                    memset(pabyData, 0, nBlockXSize * nBlockYSize);
                }

                if( nBits == 8 )
                {
                    for(int j = 0; j < nReqYSize; j++)
                    {
                        for(int i = 0; i < nReqXSize; i++)
                        {
                            pabyData[j * nBlockXSize + i] = pabyStream[j * nReqXSize + i];
                        }
                    }
                }
                else
                {
                    for(int j = 0; j < nReqYSize; j++)
                    {
                        for(int i = 0; i < nReqXSize; i++)
                        {
                            if( pabyStream[j * nReqXSize1 + i / 8] & (1 << (7 - (i % 8))) )
                                pabyData[j * nBlockXSize + i] = 255;
                            else
                                pabyData[j * nBlockXSize + i] = 0;
                        }
                    }
                }

                VSIFree(pabyStream);
                return CE_None;
            }
        }

        memset(pImage, 255, nBlockXSize * nBlockYSize);
        return CE_None;
    }

    if( poGDS->nLastBlockXOff == nBlockXOff &&
        poGDS->nLastBlockYOff == nBlockYOff &&
        poGDS->pabyCachedData != NULL )
    {
        CPLDebug("PDF", "Using cached block (%d, %d)",
                 nBlockXOff, nBlockYOff);
        // do nothing
    }
    else
    {
        if (poGDS->bTried == FALSE)
        {
            poGDS->bTried = TRUE;
            poGDS->pabyCachedData = (GByte*)VSIMalloc3(3, nBlockXSize, nBlockYSize);
        }
        if (poGDS->pabyCachedData == NULL)
            return CE_Failure;

        GDALPDFStream* poStream = poImage->GetStream();
        GByte* pabyStream = NULL;

        if( poStream == NULL )
            return CE_Failure;

        pabyStream = (GByte*) poStream->GetBytes();
        if( pabyStream == NULL )
            return CE_Failure;

        if( poStream->GetLength() != sTile.nBands * nReqXSize * nReqYSize)
        {
            VSIFree(pabyStream);
            return CE_Failure;
        }

        memcpy(poGDS->pabyCachedData, pabyStream, poStream->GetLength());
        VSIFree(pabyStream);
        poGDS->nLastBlockXOff = nBlockXOff;
        poGDS->nLastBlockYOff = nBlockYOff;
    }

    GByte* pabyData = (GByte*) pImage;
    if( nBand != 4 && (nReqXSize != nBlockXSize || nReqYSize != nBlockYSize) )
    {
        memset(pabyData, 0, nBlockXSize * nBlockYSize);
    }

    if( poGDS->nBands >= 3 && sTile.nBands == 3 )
    {
        for(int j = 0; j < nReqYSize; j++)
        {
            for(int i = 0; i < nReqXSize; i++)
            {
                pabyData[j * nBlockXSize + i] = poGDS->pabyCachedData[3 * (j * nReqXSize + i) + nBand - 1];
            }
        }
    }
    else if( sTile.nBands == 1 )
    {
        for(int j = 0; j < nReqYSize; j++)
        {
            for(int i = 0; i < nReqXSize; i++)
            {
                pabyData[j * nBlockXSize + i] = poGDS->pabyCachedData[j * nReqXSize + i];
            }
        }
    }

    return CE_None;
}

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

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

{
    PDFDataset *poGDS = (PDFDataset *) poDS;

    if (poGDS->aiTiles.size() )
    {
        if ( IReadBlockFromTile(nBlockXOff, nBlockYOff,
                                pImage) == CE_None )
        {
            return CE_None;
        }
        else
        {
            poGDS->aiTiles.resize(0);
            poGDS->bTried = FALSE;
            CPLFree(poGDS->pabyCachedData);
            poGDS->pabyCachedData = NULL;
            poGDS->nLastBlockXOff = -1;
            poGDS->nLastBlockYOff = -1;
        }
    }

    int nReqXSize = nBlockXSize;
    int nReqYSize = nBlockYSize;
    if( (nBlockXOff + 1) * nBlockXSize > nRasterXSize )
        nReqXSize = nRasterXSize - nBlockXOff * nBlockXSize;
    if( nBlockYSize == 1 )
        nReqYSize = nRasterYSize;
    else if( (nBlockYOff + 1) * nBlockYSize > nRasterYSize )
        nReqYSize = nRasterYSize - nBlockYOff * nBlockYSize;

    if (poGDS->bTried == FALSE)
    {
        poGDS->bTried = TRUE;
        if( nBlockYSize == 1 )
            poGDS->pabyCachedData = (GByte*)VSIMalloc3(MAX(3, poGDS->nBands), nRasterXSize, nRasterYSize);
        else
            poGDS->pabyCachedData = (GByte*)VSIMalloc3(MAX(3, poGDS->nBands), nBlockXSize, nBlockYSize);
    }
    if (poGDS->pabyCachedData == NULL)
        return CE_Failure;

    if( poGDS->nLastBlockXOff == nBlockXOff &&
        (nBlockYSize == 1 || poGDS->nLastBlockYOff == nBlockYOff) &&
        poGDS->pabyCachedData != NULL )
    {
        /*CPLDebug("PDF", "Using cached block (%d, %d)",
                 nBlockXOff, nBlockYOff);*/
        // do nothing
    }
    else
    {
#ifdef HAVE_PODOFO
        if (!poGDS->bUsePoppler && nBand == 4)
        {
            memset(pImage, 255, nBlockXSize * nBlockYSize);
            return CE_None;
        }
#endif

        CPLErr eErr = poGDS->ReadPixels( nBlockXOff * nBlockXSize,
                                         (nBlockYSize == 1) ? 0 : nBlockYOff * nBlockYSize,
                                         nReqXSize,
                                         nReqYSize,
                                         1,
                                         nBlockXSize,
                                         nBlockXSize * ((nBlockYSize == 1) ? nRasterYSize : nBlockYSize),
                                         poGDS->pabyCachedData);
        if( eErr == CE_None )
        {
            poGDS->nLastBlockXOff = nBlockXOff;
            poGDS->nLastBlockYOff = nBlockYOff;
        }
        else
        {
            CPLFree(poGDS->pabyCachedData);
            poGDS->pabyCachedData = NULL;
        }

    }
    if (poGDS->pabyCachedData == NULL)
        return CE_Failure;

    if( nBlockYSize == 1 )
        memcpy(pImage,
               poGDS->pabyCachedData + (nBand - 1) * nBlockXSize * nRasterYSize + nBlockYOff * nBlockXSize,
               nBlockXSize);
    else
        memcpy(pImage,
               poGDS->pabyCachedData + (nBand - 1) * nBlockXSize * nBlockYSize,
               nBlockXSize * nBlockYSize);

    return CE_None;
}

/************************************************************************/
/*                             GetOption()                              */
/************************************************************************/

const char* PDFDataset::GetOption(char** papszOpenOptions,
                                  const char* pszOptionName,
                                  const char* pszDefaultVal)
{
    CPLErr eLastErrType = CPLGetLastErrorType();
    int nLastErrno = CPLGetLastErrorNo();
    CPLString osLastErrorMsg(CPLGetLastErrorMsg());
    CPLXMLNode* psNode = CPLParseXMLString(pszOpenOptionList);
    CPLErrorSetState(eLastErrType, nLastErrno, osLastErrorMsg);
    if( psNode == NULL ) return pszDefaultVal;
    CPLXMLNode* psIter = psNode->psChild;
    while( psIter != NULL )
    {
        if( EQUAL(CPLGetXMLValue( psIter, "name", "" ), pszOptionName) )
        {
            const char* pszVal = CSLFetchNameValue(papszOpenOptions, pszOptionName);
            if( pszVal != NULL )
            {
                CPLDestroyXMLNode(psNode);
                return pszVal;
            }
            const char* pszAltConfigOption = CPLGetXMLValue( psIter, "alt_config_option", NULL );
            if( pszAltConfigOption != NULL )
            {
                pszVal = CPLGetConfigOption(pszAltConfigOption, pszDefaultVal);
                CPLDestroyXMLNode(psNode);
                return pszVal;
            }
            CPLDestroyXMLNode(psNode);
            return pszDefaultVal;
        }
        psIter = psIter->psNext;
    }
    CPLError(CE_Failure, CPLE_AppDefined,
             "Requesting an undocumented open option '%s'", pszOptionName);
    CPLDestroyXMLNode(psNode);
    return pszDefaultVal;
}

/************************************************************************/
/*                             ReadPixels()                             */
/************************************************************************/

CPLErr PDFDataset::ReadPixels( int nReqXOff, int nReqYOff,
                               int nReqXSize, int nReqYSize,
                               GSpacing nPixelSpace, GSpacing nLineSpace,
                               GSpacing nBandSpace,
                               GByte* pabyData )
{
    CPLErr eErr = CE_None;
    const char* pszRenderingOptions = GetOption(papszOpenOptions, "RENDERING_OPTIONS", NULL);

#ifdef HAVE_POPPLER
    if(bUsePoppler)
    {
        SplashColor sColor;
        sColor[0] = 255;
        sColor[1] = 255;
        sColor[2] = 255;
        GDALPDFOutputDev *poSplashOut;
        poSplashOut = new GDALPDFOutputDev((nBands < 4) ? splashModeRGB8 : splashModeXBGR8,
                                            4, gFalse,
                                            (nBands < 4) ? sColor : NULL);

        if (pszRenderingOptions != NULL)
        {
            poSplashOut->SetEnableVector(FALSE);
            poSplashOut->SetEnableText(FALSE);
            poSplashOut->SetEnableBitmap(FALSE);

            char** papszTokens = CSLTokenizeString2( pszRenderingOptions, " ,", 0 );
            for(int i=0;papszTokens[i] != NULL;i++)
            {
                if (EQUAL(papszTokens[i], "VECTOR"))
                    poSplashOut->SetEnableVector(TRUE);
                else if (EQUAL(papszTokens[i], "TEXT"))
                    poSplashOut->SetEnableText(TRUE);
                else if (EQUAL(papszTokens[i], "RASTER") ||
                            EQUAL(papszTokens[i], "BITMAP"))
                    poSplashOut->SetEnableBitmap(TRUE);
                else
                {
                    CPLError(CE_Warning, CPLE_NotSupported,
                                "Value %s is not a valid value for GDAL_PDF_RENDERING_OPTIONS",
                                papszTokens[i]);
                }
            }
            CSLDestroy(papszTokens);
        }

        PDFDoc* poDoc = poDocPoppler;
#ifdef POPPLER_0_20_OR_LATER
        poSplashOut->startDoc(poDoc);
#else
        poSplashOut->startDoc(poDoc->getXRef());
#endif

        /* EVIL: we modify a private member... */
        /* poppler (at least 0.12 and 0.14 versions) don't render correctly */
        /* some PDFs and display an error message 'Could not find a OCG with Ref' */
        /* in those cases. This processing of optional content is an addition of */
        /* poppler in comparison to original xpdf, which hasn't the issue. All in */
        /* all, nullifying optContent removes the error message and improves the rendering */
#ifdef POPPLER_HAS_OPTCONTENT
        Catalog* poCatalog = poDoc->getCatalog();
        OCGs* poOldOCGs = poCatalog->optContent;
        if (!bUseOCG)
            poCatalog->optContent = NULL;
#endif
        poDoc->displayPageSlice(poSplashOut,
                                iPage,
                                dfDPI, dfDPI,
                                0,
                                TRUE, gFalse, gFalse,
                                nReqXOff, nReqYOff,
                                nReqXSize, nReqYSize);

    /* Restore back */
#ifdef POPPLER_HAS_OPTCONTENT
        poCatalog->optContent = poOldOCGs;
#endif

        SplashBitmap* poBitmap = poSplashOut->getBitmap();
        if (poBitmap->getWidth() != nReqXSize || poBitmap->getHeight() != nReqYSize)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                        "Bitmap decoded size (%dx%d) doesn't match raster size (%dx%d)" ,
                        poBitmap->getWidth(), poBitmap->getHeight(),
                        nReqXSize, nReqYSize);
            delete poSplashOut;
            return CE_Failure;
        }

        GByte* pabyDataR = pabyData;
        GByte* pabyDataG = pabyData + nBandSpace;
        GByte* pabyDataB = pabyData + 2 * nBandSpace;
        GByte* pabyDataA = pabyData + 3 * nBandSpace;
        GByte* pabySrc   = poBitmap->getDataPtr();
        GByte* pabyAlphaSrc  = (GByte*)poBitmap->getAlphaPtr();
        int i, j;
        for(j=0;j<nReqYSize;j++)
        {
            for(i=0;i<nReqXSize;i++)
            {
                if (nBands < 4)
                {
                    pabyDataR[i * nPixelSpace] = pabySrc[i * 3 + 0];
                    pabyDataG[i * nPixelSpace] = pabySrc[i * 3 + 1];
                    pabyDataB[i * nPixelSpace] = pabySrc[i * 3 + 2];
                }
                else
                {
                    pabyDataR[i * nPixelSpace] = pabySrc[i * 4 + 2];
                    pabyDataG[i * nPixelSpace] = pabySrc[i * 4 + 1];
                    pabyDataB[i * nPixelSpace] = pabySrc[i * 4 + 0];
                    pabyDataA[i * nPixelSpace] = pabyAlphaSrc[i];
                }
            }
            pabyDataR += nLineSpace;
            pabyDataG += nLineSpace;
            pabyDataB += nLineSpace;
            pabyDataA += nLineSpace;
            pabyAlphaSrc += poBitmap->getAlphaRowSize();
            pabySrc += poBitmap->getRowSize();
        }
        delete poSplashOut;
    }
#endif // HAVE_POPPLER

#ifdef HAVE_PODOFO
    if (!bUsePoppler)
    {
        if( bPdfToPpmFailed )
            return CE_Failure;

        if (pszRenderingOptions != NULL)
        {
            CPLError(CE_Warning, CPLE_NotSupported,
                        "GDAL_PDF_RENDERING_OPTIONS only supported "
                        "when PDF driver is compiled against Poppler.");
        }

        CPLString osTmpFilename;
        int nRet;

#ifdef notdef
        int bUseSpawn = CSLTestBoolean(CPLGetConfigOption("GDAL_PDF_USE_SPAWN", "YES"));
        if( !bUseSpawn )
        {
            CPLString osCmd = CPLSPrintf("pdftoppm -r %f -x %d -y %d -W %d -H %d -f %d -l %d \"%s\"",
                    dfDPI,
                    nReqXOff,
                    nReqYOff,
                    nReqXSize,
                    nReqYSize,
                    iPage, iPage,
                    osFilename.c_str());

            if (osUserPwd.size() != 0)
            {
                osCmd += " -upw \"";
                osCmd += osUserPwd;
                osCmd += "\"";
            }

            CPLString osTmpFilenamePrefix = CPLGenerateTempFilename("pdf");
            osTmpFilename = CPLSPrintf("%s-%d.ppm",
                                           osTmpFilenamePrefix.c_str(),
                                           iPage);
            osCmd += CPLSPrintf(" \"%s\"", osTmpFilenamePrefix.c_str());

            CPLDebug("PDF", "Running '%s'", osCmd.c_str());
            nRet = CPLSystem(NULL, osCmd.c_str());
        }
        else
#endif // notdef
        {
            char** papszArgs = NULL;
            papszArgs = CSLAddString(papszArgs, "pdftoppm");
            papszArgs = CSLAddString(papszArgs, "-r");
            papszArgs = CSLAddString(papszArgs, CPLSPrintf("%f", dfDPI));
            papszArgs = CSLAddString(papszArgs, "-x");
            papszArgs = CSLAddString(papszArgs, CPLSPrintf("%d", nReqXOff));
            papszArgs = CSLAddString(papszArgs, "-y");
            papszArgs = CSLAddString(papszArgs, CPLSPrintf("%d", nReqYOff));
            papszArgs = CSLAddString(papszArgs, "-W");
            papszArgs = CSLAddString(papszArgs, CPLSPrintf("%d", nReqXSize));
            papszArgs = CSLAddString(papszArgs, "-H");
            papszArgs = CSLAddString(papszArgs, CPLSPrintf("%d", nReqYSize));
            papszArgs = CSLAddString(papszArgs, "-f");
            papszArgs = CSLAddString(papszArgs, CPLSPrintf("%d", iPage));
            papszArgs = CSLAddString(papszArgs, "-l");
            papszArgs = CSLAddString(papszArgs, CPLSPrintf("%d", iPage));
            if (osUserPwd.size() != 0)
            {
                papszArgs = CSLAddString(papszArgs, "-upw");
                papszArgs = CSLAddString(papszArgs, osUserPwd.c_str());
            }
            papszArgs = CSLAddString(papszArgs, osFilename.c_str());

            osTmpFilename = CPLSPrintf("/vsimem/pdf/temp_%p.ppm", this);
            VSILFILE* fpOut = VSIFOpenL(osTmpFilename, "wb");
            if( fpOut != NULL )
            {
                nRet = CPLSpawn(papszArgs, NULL, fpOut, FALSE);
                VSIFCloseL(fpOut);
            }
            else
                nRet = -1;

            CSLDestroy(papszArgs);
        }

        if (nRet == 0)
        {
            GDALDataset* poDS = (GDALDataset*) GDALOpen(osTmpFilename, GA_ReadOnly);
            if (poDS)
            {
                if (poDS->GetRasterCount() == 3)
                {
                    eErr = poDS->RasterIO(GF_Read, 0, 0,
                                          nReqXSize,
                                          nReqYSize,
                                          pabyData,
                                          nReqXSize, nReqYSize,
                                          GDT_Byte, 3, NULL,
                                          nPixelSpace, nLineSpace, nBandSpace, NULL);
                }
                delete poDS;
            }
        }
        else
        {
            CPLDebug("PDF", "Ret code = %d", nRet);
            bPdfToPpmFailed = TRUE;
            eErr = CE_Failure;
        }
        VSIUnlink(osTmpFilename);
    }
#endif

    return eErr;
}

/************************************************************************/
/* ==================================================================== */
/*                        PDFImageRasterBand                            */
/* ==================================================================== */
/************************************************************************/

class PDFImageRasterBand : public PDFRasterBand
{
    friend class PDFDataset;

  public:

                PDFImageRasterBand( PDFDataset *, int );

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


/************************************************************************/
/*                        PDFImageRasterBand()                          */
/************************************************************************/

PDFImageRasterBand::PDFImageRasterBand( PDFDataset *poDS, int nBand ) : PDFRasterBand(poDS, nBand)

{
}

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

CPLErr PDFImageRasterBand::IReadBlock( int CPL_UNUSED nBlockXOff, int nBlockYOff,
                                       void * pImage )
{
    PDFDataset *poGDS = (PDFDataset *) poDS;
    CPLAssert(poGDS->poImageObj != NULL);

    if (poGDS->bTried == FALSE)
    {
        int nBands = (poGDS->nBands == 1) ? 1 : 3;
        poGDS->bTried = TRUE;
        if (nBands == 3)
        {
            poGDS->pabyCachedData = (GByte*)VSIMalloc3(nBands, nRasterXSize, nRasterYSize);
            if (poGDS->pabyCachedData == NULL)
                return CE_Failure;
        }

        GDALPDFStream* poStream = poGDS->poImageObj->GetStream();
        GByte* pabyStream = NULL;

        if (poStream == NULL ||
            poStream->GetLength() != nBands * nRasterXSize * nRasterYSize ||
            (pabyStream = (GByte*) poStream->GetBytes()) == NULL)
        {
            VSIFree(poGDS->pabyCachedData);
            poGDS->pabyCachedData = NULL;
            return CE_Failure;
        }

        if (nBands == 3)
        {
            /* pixel interleaved to band interleaved */
            for(int i = 0; i < nRasterXSize * nRasterYSize; i++)
            {
                poGDS->pabyCachedData[0 * nRasterXSize * nRasterYSize + i] = pabyStream[3 * i + 0];
                poGDS->pabyCachedData[1 * nRasterXSize * nRasterYSize + i] = pabyStream[3 * i + 1];
                poGDS->pabyCachedData[2 * nRasterXSize * nRasterYSize + i] = pabyStream[3 * i + 2];
            }
            VSIFree(pabyStream);
        }
        else
            poGDS->pabyCachedData = pabyStream;
    }

    if (poGDS->pabyCachedData == NULL)
        return CE_Failure;

    if (nBand == 4)
        memset(pImage, 255, nRasterXSize);
    else
        memcpy(pImage,
            poGDS->pabyCachedData + (nBand - 1) * nRasterXSize * nRasterYSize + nBlockYOff * nRasterXSize,
            nRasterXSize);

    return CE_None;
}

/************************************************************************/
/*                            ~PDFDataset()                            */
/************************************************************************/

PDFDataset::PDFDataset()
{
    bUsePoppler = FALSE;
#ifdef HAVE_POPPLER
    poDocPoppler = NULL;
#endif
#ifdef HAVE_PODOFO
    poDocPodofo = NULL;
    bPdfToPpmFailed = FALSE;
#endif
    poImageObj = NULL;
    pszWKT = NULL;
    dfDPI = GDAL_DEFAULT_DPI;
    dfMaxArea = 0;
    adfGeoTransform[0] = 0;
    adfGeoTransform[1] = 1;
    adfGeoTransform[2] = 0;
    adfGeoTransform[3] = 0;
    adfGeoTransform[4] = 0;
    adfGeoTransform[5] = 1;
    bHasCTM = FALSE;
    bGeoTransformValid = FALSE;
    nGCPCount = 0;
    pasGCPList = NULL;
    bProjDirty = FALSE;
    bNeatLineDirty = FALSE;
    bInfoDirty = FALSE;
    bXMPDirty = FALSE;
    bTried = FALSE;
    pabyCachedData = NULL;
    nLastBlockXOff = -1;
    nLastBlockYOff = -1;
    iPage = -1;
    poNeatLine = NULL;
    bUseOCG = FALSE;
    poCatalogObject = NULL;
#ifdef HAVE_POPPLER
    poCatalogObjectPoppler = NULL;
#endif
    nBlockXSize = 0;
    nBlockYSize = 0;
    papszOpenOptions = NULL;
    
    nLayers = 0;
    papoLayers = NULL;

    dfPageWidth = dfPageHeight = 0;

    bSetStyle = CSLTestBoolean(CPLGetConfigOption("OGR_PDF_SET_STYLE", "YES"));

    InitMapOperators();
}

/************************************************************************/
/*                           PDFFreeDoc()                               */
/************************************************************************/

#ifdef HAVE_POPPLER
static void PDFFreeDoc(PDFDoc* poDoc)
{
    if (poDoc)
    {
        /* hack to avoid potential cross heap issues on Win32 */
        /* str is the VSIPDFFileStream object passed in the constructor of PDFDoc */
        // NOTE: This is potentially very dangerous. See comment in VSIPDFFileStream::FillBuffer() */
        delete poDoc->str;
        poDoc->str = NULL;

        delete poDoc;
    }
}
#endif

/************************************************************************/
/*                            GetCatalog()                              */
/************************************************************************/

GDALPDFObject* PDFDataset::GetCatalog()
{
    if (poCatalogObject)
        return poCatalogObject;

#ifdef HAVE_POPPLER
    if (bUsePoppler)
    {
        poCatalogObjectPoppler = new ObjectAutoFree;
        poDocPoppler->getXRef()->getCatalog(poCatalogObjectPoppler);
        if (!poCatalogObjectPoppler->isNull())
            poCatalogObject = new GDALPDFObjectPoppler(poCatalogObjectPoppler, FALSE);
    }
#endif

#ifdef HAVE_PODOFO
    if (!bUsePoppler)
    {
        int nCatalogNum = 0, nCatalogGen = 0;
        VSILFILE* fp = VSIFOpenL(osFilename.c_str(), "rb");
        if (fp != NULL)
        {
            GDALPDFWriter oWriter(fp, TRUE);
            if (oWriter.ParseTrailerAndXRef())
            {
                nCatalogNum = oWriter.GetCatalogNum();
                nCatalogGen = oWriter.GetCatalogGen();
            }
            oWriter.Close();
        }

        PoDoFo::PdfObject* poCatalogPodofo =
            poDocPodofo->GetObjects().GetObject(PoDoFo::PdfReference(nCatalogNum, nCatalogGen));
        if (poCatalogPodofo)
            poCatalogObject = new GDALPDFObjectPodofo(poCatalogPodofo, poDocPodofo->GetObjects());
    }
#endif

    return poCatalogObject;
}

/************************************************************************/
/*                            ~PDFDataset()                            */
/************************************************************************/

PDFDataset::~PDFDataset()
{
    CPLFree(pabyCachedData);
    pabyCachedData = NULL;

    delete poNeatLine;
    poNeatLine = NULL;

    /* Collect data necessary to update */
    int nNum = poPageObj->GetRefNum();
    int nGen = poPageObj->GetRefGen();
    GDALPDFDictionaryRW* poPageDictCopy = NULL;
    GDALPDFDictionaryRW* poCatalogDictCopy = NULL;
    if (eAccess == GA_Update &&
        (bProjDirty || bNeatLineDirty || bInfoDirty || bXMPDirty) &&
        nNum != 0 &&
        poPageObj != NULL &&
        poPageObj->GetType() == PDFObjectType_Dictionary)
    {
        poPageDictCopy = poPageObj->GetDictionary()->Clone();

        if (bXMPDirty)
        {
            /* We need the catalog because it points to the XMP Metadata object */
            GetCatalog();
            if (poCatalogObject && poCatalogObject->GetType() == PDFObjectType_Dictionary)
                poCatalogDictCopy = poCatalogObject->GetDictionary()->Clone();
        }
    }

    /* Close document (and file descriptor) to be able to open it */
    /* in read-write mode afterwards */
    delete poPageObj;
    poPageObj = NULL;
    delete poCatalogObject;
    poCatalogObject = NULL;
#ifdef HAVE_POPPLER
    delete poCatalogObjectPoppler;
    PDFFreeDoc(poDocPoppler);
    poDocPoppler = NULL;
#endif
#ifdef HAVE_PODOFO
    delete poDocPodofo;
    poDocPodofo = NULL;
#endif

    /* Now do the update */
    if (poPageDictCopy)
    {
        VSILFILE* fp = VSIFOpenL(osFilename, "rb+");
        if (fp != NULL)
        {
            GDALPDFWriter oWriter(fp, TRUE);
            if (oWriter.ParseTrailerAndXRef())
            {
                if ((bProjDirty || bNeatLineDirty) && poPageDictCopy != NULL)
                    oWriter.UpdateProj(this, dfDPI,
                                        poPageDictCopy, nNum, nGen);

                if (bInfoDirty)
                    oWriter.UpdateInfo(this);

                if (bXMPDirty && poCatalogDictCopy != NULL)
                    oWriter.UpdateXMP(this, poCatalogDictCopy);
            }
            oWriter.Close();
        }
        else
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Cannot open %s in update mode", osFilename.c_str());
        }
    }
    delete poPageDictCopy;
    poPageDictCopy = NULL;
    delete poCatalogDictCopy;
    poCatalogDictCopy = NULL;

    if( nGCPCount > 0 )
    {
        GDALDeinitGCPs( nGCPCount, pasGCPList );
        CPLFree( pasGCPList );
        pasGCPList = NULL;
        nGCPCount = 0;
    }
    CPLFree(pszWKT);
    pszWKT = NULL;
    CSLDestroy(papszOpenOptions);

    CleanupIntermediateResources();

    for(int i=0;i<nLayers;i++)
        delete papoLayers[i];
    CPLFree( papoLayers );
}


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

CPLErr PDFDataset::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)
{
    int nBandBlockXSize, nBandBlockYSize;
    GetRasterBand(1)->GetBlockSize(&nBandBlockXSize, &nBandBlockYSize);
    if( aiTiles.size() == 0 &&
        eRWFlag == GF_Read && nXSize == nBufXSize && nYSize == nBufYSize &&
        (nBufXSize > nBandBlockXSize || nBufYSize > nBandBlockYSize) &&
        eBufType == GDT_Byte && nBandCount == nBands &&
        (nBands >= 3 && panBandMap[0] == 1 && panBandMap[1] == 2 &&
         panBandMap[2] == 3 && ( nBands == 3 || panBandMap[3] == 4)) )
    {
        int bReadPixels = TRUE;
#ifdef HAVE_PODOFO
        if (!bUsePoppler && nBands == 4)
        {
            bReadPixels = FALSE;
        }
#endif
        if( bReadPixels )
            return ReadPixels(nXOff, nYOff, nXSize, nYSize,
                              nPixelSpace, nLineSpace, nBandSpace, (GByte*)pData);
    }

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

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

int PDFDataset::Identify( GDALOpenInfo * poOpenInfo )
{
    if (strncmp(poOpenInfo->pszFilename, "PDF:", 4) == 0)
        return TRUE;
    if (strncmp(poOpenInfo->pszFilename, "PDF_IMAGE:", 10) == 0)
        return TRUE;

    if (poOpenInfo->nHeaderBytes < 128)
        return FALSE;

    return strncmp((const char*)poOpenInfo->pabyHeader, "%PDF", 4) == 0;
}

/************************************************************************/
/*                    PDFDatasetErrorFunction()                         */
/************************************************************************/

#ifdef HAVE_POPPLER

static void PDFDatasetErrorFunctionCommon(const CPLString& osError)
{
    if (strcmp(osError.c_str(), "Incorrect password") == 0)
        return;
    /* Reported on newer USGS GeoPDF */
    if (strcmp(osError.c_str(), "Couldn't find group for reference to set OFF") == 0)
    {
        CPLDebug("PDF", "%s", osError.c_str());
        return;
    }

    CPLError(CE_Failure, CPLE_AppDefined, "%s", osError.c_str());
}

#ifdef POPPLER_0_20_OR_LATER
static void PDFDatasetErrorFunction(CPL_UNUSED void* userData, CPL_UNUSED ErrorCategory eErrCatagory,
#ifdef POPPLER_0_23_OR_LATER
                                    Goffset nPos,
#else
                                    int nPos,
#endif
                                    char *pszMsg)
{
    CPLString osError;

    if (nPos >= 0)
        osError.Printf("Pos = %d, ", (int)nPos);
    osError += pszMsg;
    PDFDatasetErrorFunctionCommon(osError);
}
#else
static void PDFDatasetErrorFunction(int nPos, char *pszMsg, va_list args)
{
    CPLString osError;

    if (nPos >= 0)
        osError.Printf("Pos = %d, ", nPos);
    osError += CPLString().vPrintf(pszMsg, args);
    PDFDatasetErrorFunctionCommon(osError);
}
#endif
#endif

/************************************************************************/
/*                GDALPDFParseStreamContentOnlyDrawForm()               */
/************************************************************************/

static
CPLString GDALPDFParseStreamContentOnlyDrawForm(const char* pszContent)
{
    CPLString osToken;
    char ch;
    int nCurIdx = 0;
    CPLString osCurrentForm;

    //CPLDebug("PDF", "content = %s", pszContent);

    while((ch = *pszContent) != '\0')
    {
        if (ch == '%')
        {
            /* Skip comments until end-of-line */
            while((ch = *pszContent) != '\0')
            {
                if (ch == '\r' || ch == '\n')
                    break;
                pszContent ++;
            }
            if (ch == 0)
                break;
        }
        else if (ch == ' ' || ch == '\r' || ch == '\n')
        {
            if (osToken.size())
            {
                if (nCurIdx == 0 && osToken[0] == '/')
                {
                    osCurrentForm = osToken.substr(1);
                    nCurIdx ++;
                }
                else if (nCurIdx == 1 && osToken == "Do")
                {
                    nCurIdx ++;
                }
                else
                {
                    return "";
                }
            }
            osToken = "";
        }
        else
            osToken += ch;
        pszContent ++;
    }

    return osCurrentForm;
}

/************************************************************************/
/*                    GDALPDFParseStreamContent()                       */
/************************************************************************/

typedef enum
{
    STATE_INIT,
    STATE_AFTER_q,
    STATE_AFTER_cm,
    STATE_AFTER_Do
} PDFStreamState;

/* This parser is reduced to understanding sequences that draw rasters, such as :
   q
   scaleX 0 0 scaleY translateX translateY cm
   /ImXXX Do
   Q

   All other sequences will abort the parsing.

   Returns TRUE if the stream only contains images.
*/

static
int GDALPDFParseStreamContent(const char* pszContent,
                              GDALPDFDictionary* poXObjectDict,
                              double* pdfDPI,
                              int* pbDPISet,
                              int* pnBands,
                              std::vector<GDALPDFTileDesc>& asTiles,
                              int bAcceptRotationTerms)
{
    CPLString osToken;
    char ch;
    PDFStreamState nState = STATE_INIT;
    int nCurIdx = 0;
    double adfVals[6];
    CPLString osCurrentImage;

    double dfDPI = DEFAULT_DPI;
    *pbDPISet = FALSE;

    while((ch = *pszContent) != '\0')
    {
        if (ch == '%')
        {
            /* Skip comments until end-of-line */
            while((ch = *pszContent) != '\0')
            {
                if (ch == '\r' || ch == '\n')
                    break;
                pszContent ++;
            }
            if (ch == 0)
                break;
        }
        else if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n')
        {
            if (osToken.size())
            {
                if (nState == STATE_INIT)
                {
                    if (osToken == "q")
                    {
                        nState = STATE_AFTER_q;
                        nCurIdx = 0;
                    }
                    else if (osToken != "Q")
                        return FALSE;
                }
                else if (nState == STATE_AFTER_q)
                {
                    if (osToken == "q")
                    {
                        // ignore
                    }
                    else if (nCurIdx < 6)
                    {
                        adfVals[nCurIdx ++] = CPLAtof(osToken);
                    }
                    else if (nCurIdx == 6 && osToken == "cm")
                    {
                        nState = STATE_AFTER_cm;
                        nCurIdx = 0;
                    }
                    else
                        return FALSE;
                }
                else if (nState == STATE_AFTER_cm)
                {
                    if (nCurIdx == 0 && osToken[0] == '/')
                    {
                        osCurrentImage = osToken.substr(1);
                    }
                    else if (osToken == "Do")
                    {
                        nState = STATE_AFTER_Do;
                    }
                    else
                        return FALSE;
                }
                else if (nState == STATE_AFTER_Do)
                {
                    if (osToken == "Q")
                    {
                        GDALPDFObject* poImage = poXObjectDict->Get(osCurrentImage);
                        if (poImage != NULL && poImage->GetType() == PDFObjectType_Dictionary)
                        {
                            GDALPDFTileDesc sTile;
                            GDALPDFDictionary* poImageDict = poImage->GetDictionary();
                            GDALPDFObject* poWidth = poImageDict->Get("Width");
                            GDALPDFObject* poHeight = poImageDict->Get("Height");
                            GDALPDFObject* poColorSpace = poImageDict->Get("ColorSpace");
                            GDALPDFObject* poSMask = poImageDict->Get("SMask");
                            if (poColorSpace && poColorSpace->GetType() == PDFObjectType_Name)
                            {
                                if (poColorSpace->GetName() == "DeviceRGB")
                                {
                                    sTile.nBands = 3;
                                    if ( *pnBands < 3)
                                        *pnBands = 3;
                                }
                                else if (poColorSpace->GetName() == "DeviceGray")
                                {
                                    sTile.nBands = 1;
                                    if ( *pnBands < 1)
                                        *pnBands = 1;
                                }
                                else
                                    sTile.nBands = 0;
                            }
                            if ( poSMask != NULL )
                                *pnBands = 4;

                            if (poWidth && poHeight && ((bAcceptRotationTerms && adfVals[1] == -adfVals[2]) ||
                                                        (!bAcceptRotationTerms && adfVals[1] == 0.0 && adfVals[2] == 0.0)))
                            {
                                double dfWidth = Get(poWidth);
                                double dfHeight = Get(poHeight);
                                double dfScaleX = adfVals[0];
                                double dfScaleY = adfVals[3];
                                double dfDPI_X = ROUND_TO_INT_IF_CLOSE(dfWidth / dfScaleX * DEFAULT_DPI, 1e-3);
                                double dfDPI_Y = ROUND_TO_INT_IF_CLOSE(dfHeight / dfScaleY * DEFAULT_DPI, 1e-3);
                                //CPLDebug("PDF", "Image %s, width = %.16g, height = %.16g, scaleX = %.16g, scaleY = %.16g --> DPI_X = %.16g, DPI_Y = %.16g",
                                //                osCurrentImage.c_str(), dfWidth, dfHeight, dfScaleX, dfScaleY, dfDPI_X, dfDPI_Y);
                                if (dfDPI_X > dfDPI) dfDPI = dfDPI_X;
                                if (dfDPI_Y > dfDPI) dfDPI = dfDPI_Y;

                                memcpy(&(sTile.adfCM), adfVals, 6 * sizeof(double));
                                sTile.poImage = poImage;
                                sTile.dfWidth = dfWidth;
                                sTile.dfHeight = dfHeight;
                                asTiles.push_back(sTile);

                                *pbDPISet = TRUE;
                                *pdfDPI = dfDPI;
                            }
                        }
                        nState = STATE_INIT;
                    }
                    else
                        return FALSE;
                }
            }
            osToken = "";
        }
        else
            osToken += ch;
        pszContent ++;
    }

    return TRUE;
}

/************************************************************************/
/*                         CheckTiledRaster()                           */
/************************************************************************/

int PDFDataset::CheckTiledRaster()
{
    size_t i;
    int nBlockXSize = 0, nBlockYSize = 0;
    const double dfUserUnit = dfDPI * USER_UNIT_IN_INCH;

    /* First pass : check that all tiles have same DPI, */
    /* are contained entirely in the raster size, */
    /* and determine the block size */
    for(i=0; i<asTiles.size(); i++)
    {
        double dfDrawWidth = asTiles[i].adfCM[0] * dfUserUnit;
        double dfDrawHeight = asTiles[i].adfCM[3] * dfUserUnit;
        double dfX = asTiles[i].adfCM[4] * dfUserUnit;
        double dfY = asTiles[i].adfCM[5] * dfUserUnit;
        int nX = (int)(dfX+0.1);
        int nY = (int)(dfY+0.1);
        int nWidth = (int)(asTiles[i].dfWidth + 1e-8);
        int nHeight = (int)(asTiles[i].dfHeight + 1e-8);

        GDALPDFDictionary* poImageDict = asTiles[i].poImage->GetDictionary();
        GDALPDFObject* poBitsPerComponent = poImageDict->Get("BitsPerComponent");
        GDALPDFObject* poColorSpace = poImageDict->Get("ColorSpace");
        GDALPDFObject* poFilter = poImageDict->Get("Filter");

        /* Podofo cannot uncompress JPEG2000 streams */
        if( !bUsePoppler && poFilter != NULL &&
            poFilter->GetType() == PDFObjectType_Name &&
            poFilter->GetName() == "JPXDecode" )
        {
            CPLDebug("PDF", "Tile %d : Incompatible image for tiled reading",
                     (int)i);
            return FALSE;
        }

        if( poBitsPerComponent == NULL ||
            Get(poBitsPerComponent) != 8 ||
            poColorSpace == NULL ||
            poColorSpace->GetType() != PDFObjectType_Name ||
            (poColorSpace->GetName() != "DeviceRGB" &&
             poColorSpace->GetName() != "DeviceGray") )
        {
            CPLDebug("PDF", "Tile %d : Incompatible image for tiled reading",
                     (int)i);
            return FALSE;
        }

        if( fabs(dfDrawWidth - asTiles[i].dfWidth) > 1e-2 ||
            fabs(dfDrawHeight - asTiles[i].dfHeight) > 1e-2 ||
            fabs(nWidth - asTiles[i].dfWidth) > 1e-8 ||
            fabs(nHeight - asTiles[i].dfHeight) > 1e-8 ||
            fabs(nX - dfX) > 1e-1 ||
            fabs(nY - dfY) > 1e-1 ||
            nX < 0 || nY < 0 || nX + nWidth > nRasterXSize ||
            nY >= nRasterYSize )
        {
            CPLDebug("PDF", "Tile %d : %f %f %f %f %f %f",
                     (int)i, dfX, dfY, dfDrawWidth, dfDrawHeight,
                     asTiles[i].dfWidth, asTiles[i].dfHeight);
            return FALSE;
        }
        if( nBlockXSize == 0 && nBlockYSize == 0 &&
            nX == 0 && nY != 0 )
        {
            nBlockXSize = nWidth;
            nBlockYSize = nHeight;
        }
    }
    if( nBlockXSize <= 0 || nBlockYSize <= 0 || nBlockXSize > 2048 || nBlockYSize > 2048 )
        return FALSE;

    int nXBlocks = DIV_ROUND_UP(nRasterXSize, nBlockXSize);
    int nYBlocks = DIV_ROUND_UP(nRasterYSize, nBlockYSize);

    /* Second pass to determine that all tiles are properly aligned on block size */
    for(i=0; i<asTiles.size(); i++)
    {
        double dfX = asTiles[i].adfCM[4] * dfUserUnit;
        double dfY = asTiles[i].adfCM[5] * dfUserUnit;
        int nX = (int)(dfX+0.1);
        int nY = (int)(dfY+0.1);
        int nWidth = (int)(asTiles[i].dfWidth + 1e-8);
        int nHeight = (int)(asTiles[i].dfHeight + 1e-8);
        int bOK = TRUE;
        int nBlockXOff = nX / nBlockXSize;
        if( (nX % nBlockXSize) != 0 )
            bOK = FALSE;
        if( nBlockXOff < nXBlocks - 1 && nWidth != nBlockXSize )
            bOK = FALSE;
        if( nBlockXOff == nXBlocks - 1 && nX + nWidth != nRasterXSize )
            bOK = FALSE;

        if( nY > 0 && nHeight != nBlockYSize )
            bOK = FALSE;
        if( nY == 0 && nHeight != nRasterYSize - (nYBlocks - 1) * nBlockYSize)
            bOK = FALSE;

        if( !bOK )
        {
            CPLDebug("PDF", "Tile %d : %d %d %d %d",
                     (int)i, nX, nY, nWidth, nHeight);
            return FALSE;
        }
    }

    /* Third pass to set the aiTiles array */
    aiTiles.resize(nXBlocks * nYBlocks, -1);
    for(i=0; i<asTiles.size(); i++)
    {
        double dfX = asTiles[i].adfCM[4] * dfUserUnit;
        double dfY = asTiles[i].adfCM[5] * dfUserUnit;
        int nHeight = (int)(asTiles[i].dfHeight + 1e-8);
        int nX = (int)(dfX+0.1);
        int nY = nRasterYSize - ((int)(dfY+0.1) + nHeight);
        int nBlockXOff = nX / nBlockXSize;
        int nBlockYOff = nY / nBlockYSize;
        aiTiles[ nBlockYOff * nXBlocks + nBlockXOff ] = (int) i;
    }

    this->nBlockXSize = nBlockXSize;
    this->nBlockYSize = nBlockYSize;

    return TRUE;
}

/************************************************************************/
/*                              GuessDPI()                              */
/************************************************************************/

void PDFDataset::GuessDPI(GDALPDFDictionary* poPageDict, int* pnBands)
{
    const char* pszDPI = GetOption(papszOpenOptions, "DPI", NULL);
    if (pszDPI != NULL)
    {
        dfDPI = CPLAtof(pszDPI);
    }
    else
    {
        /* Try to get a better value from the images that are drawn */
        /* Very simplistic logic. Will only work for raster only PDF */

        GDALPDFObject* poContents = poPageDict->Get("Contents");
        if (poContents != NULL && poContents->GetType() == PDFObjectType_Array)
        {
            GDALPDFArray* poContentsArray = poContents->GetArray();
            if (poContentsArray->GetLength() == 1)
            {
                poContents = poContentsArray->Get(0);
            }
        }

        GDALPDFObject* poXObject = poPageDict->LookupObject("Resources.XObject");
        if (poContents != NULL &&
            poContents->GetType() == PDFObjectType_Dictionary &&
            poXObject != NULL &&
            poXObject->GetType() == PDFObjectType_Dictionary)
        {
            GDALPDFDictionary* poXObjectDict = poXObject->GetDictionary();
            GDALPDFDictionary* poContentDict = poXObjectDict;
            GDALPDFStream* poPageStream = poContents->GetStream();
            if (poPageStream != NULL)
            {
                char* pszContent = NULL;
                int nLength = poPageStream->GetLength();
                int bResetTiles = FALSE;
                double dfScaleDPI = 1.0;

                if( nLength < 100000 )
                {
                    CPLString osForm;
                    pszContent = poPageStream->GetBytes();
                    if( pszContent != NULL )
                    {
#ifdef DEBUG
                        const char* pszDumpStream = CPLGetConfigOption("PDF_DUMP_STREAM", NULL);
                        if( pszDumpStream != NULL )
                        {
                            VSILFILE* fpDump = VSIFOpenL(pszDumpStream, "wb");
                            if( fpDump )
                            {
                                VSIFWriteL(pszContent, 1, nLength, fpDump);
                                VSIFCloseL(fpDump);
                            }
                        }
#endif // DEBUG
                        osForm = GDALPDFParseStreamContentOnlyDrawForm(pszContent);
                        if (osForm.size() == 0)
                        {
                            /* Special case for USGS Topo PDF, like CA_Hollywood_20090811_OM_geo.pdf */
                            const char* pszOGCDo = strstr(pszContent, " /XO1 Do");
                            if( pszOGCDo )
                            {
                                const char* pszcm = strstr(pszContent, " cm ");
                                if( pszcm != NULL && pszcm < pszOGCDo )
                                {
                                    const char* pszNextcm = strstr(pszcm + 2, "cm");
                                    if( pszNextcm == NULL || pszNextcm > pszOGCDo )
                                    {
                                        const char* pszIter = pszcm;
                                        while( pszIter > pszContent )
                                        {
                                            if( (*pszIter >= '0' && *pszIter <= '9') ||
                                                *pszIter == '-' ||
                                                *pszIter == '.' ||
                                                *pszIter == ' ' )
                                                pszIter --;
                                            else
                                            {
                                                pszIter ++;
                                                break;
                                            }
                                        }
                                        CPLString oscm(pszIter);
                                        oscm.resize(pszcm - pszIter);
                                        char** papszTokens = CSLTokenizeString(oscm);
                                        double dfScaleX = -1, dfScaleY = -2;
                                        if( CSLCount(papszTokens) == 6 )
                                        {
                                            dfScaleX = CPLAtof(papszTokens[0]);
                                            dfScaleY = CPLAtof(papszTokens[3]);
                                        }
                                        CSLDestroy(papszTokens);
                                        if( dfScaleX == dfScaleY && dfScaleX > 0.0 )
                                        {
                                            osForm = "XO1";
                                            bResetTiles = TRUE;
                                            dfScaleDPI = 1.0 / dfScaleX;
                                        }
                                    }
                                }
                                else
                                {
                                    osForm = "XO1";
                                    bResetTiles = TRUE;
                                }
                            }
                            /* Special case for USGS Topo PDF, like CA_Sacramento_East_20120308_TM_geo.pdf */
                            else
                            {
                                CPLString osOCG = FindLayerOCG(poPageDict, "Orthoimage");
                                if( osOCG.size() )
                                {
                                    const char* pszBDCLookup = CPLSPrintf("/OC /%s BDC", osOCG.c_str());
                                    const char* pszBDC = strstr(pszContent, pszBDCLookup);
                                    if( pszBDC != NULL )
                                    {
                                        const char* pszIter = pszBDC + strlen(pszBDCLookup);
                                        while( *pszIter != '\0' )
                                        {
                                            if( *pszIter == 13 || *pszIter == 10 ||
                                                *pszIter == ' '|| *pszIter == 'q' )
                                                pszIter ++;
                                            else
                                                break;
                                        }
                                        if( strncmp(pszIter, "1 0 0 1 0 0 cm\n",
                                                    strlen( "1 0 0 1 0 0 cm\n" )) == 0 )
                                            pszIter += strlen("1 0 0 1 0 0 cm\n");
                                        if( *pszIter == '/' )
                                        {
                                            pszIter ++;
                                            const char* pszDo = strstr(pszIter, " Do");
                                            if( pszDo != NULL )
                                            {
                                                osForm = pszIter;
                                                osForm.resize(pszDo - pszIter);
                                                bResetTiles = TRUE;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }

                    if (osForm.size())
                    {
                        CPLFree(pszContent);
                        pszContent = NULL;

                        GDALPDFObject* poObjForm = poXObjectDict->Get(osForm);
                        if (poObjForm != NULL &&
                            poObjForm->GetType() == PDFObjectType_Dictionary &&
                            (poPageStream = poObjForm->GetStream()) != NULL)
                        {
                            GDALPDFDictionary* poObjFormDict = poObjForm->GetDictionary();
                            GDALPDFObject* poSubtype;
                            if ((poSubtype = poObjFormDict->Get("Subtype")) != NULL &&
                                poSubtype->GetType() == PDFObjectType_Name &&
                                poSubtype->GetName() == "Form")
                            {
                                int nLength = poPageStream->GetLength();
                                if( nLength < 100000 )
                                {
                                    pszContent = poPageStream->GetBytes();

                                    GDALPDFObject* poXObject2 = poObjFormDict->LookupObject("Resources.XObject");
                                    if( poXObject2 != NULL &&
                                        poXObject2->GetType() == PDFObjectType_Dictionary )
                                        poContentDict = poXObject2->GetDictionary();
                                }
                            }
                        }
                    }
                }

                if (pszContent != NULL)
                {
                    int bDPISet = FALSE;

                    const char* pszContentToParse = pszContent;
                    if( bResetTiles )
                    {
                        while( *pszContentToParse != '\0' )
                        {
                            if( *pszContentToParse == 13 || *pszContentToParse == 10 ||
                                *pszContentToParse == ' ' ||
                                (*pszContentToParse >= '0' && *pszContentToParse <= '9') ||
                                *pszContentToParse == '.' || 
                                *pszContentToParse == '-' || 
                                *pszContentToParse == 'l' ||
                                *pszContentToParse == 'm' ||
                                *pszContentToParse == 'n' ||
                                *pszContentToParse == 'W' )
                                pszContentToParse ++;
                            else
                                break;
                        }
                    }

                    GDALPDFParseStreamContent(pszContentToParse,
                                              poContentDict,
                                              &(dfDPI),
                                              &bDPISet,
                                              pnBands,
                                              asTiles,
                                              bResetTiles);
                    CPLFree(pszContent);
                    if (bDPISet)
                    {
                        dfDPI *= dfScaleDPI;

                        CPLDebug("PDF", "DPI guessed from contents stream = %.16g", dfDPI);
                        SetMetadataItem("DPI", CPLSPrintf("%.16g", dfDPI));
                        if( bResetTiles )
                            asTiles.resize(0);
                    }
                    else
                        asTiles.resize(0);
                }
            }
        }

        GDALPDFObject* poUserUnit = NULL;
        if ( (poUserUnit = poPageDict->Get("UserUnit")) != NULL &&
              (poUserUnit->GetType() == PDFObjectType_Int ||
               poUserUnit->GetType() == PDFObjectType_Real) )
        {
            dfDPI = ROUND_TO_INT_IF_CLOSE(Get(poUserUnit) * DEFAULT_DPI);
            CPLDebug("PDF", "Found UserUnit in Page --> DPI = %.16g", dfDPI);
            SetMetadataItem("DPI", CPLSPrintf("%.16g", dfDPI));
        }
    }

    if (dfDPI < 1 || dfDPI > 7200)
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "Invalid value for GDAL_PDF_DPI. Using default value instead");
        dfDPI = GDAL_DEFAULT_DPI;
    }
}

/************************************************************************/
/*                              FindXMP()                               */
/************************************************************************/

void PDFDataset::FindXMP(GDALPDFObject* poObj)
{
    if (poObj->GetType() != PDFObjectType_Dictionary)
        return;

    GDALPDFDictionary* poDict = poObj->GetDictionary();
    GDALPDFObject* poType = poDict->Get("Type");
    GDALPDFObject* poSubtype = poDict->Get("Subtype");
    if (poType == NULL ||
        poType->GetType() != PDFObjectType_Name ||
        poType->GetName() != "Metadata" ||
        poSubtype == NULL ||
        poSubtype->GetType() != PDFObjectType_Name ||
        poSubtype->GetName() != "XML")
    {
        return;
    }

    GDALPDFStream* poStream = poObj->GetStream();
    if (poStream == NULL)
        return;

    char* pszContent = poStream->GetBytes();
    int nLength = (int)poStream->GetLength();
    if (pszContent != NULL && nLength > 15 &&
        strncmp(pszContent, "<?xpacket begin=", strlen("<?xpacket begin=")) == 0)
    {
        char *apszMDList[2];
        apszMDList[0] = pszContent;
        apszMDList[1] = NULL;
        SetMetadata(apszMDList, "xml:XMP");
    }
    CPLFree(pszContent);
}

/************************************************************************/
/*                             ParseInfo()                              */
/************************************************************************/

void PDFDataset::ParseInfo(GDALPDFObject* poInfoObj)
{
    if (poInfoObj->GetType() != PDFObjectType_Dictionary)
        return;

    GDALPDFDictionary* poInfoObjDict = poInfoObj->GetDictionary();
    GDALPDFObject* poItem = NULL;
    int bOneMDISet = FALSE;
    if ((poItem = poInfoObjDict->Get("Author")) != NULL &&
            poItem->GetType() == PDFObjectType_String)
    {
        SetMetadataItem("AUTHOR", poItem->GetString().c_str());
        bOneMDISet = TRUE;
    }
    if ((poItem = poInfoObjDict->Get("Creator")) != NULL &&
            poItem->GetType() == PDFObjectType_String)
    {
        SetMetadataItem("CREATOR", poItem->GetString().c_str());
        bOneMDISet = TRUE;
    }
    if ((poItem = poInfoObjDict->Get("Keywords")) != NULL &&
            poItem->GetType() == PDFObjectType_String)
    {
        SetMetadataItem("KEYWORDS", poItem->GetString().c_str());
        bOneMDISet = TRUE;
    }
    if ((poItem = poInfoObjDict->Get("Subject")) != NULL &&
            poItem->GetType() == PDFObjectType_String)
    {
        SetMetadataItem("SUBJECT", poItem->GetString().c_str());
        bOneMDISet = TRUE;
    }
    if ((poItem = poInfoObjDict->Get("Title")) != NULL &&
            poItem->GetType() == PDFObjectType_String)
    {
        SetMetadataItem("TITLE", poItem->GetString().c_str());
        bOneMDISet = TRUE;
    }
    if ((poItem = poInfoObjDict->Get("Producer")) != NULL &&
            poItem->GetType() == PDFObjectType_String)
    {
        if (bOneMDISet || poItem->GetString() != "PoDoFo - http://podofo.sf.net")
        {
            SetMetadataItem("PRODUCER", poItem->GetString().c_str());
            bOneMDISet = TRUE;
        }
    }
    if ((poItem = poInfoObjDict->Get("CreationDate")) != NULL &&
        poItem->GetType() == PDFObjectType_String)
    {
        if (bOneMDISet)
            SetMetadataItem("CREATION_DATE", poItem->GetString().c_str());
    }
}

#ifdef HAVE_POPPLER

/************************************************************************/
/*                               AddLayer()                             */
/************************************************************************/

void PDFDataset::AddLayer(const char* pszLayerName, OptionalContentGroup* ocg)
{
    int nNewIndex = osLayerList.size() /*/ 2*/;

    if (nNewIndex == 100)
    {
        CPLStringList osNewLayerList;
        for(int i=0;i<100;i++)
        {
            osNewLayerList.AddNameValue(CPLSPrintf("LAYER_%03d_NAME", i),
                                        osLayerList[/*2 * */ i] + strlen("LAYER_00_NAME="));
            /*osNewLayerList.AddNameValue(CPLSPrintf("LAYER_%03d_INIT_STATE", i),
                                        osLayerList[i * 2 + 1] + strlen("LAYER_00_INIT_STATE="));*/
        }
        osLayerList = osNewLayerList;
    }

    char szFormatName[64];
    /* char szFormatInitState[64]; */
    sprintf(szFormatName, "LAYER_%%0%dd_NAME",  nNewIndex >= 100 ? 3 : 2);
    /* sprintf(szFormatInitState, "LAYER_%%0%dd_INIT_STATE", nNewIndex >= 100 ? 3 : 2); */

    osLayerList.AddNameValue(CPLSPrintf(szFormatName, nNewIndex),
                             pszLayerName);
    /*osLayerList.AddNameValue(CPLSPrintf(szFormatInitState, nNewIndex),
                             (ocg == NULL || ocg->getState() == OptionalContentGroup::On) ? "ON" : "OFF");*/
    oLayerOCGMap[pszLayerName] = ocg;

    //if (ocg != NULL && ocg->getState() == OptionalContentGroup::Off)
    //    bUseOCG = TRUE;
}

/************************************************************************/
/*                             ExploreLayers()                          */
/************************************************************************/

void PDFDataset::ExploreLayers(GDALPDFArray* poArray,
                               int nRecLevel,
                               CPLString osTopLayer)
{
    if( nRecLevel == 16 )
        return;

    int nLength = poArray->GetLength();
    CPLString osCurLayer;
    for(int i=0;i<nLength;i++)
    {
        GDALPDFObject* poObj = poArray->Get(i);
        if (i == 0 && poObj->GetType() == PDFObjectType_String)
        {
            CPLString osName = PDFSanitizeLayerName(poObj->GetString().c_str());
            if (osTopLayer.size())
                osTopLayer = osTopLayer + "." + osName;
            else
                osTopLayer = osName;
            AddLayer(osTopLayer.c_str(), NULL);
        }
        else if (poObj->GetType() == PDFObjectType_Array)
        {
            ExploreLayers(poObj->GetArray(), nRecLevel + 1, osCurLayer);
            osCurLayer = "";
        }
        else if (poObj->GetType() == PDFObjectType_Dictionary)
        {
            GDALPDFDictionary* poDict = poObj->GetDictionary();
            GDALPDFObject* poName = poDict->Get("Name");
            if (poName != NULL && poName->GetType() == PDFObjectType_String)
            {
                CPLString osName = PDFSanitizeLayerName(poName->GetString().c_str());
                if (osTopLayer.size())
                    osCurLayer = osTopLayer + "." + osName;
                else
                    osCurLayer = osName;
                //CPLDebug("PDF", "Layer %s", osCurLayer.c_str());

                OCGs* optContentConfig = poDocPoppler->getOptContentConfig();
                struct Ref r;
                r.num = poObj->GetRefNum();
                r.gen = poObj->GetRefGen();
                OptionalContentGroup* ocg = optContentConfig->findOcgByRef(r);
                if (ocg)
                {
                    AddLayer(osCurLayer.c_str(), ocg);
                    osLayerWithRefList.AddString(
                        CPLSPrintf("%s %d %d", osCurLayer.c_str(), r.num, r.gen));
                }
            }
        }
    }
}

/************************************************************************/
/*                              FindLayers()                            */
/************************************************************************/

void PDFDataset::FindLayers()
{
    OCGs* optContentConfig = poDocPoppler->getOptContentConfig();
    if (optContentConfig == NULL || !optContentConfig->isOk() )
        return;

    Array* array = optContentConfig->getOrderArray();
    if (array)
    {
        GDALPDFArray* poArray = GDALPDFCreateArray(array);
        ExploreLayers(poArray, 0);
        delete poArray;
    }
    else
    {
        GooList* ocgList = optContentConfig->getOCGs();
        for(int i=0;i<ocgList->getLength();i++)
        {
            OptionalContentGroup* ocg = (OptionalContentGroup*) ocgList->get(i);
            if( ocg != NULL && ocg->getName() != NULL )
                AddLayer((const char*)ocg->getName()->getCString(), ocg);
        }
    }

    oMDMD.SetMetadata(osLayerList.List(), "LAYERS");
}

/************************************************************************/
/*                            TurnLayersOnOff()                         */
/************************************************************************/

void PDFDataset::TurnLayersOnOff()
{
    OCGs* optContentConfig = poDocPoppler->getOptContentConfig();
    if (optContentConfig == NULL || !optContentConfig->isOk() )
        return;

    // Which layers to turn ON ?
    const char* pszLayers = GetOption(papszOpenOptions, "LAYERS", NULL);
    if (pszLayers)
    {
        int i;
        int bAll = EQUAL(pszLayers, "ALL");
        GooList* ocgList = optContentConfig->getOCGs();
        for(i=0;i<ocgList->getLength();i++)
        {
            OptionalContentGroup* ocg = (OptionalContentGroup*) ocgList->get(i);
            ocg->setState( (bAll) ? OptionalContentGroup::On : OptionalContentGroup::Off );
        }

        char** papszLayers = CSLTokenizeString2(pszLayers, ",", 0);
        for(i=0;!bAll && papszLayers[i] != NULL;i++)
        {
            std::map<CPLString, OptionalContentGroup*>::iterator oIter =
                oLayerOCGMap.find(papszLayers[i]);
            if (oIter != oLayerOCGMap.end())
            {
                if (oIter->second)
                {
                    //CPLDebug("PDF", "Turn '%s' on", papszLayers[i]);
                    oIter->second->setState(OptionalContentGroup::On);
                }

                // Turn child layers on, unless there's one of them explicitly listed
                // in the list.
                size_t nLen = strlen(papszLayers[i]);
                int bFoundChildLayer = FALSE;
                oIter = oLayerOCGMap.begin();
                for( ; oIter != oLayerOCGMap.end() && !bFoundChildLayer; oIter ++)
                {
                    if (oIter->first.size() > nLen &&
                        strncmp(oIter->first.c_str(), papszLayers[i], nLen) == 0 &&
                        oIter->first[nLen] == '.')
                    {
                        for(int j=0;papszLayers[j] != NULL;j++)
                        {
                            if (strcmp(papszLayers[j], oIter->first.c_str()) == 0)
                                bFoundChildLayer = TRUE;
                        }
                    }
                }

                if( !bFoundChildLayer )
                {
                    oIter = oLayerOCGMap.begin();
                    for( ; oIter != oLayerOCGMap.end() && !bFoundChildLayer; oIter ++)
                    {
                        if (oIter->first.size() > nLen &&
                            strncmp(oIter->first.c_str(), papszLayers[i], nLen) == 0 &&
                            oIter->first[nLen] == '.')
                        {
                            if (oIter->second)
                            {
                                //CPLDebug("PDF", "Turn '%s' on too", oIter->first.c_str());
                                oIter->second->setState(OptionalContentGroup::On);
                            }
                        }
                    }
                }

                // Turn parent layers on too
                char* pszLastDot;
                while( (pszLastDot = strrchr(papszLayers[i], '.')) != NULL)
                {
                    *pszLastDot = '\0';
                    oIter = oLayerOCGMap.find(papszLayers[i]);
                    if (oIter != oLayerOCGMap.end())
                    {
                        if (oIter->second)
                        {
                            //CPLDebug("PDF", "Turn '%s' on too", papszLayers[i]);
                            oIter->second->setState(OptionalContentGroup::On);
                        }
                    }
                }
            }
            else
            {
                CPLError(CE_Warning, CPLE_AppDefined, "Unknown layer '%s'",
                            papszLayers[i]);
            }
        }
        CSLDestroy(papszLayers);

        bUseOCG = TRUE;
    }

    // Which layers to turn OFF ?
    const char* pszLayersOFF = GetOption(papszOpenOptions, "LAYERS_OFF", NULL);
    if (pszLayersOFF)
    {
        char** papszLayersOFF = CSLTokenizeString2(pszLayersOFF, ",", 0);
        for(int i=0;papszLayersOFF[i] != NULL;i++)
        {
            std::map<CPLString, OptionalContentGroup*>::iterator oIter =
                oLayerOCGMap.find(papszLayersOFF[i]);
            if (oIter != oLayerOCGMap.end())
            {
                if (oIter->second)
                {
                    //CPLDebug("PDF", "Turn '%s' off", papszLayersOFF[i]);
                    oIter->second->setState(OptionalContentGroup::Off);
                }

                // Turn child layers off too
                size_t nLen = strlen(papszLayersOFF[i]);
                oIter = oLayerOCGMap.begin();
                for( ; oIter != oLayerOCGMap.end(); oIter ++)
                {
                    if (oIter->first.size() > nLen &&
                        strncmp(oIter->first.c_str(), papszLayersOFF[i], nLen) == 0 &&
                        oIter->first[nLen] == '.')
                    {
                        if (oIter->second)
                        {
                            //CPLDebug("PDF", "Turn '%s' off too", oIter->first.c_str());
                            oIter->second->setState(OptionalContentGroup::Off);
                        }
                    }
                }
            }
            else
            {
                CPLError(CE_Warning, CPLE_AppDefined, "Unknown layer '%s'",
                            papszLayersOFF[i]);
            }
        }
        CSLDestroy(papszLayersOFF);

        bUseOCG = TRUE;
    }
}

#endif

/************************************************************************/
/*                           FindLayerOCG()                             */
/************************************************************************/

CPLString PDFDataset::FindLayerOCG(GDALPDFDictionary* poPageDict,
                                   const char* pszLayerName)
{
    GDALPDFObject* poProperties =
        poPageDict->LookupObject("Resources.Properties");
    if (poProperties != NULL &&
        poProperties->GetType() == PDFObjectType_Dictionary)
    {
        std::map<CPLString, GDALPDFObject*>& oMap =
                                poProperties->GetDictionary()->GetValues();
        std::map<CPLString, GDALPDFObject*>::iterator oIter = oMap.begin();
        std::map<CPLString, GDALPDFObject*>::iterator oEnd = oMap.end();

        for(; oIter != oEnd; ++oIter)
        {
            GDALPDFObject* poObj = oIter->second;
            if( poObj->GetRefNum() != 0 && poObj->GetType() == PDFObjectType_Dictionary )
            {
                GDALPDFObject* poType = poObj->GetDictionary()->Get("Type");
                GDALPDFObject* poName = poObj->GetDictionary()->Get("Name");
                if( poType != NULL &&
                    poType->GetType() == PDFObjectType_Name &&
                    poType->GetName() == "OCG" &&
                    poName != NULL &&
                    poName->GetType() == PDFObjectType_String )
                {
                    if( strcmp(poName->GetString().c_str(), pszLayerName) ==  0)
                        return oIter->first;
                }
            }
        }
    }
    return "";
}

/************************************************************************/
/*                         FindLayersGeneric()                          */
/************************************************************************/

void PDFDataset::FindLayersGeneric(GDALPDFDictionary* poPageDict)
{
    GDALPDFObject* poProperties =
        poPageDict->LookupObject("Resources.Properties");
    if (poProperties != NULL &&
        poProperties->GetType() == PDFObjectType_Dictionary)
    {
        std::map<CPLString, GDALPDFObject*>& oMap =
                                poProperties->GetDictionary()->GetValues();
        std::map<CPLString, GDALPDFObject*>::iterator oIter = oMap.begin();
        std::map<CPLString, GDALPDFObject*>::iterator oEnd = oMap.end();

        for(; oIter != oEnd; ++oIter)
        {
            GDALPDFObject* poObj = oIter->second;
            if( poObj->GetRefNum() != 0 && poObj->GetType() == PDFObjectType_Dictionary )
            {
                GDALPDFObject* poType = poObj->GetDictionary()->Get("Type");
                GDALPDFObject* poName = poObj->GetDictionary()->Get("Name");
                if( poType != NULL &&
                    poType->GetType() == PDFObjectType_Name &&
                    poType->GetName() == "OCG" &&
                    poName != NULL &&
                    poName->GetType() == PDFObjectType_String )
                {
                    osLayerWithRefList.AddString(
                        CPLSPrintf("%s %d %d",
                                    PDFSanitizeLayerName(poName->GetString()).c_str(),
                                    poObj->GetRefNum(),
                                    poObj->GetRefGen()));
                }
            }
        }
    }
}

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

GDALDataset *PDFDataset::Open( GDALOpenInfo * poOpenInfo )

{
    if (!Identify(poOpenInfo))
        return NULL;

    const char* pszUserPwd = GetOption(poOpenInfo->papszOpenOptions, "USER_PWD", NULL);

    int bOpenSubdataset = strncmp(poOpenInfo->pszFilename, "PDF:", 4) == 0;
    int bOpenSubdatasetImage = strncmp(poOpenInfo->pszFilename, "PDF_IMAGE:", 10) == 0;
    int iPage = -1;
    int nImageNum = -1;
    const char* pszFilename = poOpenInfo->pszFilename;
    char szPassword[81];

    if (bOpenSubdataset)
    {
        iPage = atoi(pszFilename + 4);
        if (iPage <= 0)
            return NULL;
         pszFilename = strchr(pszFilename + 4, ':');
        if (pszFilename == NULL)
            return NULL;
        pszFilename ++;
    }
    else if (bOpenSubdatasetImage)
    {
        iPage = atoi(pszFilename + 10);
        if (iPage <= 0)
            return NULL;
        const char* pszNext = strchr(pszFilename + 10, ':');
        if (pszNext == NULL)
            return NULL;
        nImageNum = atoi(pszNext + 1);
        if (nImageNum <= 0)
            return NULL;
        pszFilename = strchr(pszNext + 1, ':');
        if (pszFilename == NULL)
            return NULL;
        pszFilename ++;
    }
    else
        iPage = 1;

    int bUsePoppler;
#if defined(HAVE_POPPLER) && !defined(HAVE_PODOFO)
    bUsePoppler = TRUE;
#elif !defined(HAVE_POPPLER) && defined(HAVE_PODOFO)
    bUsePoppler = FALSE;
#elif defined(HAVE_POPPLER) && defined(HAVE_PODOFO)
    const char* pszPDFLib = GetOption(poOpenInfo->papszOpenOptions, "PDF_LIB", "POPPLER");
    if (EQUAL(pszPDFLib, "POPPLER"))
        bUsePoppler = TRUE;
    else if (EQUAL(pszPDFLib, "PODOFO"))
        bUsePoppler = FALSE;
    else
    {
        CPLDebug("PDF", "Invalid value for GDAL_PDF_LIB config option");
        bUsePoppler = TRUE;
    }
#else
    return NULL;
#endif

    GDALPDFObject* poPageObj = NULL;
#ifdef HAVE_POPPLER
    PDFDoc* poDocPoppler = NULL;
    ObjectAutoFree oObj;
    Page* poPagePoppler = NULL;
    Catalog* poCatalogPoppler = NULL;
#endif
#ifdef HAVE_PODOFO
    PoDoFo::PdfMemDocument* poDocPodofo = NULL;
    PoDoFo::PdfPage* poPagePodofo = NULL;
#endif
    int nPages = 0;

#ifdef HAVE_POPPLER
  if(bUsePoppler)
  {
    GooString* poUserPwd = NULL;

    /* Set custom error handler for poppler errors */
#ifdef POPPLER_0_20_OR_LATER
    setErrorCallback(PDFDatasetErrorFunction, NULL);
#else
    setErrorFunction(PDFDatasetErrorFunction);
#endif

    {
        CPLMutexHolderD(&hGlobalParamsMutex);
        /* poppler global variable */
        if (globalParams == NULL)
            globalParams = new GlobalParams();

        globalParams->setPrintCommands(CSLTestBoolean(
            CPLGetConfigOption("GDAL_PDF_PRINT_COMMANDS", "FALSE")));
    }

    while(TRUE)
    {
        VSILFILE* fp = VSIFOpenL(pszFilename, "rb");
        if (fp == NULL)
            return NULL;

        fp = (VSILFILE*)VSICreateBufferedReaderHandle((VSIVirtualHandle*)fp);

        if (pszUserPwd)
            poUserPwd = new GooString(pszUserPwd);

        oObj.initNull();
        poDocPoppler = new PDFDoc(new VSIPDFFileStream(fp, pszFilename, &oObj), NULL, poUserPwd);
        delete poUserPwd;

        if ( !poDocPoppler->isOk() || poDocPoppler->getNumPages() == 0 )
        {
            if (poDocPoppler->getErrorCode() == errEncrypted)
            {
                if (pszUserPwd && EQUAL(pszUserPwd, "ASK_INTERACTIVE"))
                {
                    printf( "Enter password (will be echo'ed in the console): " );
                    if (0 == fgets( szPassword, sizeof(szPassword), stdin ))
                    {
                      fprintf(stderr, "WARNING: Error getting password.\n");
                    }
                    szPassword[sizeof(szPassword)-1] = 0;
                    char* sz10 = strchr(szPassword, '\n');
                    if (sz10)
                        *sz10 = 0;
                    pszUserPwd = szPassword;
                    PDFFreeDoc(poDocPoppler);
                    continue;
                }
                else if (pszUserPwd == NULL)
                {
                    CPLError(CE_Failure, CPLE_AppDefined,
                             "A password is needed. You can specify it through the PDF_USER_PWD "
                             "configuration option (that can be set to ASK_INTERACTIVE)");
                }
                else
                {
                    CPLError(CE_Failure, CPLE_AppDefined, "Invalid password");
                }
            }
            else
            {
                CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF");
            }

            PDFFreeDoc(poDocPoppler);

            return NULL;
        }
        else
            break;

        /* Reset errors that could have been issued during opening and that */
        /* did not result in an invalid document */
        CPLErrorReset();
    }

    poCatalogPoppler = poDocPoppler->getCatalog();
    if ( poCatalogPoppler == NULL || !poCatalogPoppler->isOk() )
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF : invalid catalog");
        PDFFreeDoc(poDocPoppler);
        return NULL;
    }

#ifdef dump_catalog
    {
        ObjectAutoFree oCatalog;
        poDocPoppler->getXRef()->getCatalog(&oCatalog);
        GDALPDFObjectPoppler oCatalogGDAL(&oCatalog, FALSE);
        GDALPDFDumper oDumper(stderr);
        oDumper.Dump(&oCatalogGDAL);
    }
#endif

    nPages = poDocPoppler->getNumPages();
    if (iPage < 1 || iPage > nPages)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid page number (%d/%d)",
                 iPage, nPages);
        PDFFreeDoc(poDocPoppler);
        return NULL;
    }

    /* Sanity check to validate page count */
    if( iPage != nPages )
    {
        poPagePoppler = poCatalogPoppler->getPage(nPages);
        if ( poPagePoppler == NULL || !poPagePoppler->isOk() )
        {
            CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF : invalid page count");
            PDFFreeDoc(poDocPoppler);
            return NULL;
        }
    }

    poPagePoppler = poCatalogPoppler->getPage(iPage);
    if ( poPagePoppler == NULL || !poPagePoppler->isOk() )
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF : invalid page");
        PDFFreeDoc(poDocPoppler);
        return NULL;
    }

    /* Here's the dirty part: this is a private member */
    /* so we had to #define private public to get it ! */
    Object& oPageObj = poPagePoppler->pageObj;
    if ( !oPageObj.isDict() )
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF : !oPageObj.isDict()");
        PDFFreeDoc(poDocPoppler);
        return NULL;
    }

    poPageObj = new GDALPDFObjectPoppler(&oPageObj, FALSE);
    Ref* poPageRef = poCatalogPoppler->getPageRef(iPage);
    if (poPageRef != NULL)
    {
        ((GDALPDFObjectPoppler*)poPageObj)->SetRefNumAndGen(poPageRef->num, poPageRef->gen);
    }
  }
#endif

#ifdef HAVE_PODOFO
  if (!bUsePoppler)
  {
    PoDoFo::PdfError::EnableDebug( false );
    PoDoFo::PdfError::EnableLogging( false );

    poDocPodofo = new PoDoFo::PdfMemDocument();
    try
    {
        poDocPodofo->Load(pszFilename);
    }
    catch(PoDoFo::PdfError& oError)
    {
        if (oError.GetError() == PoDoFo::ePdfError_InvalidPassword)
        {
            if (pszUserPwd)
            {
                if (EQUAL(pszUserPwd, "ASK_INTERACTIVE"))
                {
                    printf( "Enter password (will be echo'ed in the console): " );
                    if (0 == fgets( szPassword, sizeof(szPassword), stdin ))
                    {
                      fprintf(stderr, "WARNING: Error getting password.\n");
                    }
                    szPassword[sizeof(szPassword)-1] = 0;
                    char* sz10 = strchr(szPassword, '\n');
                    if (sz10)
                        *sz10 = 0;
                    pszUserPwd = szPassword;
                }

                try
                {
                    poDocPodofo->SetPassword(pszUserPwd);
                }
                catch(PoDoFo::PdfError& oError)
                {
                    if (oError.GetError() == PoDoFo::ePdfError_InvalidPassword)
                    {
                        CPLError(CE_Failure, CPLE_AppDefined, "Invalid password");
                    }
                    else
                    {
                        CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF : %s", oError.what());
                    }
                    delete poDocPodofo;
                    return NULL;
                }
                catch(...)
                {
                    CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF");
                    delete poDocPodofo;
                    return NULL;
                }
            }
            else
            {
                CPLError(CE_Failure, CPLE_AppDefined,
                            "A password is needed. You can specify it through the PDF_USER_PWD "
                            "configuration option (that can be set to ASK_INTERACTIVE)");
                delete poDocPodofo;
                return NULL;
            }
        }
        else
        {
            CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF : %s", oError.what());
            delete poDocPodofo;
            return NULL;
        }
    }
    catch(...)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF");
        delete poDocPodofo;
        return NULL;
    }

    nPages = poDocPodofo->GetPageCount();
    if (iPage < 1 || iPage > nPages)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid page number (%d/%d)",
                 iPage, nPages);
        delete poDocPodofo;
        return NULL;
    }

    try
    {
        /* Sanity check to validate page count */
        if( iPage != nPages )
            poPagePodofo = poDocPodofo->GetPage(nPages - 1);

        poPagePodofo = poDocPodofo->GetPage(iPage - 1);
    }
    catch(PoDoFo::PdfError& oError)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF : %s", oError.what());
        delete poDocPodofo;
        return NULL;
    }
    catch(...)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF");
        delete poDocPodofo;
        return NULL;
    }

    if ( poPagePodofo == NULL )
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF : invalid page");
        delete poDocPodofo;
        return NULL;
    }

    PoDoFo::PdfObject* pObj = poPagePodofo->GetObject();
    poPageObj = new GDALPDFObjectPodofo(pObj, poDocPodofo->GetObjects());
  }
#endif

    GDALPDFDictionary* poPageDict = poPageObj->GetDictionary();
    if ( poPageDict == NULL )
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Invalid PDF : poPageDict == NULL");
#ifdef HAVE_POPPLER
        PDFFreeDoc(poDocPoppler);
#endif
#ifdef HAVE_PODOFO
        delete poDocPodofo;
#endif
        return NULL;
    }

    const char* pszDumpObject = CPLGetConfigOption("PDF_DUMP_OBJECT", NULL);
    if (pszDumpObject != NULL)
    {
        FILE* f;
        if (strcmp(pszDumpObject, "stderr") == 0)
            f = stderr;
        else if (EQUAL(pszDumpObject, "YES"))
            f = fopen(CPLSPrintf("dump_%s.txt", CPLGetFilename(pszFilename)), "wt");
        else
            f = fopen(pszDumpObject, "wt");
        if (f == NULL)
            f = stderr;

        GDALPDFDumper oDumper(f);
        oDumper.Dump(poPageObj);
        if (f != stderr)
            fclose(f);
    }

    PDFDataset* poDS = new PDFDataset();
    poDS->papszOpenOptions = CSLDuplicate(poOpenInfo->papszOpenOptions);
    poDS->bUsePoppler = bUsePoppler;
    poDS->osFilename = pszFilename;
    poDS->eAccess = poOpenInfo->eAccess;

    if ( nPages > 1 && !bOpenSubdataset )
    {
        int i;
        CPLStringList aosList;
        for(i=0;i<nPages;i++)
        {
            char szKey[32];
            sprintf( szKey, "SUBDATASET_%d_NAME", i+1 );
            aosList.AddNameValue(szKey, CPLSPrintf("PDF:%d:%s", i+1, poOpenInfo->pszFilename));
            sprintf( szKey, "SUBDATASET_%d_DESC", i+1 );
            aosList.AddNameValue(szKey, CPLSPrintf("Page %d of %s", i+1, poOpenInfo->pszFilename));
        }
        poDS->SetMetadata( aosList.List(), "SUBDATASETS" );
    }

#ifdef HAVE_POPPLER
    poDS->poDocPoppler = poDocPoppler;
#endif
#ifdef HAVE_PODOFO
    poDS->poDocPodofo = poDocPodofo;
#endif
    poDS->poPageObj = poPageObj;
    poDS->osUserPwd = pszUserPwd ? pszUserPwd : "";
    poDS->iPage = iPage;

    int nBandsGuessed = 0;
    if (nImageNum < 0)
    {
        poDS->GuessDPI(poPageDict, &nBandsGuessed);
        if( nBandsGuessed < 4 )
            nBandsGuessed = 0;
    }
    else
    {
        const char* pszDPI = GetOption(poOpenInfo->papszOpenOptions, "DPI", NULL);
        if (pszDPI != NULL)
        {
            poDS->dfDPI = CPLAtof(pszDPI);
        }
    }

    double dfX1 = 0.0, dfY1 = 0.0, dfX2 = 0.0, dfY2 = 0.0;

#ifdef HAVE_POPPLER
    if (bUsePoppler)
    {
        PDFRectangle* psMediaBox = poPagePoppler->getMediaBox();
        dfX1 = psMediaBox->x1;
        dfY1 = psMediaBox->y1;
        dfX2 = psMediaBox->x2;
        dfY2 = psMediaBox->y2;
    }
#endif

#ifdef HAVE_PODOFO
    if (!bUsePoppler)
    {
        PoDoFo::PdfRect oMediaBox = poPagePodofo->GetMediaBox();
        dfX1 = oMediaBox.GetLeft();
        dfY1 = oMediaBox.GetBottom();
        dfX2 = dfX1 + oMediaBox.GetWidth();
        dfY2 = dfY1 + oMediaBox.GetHeight();
    }
#endif

    double dfUserUnit = poDS->dfDPI * USER_UNIT_IN_INCH;
    poDS->dfPageWidth = dfX2 - dfX1;
    poDS->dfPageHeight = dfY2 - dfY1;
    poDS->nRasterXSize = (int) floor((dfX2 - dfX1) * dfUserUnit+0.5);
    poDS->nRasterYSize = (int) floor((dfY2 - dfY1) * dfUserUnit+0.5);

    if( !GDALCheckDatasetDimensions(poDS->nRasterXSize, poDS->nRasterYSize) )
    {
        delete poDS;
        return NULL;
    }

    double dfRotation = 0;
#ifdef HAVE_POPPLER
    if (bUsePoppler)
        dfRotation = poDocPoppler->getPageRotate(iPage);
#endif

#ifdef HAVE_PODOFO
    if (!bUsePoppler)
        dfRotation = poPagePodofo->GetRotation();
#endif
    if ( dfRotation == 90 ||
         dfRotation == -90 ||
         dfRotation == 270 )
    {
/* FIXME: the non poppler case should be implemented. This needs to rotate */
/* the output of pdftoppm */
#ifdef HAVE_POPPLER
      if (bUsePoppler)
      {
        /* Wondering how it would work with a georeferenced image */
        /* Has only been tested with ungeoreferenced image */
        int nTmp = poDS->nRasterXSize;
        poDS->nRasterXSize = poDS->nRasterYSize;
        poDS->nRasterYSize = nTmp;
      }
#endif
    }

    /* Check if the PDF is only made of regularly tiled images */
    /* (like some USGS GeoPDF production) */
    if( dfRotation == 0.0 && poDS->asTiles.size() &&
        EQUAL(GetOption(poOpenInfo->papszOpenOptions, "LAYERS", "ALL"), "ALL") )
    {
        poDS->CheckTiledRaster();
        if (poDS->aiTiles.size() )
            poDS->SetMetadataItem( "INTERLEAVE", "PIXEL", "IMAGE_STRUCTURE" );
    }

    GDALPDFObject* poLGIDict = NULL;
    GDALPDFObject* poVP = NULL;
    int bIsOGCBP = FALSE;
    if ( (poLGIDict = poPageDict->Get("LGIDict")) != NULL && nImageNum < 0 )
    {
        /* Cf 08-139r3_GeoPDF_Encoding_Best_Practice_Version_2.2.pdf */
        CPLDebug("PDF", "OGC Encoding Best Practice style detected");
        if (poDS->ParseLGIDictObject(poLGIDict))
        {
            if (poDS->bHasCTM )
            {
                if ( dfRotation == 90 )
                {
                    poDS->adfGeoTransform[0] = poDS->adfCTM[4];
                    poDS->adfGeoTransform[1] = poDS->adfCTM[2] / dfUserUnit;
                    poDS->adfGeoTransform[2] = poDS->adfCTM[0] / dfUserUnit;
                    poDS->adfGeoTransform[3] = poDS->adfCTM[5];
                    poDS->adfGeoTransform[4] = poDS->adfCTM[3] / dfUserUnit;
                    poDS->adfGeoTransform[5] = poDS->adfCTM[1] / dfUserUnit;
                }
                else if ( dfRotation == -90 || dfRotation == 270 )
                {
                    poDS->adfGeoTransform[0] = poDS->adfCTM[4] + poDS->adfCTM[2] * poDS->dfPageHeight + poDS->adfCTM[0] * poDS->dfPageWidth;
                    poDS->adfGeoTransform[1] = -poDS->adfCTM[2] / dfUserUnit;
                    poDS->adfGeoTransform[2] = -poDS->adfCTM[0] / dfUserUnit;
                    poDS->adfGeoTransform[3] = poDS->adfCTM[5] + poDS->adfCTM[3] * poDS->dfPageHeight + poDS->adfCTM[1] * poDS->dfPageWidth;
                    poDS->adfGeoTransform[4] = -poDS->adfCTM[3] / dfUserUnit;
                    poDS->adfGeoTransform[5] = -poDS->adfCTM[1] / dfUserUnit;
                }
                else
                {
                    poDS->adfGeoTransform[0] = poDS->adfCTM[4] + poDS->adfCTM[2] * poDS->dfPageHeight;
                    poDS->adfGeoTransform[1] = poDS->adfCTM[0] / dfUserUnit;
                    poDS->adfGeoTransform[2] = - poDS->adfCTM[2] / dfUserUnit;
                    poDS->adfGeoTransform[3] = poDS->adfCTM[5] + poDS->adfCTM[3] * poDS->dfPageHeight;
                    poDS->adfGeoTransform[4] = poDS->adfCTM[1] / dfUserUnit;
                    poDS->adfGeoTransform[5] = - poDS->adfCTM[3] / dfUserUnit;
                }
                
                poDS->bGeoTransformValid = TRUE;
            }

            bIsOGCBP = TRUE;

            int i;
            for(i=0;i<poDS->nGCPCount;i++)
            {
                if ( dfRotation == 90 )
                {
                    double dfPixel = poDS->pasGCPList[i].dfGCPPixel * dfUserUnit;
                    double dfLine = poDS->pasGCPList[i].dfGCPLine * dfUserUnit;
                    poDS->pasGCPList[i].dfGCPPixel = dfLine;
                    poDS->pasGCPList[i].dfGCPLine = dfPixel;
                }
                else if ( dfRotation == -90 || dfRotation == 270 )
                {
                    double dfPixel = poDS->pasGCPList[i].dfGCPPixel * dfUserUnit;
                    double dfLine = poDS->pasGCPList[i].dfGCPLine * dfUserUnit;
                    poDS->pasGCPList[i].dfGCPPixel = poDS->nRasterXSize - dfLine;
                    poDS->pasGCPList[i].dfGCPLine = poDS->nRasterYSize - dfPixel;
                }
                else
                {
                    poDS->pasGCPList[i].dfGCPPixel = poDS->pasGCPList[i].dfGCPPixel * dfUserUnit;
                    poDS->pasGCPList[i].dfGCPLine = poDS->nRasterYSize - poDS->pasGCPList[i].dfGCPLine * dfUserUnit;
                }
            }
        }
    }
    else if ( (poVP = poPageDict->Get("VP")) != NULL && nImageNum < 0 )
    {
        /* Cf adobe_supplement_iso32000.pdf */
        CPLDebug("PDF", "Adobe ISO32000 style Geospatial PDF perhaps ?");
        if (dfX1 != 0 || dfY1 != 0)
        {
            CPLDebug("PDF", "non null dfX1 or dfY1 values. untested case...");
        }
        poDS->ParseVP(poVP, dfX2 - dfX1, dfY2 - dfY1);
    }
    else
    {
        GDALPDFObject* poXObject = poPageDict->LookupObject("Resources.XObject");

        if (poXObject != NULL &&
            poXObject->GetType() == PDFObjectType_Dictionary)
        {
            GDALPDFDictionary* poXObjectDict = poXObject->GetDictionary();
            std::map<CPLString, GDALPDFObject*>& oMap = poXObjectDict->GetValues();
            std::map<CPLString, GDALPDFObject*>::iterator oMapIter = oMap.begin();
            int nSubDataset = 0;
            while(oMapIter != oMap.end())
            {
                GDALPDFObject* poObj = oMapIter->second;
                if (poObj->GetType() == PDFObjectType_Dictionary)
                {
                    GDALPDFDictionary* poDict = poObj->GetDictionary();
                    GDALPDFObject* poSubtype = NULL;
                    GDALPDFObject* poMeasure = NULL;
                    GDALPDFObject* poWidth = NULL;
                    GDALPDFObject* poHeight = NULL;
                    int nW = 0;
                    int nH = 0;
                    if ((poSubtype = poDict->Get("Subtype")) != NULL &&
                        poSubtype->GetType() == PDFObjectType_Name &&
                        poSubtype->GetName() == "Image" &&
                        (poMeasure = poDict->Get("Measure")) != NULL &&
                        poMeasure->GetType() == PDFObjectType_Dictionary &&
                        (poWidth = poDict->Get("Width")) != NULL &&
                        poWidth->GetType() == PDFObjectType_Int &&
                        (nW = poWidth->GetInt()) > 0 &&
                        (poHeight = poDict->Get("Height")) != NULL &&
                        poHeight->GetType() == PDFObjectType_Int &&
                        (nH = poHeight->GetInt()) > 0 )
                    {
                        if (nImageNum < 0)
                            CPLDebug("PDF", "Measure found on Image object (%d)",
                                     poObj->GetRefNum());

                        GDALPDFObject* poColorSpace = poDict->Get("ColorSpace");
                        GDALPDFObject* poBitsPerComponent = poDict->Get("BitsPerComponent");
                        if (poObj->GetRefNum() != 0 &&
                            poObj->GetRefGen() == 0 &&
                            poColorSpace != NULL &&
                            poColorSpace->GetType() == PDFObjectType_Name &&
                            (poColorSpace->GetName() == "DeviceGray" ||
                             poColorSpace->GetName() == "DeviceRGB") &&
                            (poBitsPerComponent == NULL ||
                             (poBitsPerComponent->GetType() == PDFObjectType_Int &&
                              poBitsPerComponent->GetInt() == 8)))
                        {
                            if (nImageNum < 0)
                            {
                                nSubDataset ++;
                                poDS->SetMetadataItem(CPLSPrintf("SUBDATASET_%d_NAME",
                                                                 nSubDataset),
                                                      CPLSPrintf("PDF_IMAGE:%d:%d:%s",
                                                                 iPage, poObj->GetRefNum(), pszFilename),
                                                      "SUBDATASETS");
                                poDS->SetMetadataItem(CPLSPrintf("SUBDATASET_%d_DESC",
                                                                 nSubDataset),
                                                      CPLSPrintf("Georeferenced image of size %dx%d of page %d of %s",
                                                                 nW, nH, iPage, pszFilename),
                                                      "SUBDATASETS");
                            }
                            else if (poObj->GetRefNum() == nImageNum)
                            {
                                poDS->nRasterXSize = nW;
                                poDS->nRasterYSize = nH;
                                poDS->ParseMeasure(poMeasure, nW, nH, 0, nH, nW, 0);
                                poDS->poImageObj = poObj;
                                if (poColorSpace->GetName() == "DeviceGray")
                                    nBandsGuessed = 1;
                                break;
                            }
                        }
                    }
                }
                ++ oMapIter;
            }
        }

        if (nImageNum >= 0 && poDS->poImageObj == NULL)
        {
            CPLError(CE_Failure, CPLE_AppDefined, "Cannot find image %d", nImageNum);
            delete poDS;
            return NULL;
        }

        /* Not a geospatial PDF doc */
    }

    /* If pixel size or top left coordinates are very close to an int, round them to the int */
    double dfEps = ( fabs(poDS->adfGeoTransform[0]) > 1e5 &&
                     fabs(poDS->adfGeoTransform[3]) > 1e5 ) ? 1e-5 : 1e-8;
    poDS->adfGeoTransform[0] = ROUND_TO_INT_IF_CLOSE(poDS->adfGeoTransform[0], dfEps);
    poDS->adfGeoTransform[1] = ROUND_TO_INT_IF_CLOSE(poDS->adfGeoTransform[1]);
    poDS->adfGeoTransform[3] = ROUND_TO_INT_IF_CLOSE(poDS->adfGeoTransform[3], dfEps);
    poDS->adfGeoTransform[5] = ROUND_TO_INT_IF_CLOSE(poDS->adfGeoTransform[5]);

    if (poDS->poNeatLine)
    {
        char* pszNeatLineWkt = NULL;
        OGRLinearRing* poRing = poDS->poNeatLine->getExteriorRing();
        /* Adobe style is already in target SRS units */
        if (bIsOGCBP)
        {
            int nPoints = poRing->getNumPoints();
            int i;

            for(i=0;i<nPoints;i++)
            {
                double x, y;
                if( dfRotation == 90.0 )
                {
                    x = poRing->getY(i) * dfUserUnit;
                    y = poRing->getX(i) * dfUserUnit;
                }
                else if( dfRotation == -90.0 || dfRotation == 270.0 )
                {
                    x = poDS->nRasterXSize - poRing->getY(i) * dfUserUnit;
                    y = poDS->nRasterYSize - poRing->getX(i) * dfUserUnit;
                }
                else
                {
                    x = poRing->getX(i) * dfUserUnit;
                    y = poDS->nRasterYSize - poRing->getY(i) * dfUserUnit;
                }
                double X = poDS->adfGeoTransform[0] + x * poDS->adfGeoTransform[1] +
                                                      y * poDS->adfGeoTransform[2];
                double Y = poDS->adfGeoTransform[3] + x * poDS->adfGeoTransform[4] +
                                                      y * poDS->adfGeoTransform[5];
                poRing->setPoint(i, X, Y);
            }
        }
        poRing->closeRings();

        poDS->poNeatLine->exportToWkt(&pszNeatLineWkt);
        if (nImageNum < 0)
            poDS->SetMetadataItem("NEATLINE", pszNeatLineWkt);
        CPLFree(pszNeatLineWkt);
    }


#ifdef HAVE_POPPLER
  if (bUsePoppler)
  {
    GooString* poMetadata = poCatalogPoppler->readMetadata();
    if (poMetadata)
    {
        char* pszContent = poMetadata->getCString();
        if (pszContent != NULL &&
            strncmp(pszContent, "<?xpacket begin=", strlen("<?xpacket begin=")) == 0)
        {
            char *apszMDList[2];
            apszMDList[0] = pszContent;
            apszMDList[1] = NULL;
            poDS->SetMetadata(apszMDList, "xml:XMP");
        }
        delete poMetadata;
    }

    /* Read Info object */
    /* The test is necessary since with some corrupted PDFs poDocPoppler->getDocInfo() */
    /* might abort() */
    if( poDocPoppler->getXRef()->isOk() )
    {
        Object oInfo;
        poDocPoppler->getDocInfo(&oInfo);
        GDALPDFObjectPoppler oInfoObjPoppler(&oInfo, FALSE);
        poDS->ParseInfo(&oInfoObjPoppler);
        oInfo.free();
    }

    /* Find layers */
    poDS->FindLayers();

    /* Turn user specified layers on or off */
    poDS->TurnLayersOnOff();
  }
#endif

#ifdef HAVE_PODOFO
  if (!bUsePoppler)
  {
    PoDoFo::TIVecObjects it = poDocPodofo->GetObjects().begin();
    for( ; it != poDocPodofo->GetObjects().end(); ++it )
    {
        GDALPDFObjectPodofo oObjPodofo((*it), poDocPodofo->GetObjects());
        poDS->FindXMP(&oObjPodofo);
    }

    /* Find layers */
    poDS->FindLayersGeneric(poPageDict);

    /* Read Info object */
    PoDoFo::PdfInfo* poInfo = poDocPodofo->GetInfo();
    if (poInfo != NULL)
    {
        GDALPDFObjectPodofo oInfoObjPodofo(poInfo->GetObject(), poDocPodofo->GetObjects());
        poDS->ParseInfo(&oInfoObjPodofo);
    }
  }
#endif

    int nBands = 3;
    if( nBandsGuessed )
        nBands = nBandsGuessed;
    const char* pszPDFBands = GetOption(poOpenInfo->papszOpenOptions, "BANDS", NULL);
    if( pszPDFBands )
    {
        nBands = atoi(pszPDFBands);
        if (nBands != 3 && nBands != 4)
        {
            CPLError(CE_Warning, CPLE_NotSupported,
                    "Invalid value for GDAL_PDF_BANDS. Using 3 as a fallback");
            nBands = 3;
        }
    }
#ifdef HAVE_PODOFO
    if (!bUsePoppler && nBands == 4 && poDS->aiTiles.size() == 0)
    {
        CPLError(CE_Warning, CPLE_NotSupported,
                 "GDAL_PDF_BANDS=4 only supported when PDF driver is compiled against Poppler. "
                 "Using 3 as a fallback");
        nBands = 3;
    }
#endif

    int iBand;
    for(iBand = 1; iBand <= nBands; iBand ++)
    {
        if (poDS->poImageObj != NULL)
            poDS->SetBand(iBand, new PDFImageRasterBand(poDS, iBand));
        else
            poDS->SetBand(iBand, new PDFRasterBand(poDS, iBand));
    }

    int bHasNonEmptyVectorLayers = poDS->OpenVectorLayers(poPageDict);

    /* Check if this is a raster-only PCIDSK file and that we are */
    /* opened in vector-only mode */
    if( (poOpenInfo->nOpenFlags & GDAL_OF_RASTER) == 0 &&
        (poOpenInfo->nOpenFlags & GDAL_OF_VECTOR) != 0 &&
        !bHasNonEmptyVectorLayers )
    {
        CPLDebug("PCIDSK", "This is a raster-only PDF dataset, "
                    "but it has been opened in vector-only mode");
        delete poDS;
        return NULL;
    }

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

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

    /* Clear dirty flag */
    poDS->bProjDirty = FALSE;
    poDS->bNeatLineDirty = FALSE;
    poDS->bInfoDirty = FALSE;
    poDS->bXMPDirty = FALSE;

    return( poDS );
}

/************************************************************************/
/*                       ParseLGIDictObject()                           */
/************************************************************************/

int PDFDataset::ParseLGIDictObject(GDALPDFObject* poLGIDict)
{
    int i;
    int bOK = FALSE;
    if (poLGIDict->GetType() == PDFObjectType_Array)
    {
        GDALPDFArray* poArray = poLGIDict->GetArray();
        int nArrayLength = poArray->GetLength();
        int iMax = -1;
        GDALPDFObject* poArrayElt;
        for (i=0; i<nArrayLength; i++)
        {
            if ( (poArrayElt = poArray->Get(i)) == NULL ||
                 poArrayElt->GetType() != PDFObjectType_Dictionary )
            {
                CPLError(CE_Failure, CPLE_AppDefined,
                         "LGIDict[%d] is not a dictionary", i);
                return FALSE;
            }

            int bIsBestCandidate = FALSE;
            if (ParseLGIDictDictFirstPass(poArrayElt->GetDictionary(), &bIsBestCandidate))
            {
                if (bIsBestCandidate || iMax < 0)
                    iMax = i;
            }
        }

        if (iMax < 0)
            return FALSE;

        poArrayElt = poArray->Get(iMax);
        bOK = ParseLGIDictDictSecondPass(poArrayElt->GetDictionary());
    }
    else if (poLGIDict->GetType() == PDFObjectType_Dictionary)
    {
        bOK = ParseLGIDictDictFirstPass(poLGIDict->GetDictionary()) &&
              ParseLGIDictDictSecondPass(poLGIDict->GetDictionary());
    }
    else
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "LGIDict is of type %s", poLGIDict->GetTypeName());
    }

    return bOK;
}

/************************************************************************/
/*                            Get()                                     */
/************************************************************************/

static double Get(GDALPDFObject* poObj, int nIndice)
{
    if (poObj->GetType() == PDFObjectType_Array && nIndice >= 0)
    {
        poObj = poObj->GetArray()->Get(nIndice);
        if (poObj == NULL)
            return 0;
        return Get(poObj);
    }
    else if (poObj->GetType() == PDFObjectType_Int)
        return poObj->GetInt();
    else if (poObj->GetType() == PDFObjectType_Real)
        return poObj->GetReal();
    else if (poObj->GetType() == PDFObjectType_String)
    {
        const char* pszStr = poObj->GetString().c_str();
        size_t nLen = strlen(pszStr);
        /* cf Military_Installations_2008.pdf that has values like "96 0 0.0W" */
        char chLast = pszStr[nLen-1];
        if (chLast == 'W' || chLast == 'E' || chLast == 'N' || chLast == 'S')
        {
            double dfDeg = CPLAtof(pszStr);
            double dfMin = 0, dfSec = 0;
            const char* pszNext = strchr(pszStr, ' ');
            if (pszNext)
                pszNext ++;
            if (pszNext)
                dfMin = CPLAtof(pszNext);
            if (pszNext)
                pszNext = strchr(pszNext, ' ');
            if (pszNext)
                pszNext ++;
            if (pszNext)
                dfSec = CPLAtof(pszNext);
            double dfVal = dfDeg + dfMin / 60 + dfSec / 3600;
            if (chLast == 'W' || chLast == 'S')
                return -dfVal;
            else
                return dfVal;
        }
        return CPLAtof(pszStr);
    }
    else
    {
        CPLError(CE_Warning, CPLE_AppDefined, "Unexpected type : %s",
                 poObj->GetTypeName());
        return 0;
    }
}

/************************************************************************/
/*                            Get()                                */
/************************************************************************/

static double Get(GDALPDFDictionary* poDict, const char* pszName)
{
    GDALPDFObject* poObj;
    if ( (poObj = poDict->Get(pszName)) != NULL )
        return Get(poObj);
    CPLError(CE_Failure, CPLE_AppDefined,
             "Cannot find parameter %s", pszName);
    return 0;
}

/************************************************************************/
/*                   ParseLGIDictDictFirstPass()                        */
/************************************************************************/

int PDFDataset::ParseLGIDictDictFirstPass(GDALPDFDictionary* poLGIDict,
                                          int* pbIsBestCandidate)
{
    int i;

    if (pbIsBestCandidate)
        *pbIsBestCandidate = FALSE;

    if (poLGIDict == NULL)
        return FALSE;

/* -------------------------------------------------------------------- */
/*      Extract Type attribute                                          */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poType;
    if ((poType = poLGIDict->Get("Type")) == NULL)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot find Type of LGIDict object");
        return FALSE;
    }

    if ( poType->GetType() != PDFObjectType_Name )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Invalid type for Type of LGIDict object");
        return FALSE;
    }

    if ( strcmp(poType->GetName().c_str(), "LGIDict") != 0 )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Invalid value for Type of LGIDict object : %s",
                 poType->GetName().c_str());
        return FALSE;
    }

/* -------------------------------------------------------------------- */
/*      Extract Version attribute                                       */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poVersion;
    if ((poVersion = poLGIDict->Get("Version")) == NULL)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot find Version of LGIDict object");
        return FALSE;
    }

    if ( poVersion->GetType() == PDFObjectType_String )
    {
        /* OGC best practice is 2.1 */
        CPLDebug("PDF", "LGIDict Version : %s",
                 poVersion->GetString().c_str());
    }
    else if (poVersion->GetType() == PDFObjectType_Int)
    {
        /* Old TerraGo is 2 */
        CPLDebug("PDF", "LGIDict Version : %d",
                 poVersion->GetInt());
    }

    /* USGS PDF maps have several LGIDict. Keep the one whose description */
    /* is "Map Layers" by default */
    const char* pszNeatlineToSelect =
        GetOption(papszOpenOptions, "NEATLINE", "Map Layers");

/* -------------------------------------------------------------------- */
/*      Extract Neatline attribute                                      */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poNeatline;
    if ((poNeatline = poLGIDict->Get("Neatline")) != NULL &&
        poNeatline->GetType() == PDFObjectType_Array)
    {
        int nLength = poNeatline->GetArray()->GetLength();
        if ( (nLength % 2) != 0 || nLength < 4 )
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Invalid length for Neatline");
            return FALSE;
        }

        GDALPDFObject* poDescription;
        int bIsAskedNeatline = FALSE;
        if ( (poDescription = poLGIDict->Get("Description")) != NULL &&
            poDescription->GetType() == PDFObjectType_String )
        {
            CPLDebug("PDF", "Description = %s", poDescription->GetString().c_str());

            if( EQUAL(poDescription->GetString().c_str(), pszNeatlineToSelect) )
            {
                dfMaxArea = 1e300;
                bIsAskedNeatline = TRUE;
            }
        }

        if( !bIsAskedNeatline )
        {
            double dfMinX = 0, dfMinY = 0, dfMaxX = 0, dfMaxY = 0;
            for(i=0;i<nLength;i+=2)
            {
                double dfX = Get(poNeatline, i);
                double dfY = Get(poNeatline, i + 1);
                if (i == 0 || dfX < dfMinX) dfMinX = dfX;
                if (i == 0 || dfY < dfMinY) dfMinY = dfY;
                if (i == 0 || dfX > dfMaxX) dfMaxX = dfX;
                if (i == 0 || dfY > dfMaxY) dfMaxY = dfY;
            }
            double dfArea = (dfMaxX - dfMinX) * (dfMaxY - dfMinY);
            if (dfArea < dfMaxArea)
            {
                CPLDebug("PDF", "Not the largest neatline. Skipping it");
                return TRUE;
            }

            CPLDebug("PDF", "This is the largest neatline for now");
            dfMaxArea = dfArea;
        }
        else
            CPLDebug("PDF", "The \"%s\" registration will be selected",
                     pszNeatlineToSelect);

        if (pbIsBestCandidate)
            *pbIsBestCandidate = TRUE;

        delete poNeatLine;
        poNeatLine = new OGRPolygon();
        OGRLinearRing* poRing = new OGRLinearRing();
        if (nLength == 4)
        {
            /* 2 points only ? They are the bounding box */
            double dfX1 = Get(poNeatline, 0);
            double dfY1 = Get(poNeatline, 1);
            double dfX2 = Get(poNeatline, 2);
            double dfY2 = Get(poNeatline, 3);
            poRing->addPoint(dfX1, dfY1);
            poRing->addPoint(dfX2, dfY1);
            poRing->addPoint(dfX2, dfY2);
            poRing->addPoint(dfX1, dfY2);
        }
        else
        {
            for(i=0;i<nLength;i+=2)
            {
                double dfX = Get(poNeatline, i);
                double dfY = Get(poNeatline, i + 1);
                poRing->addPoint(dfX, dfY);
            }
        }
        poNeatLine->addRingDirectly(poRing);
    }

    return TRUE;
}

/************************************************************************/
/*                  ParseLGIDictDictSecondPass()                        */
/************************************************************************/

int PDFDataset::ParseLGIDictDictSecondPass(GDALPDFDictionary* poLGIDict)
{
    int i;

/* -------------------------------------------------------------------- */
/*      Extract Description attribute                                   */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poDescription;
    if ( (poDescription = poLGIDict->Get("Description")) != NULL &&
         poDescription->GetType() == PDFObjectType_String )
    {
        CPLDebug("PDF", "Description = %s", poDescription->GetString().c_str());
    }

/* -------------------------------------------------------------------- */
/*      Extract CTM attribute                                           */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poCTM;
    bHasCTM = FALSE;
    if ((poCTM = poLGIDict->Get("CTM")) != NULL &&
        poCTM->GetType() == PDFObjectType_Array)
    {
        int nLength = poCTM->GetArray()->GetLength();
        if ( nLength != 6 )
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Invalid length for CTM");
            return FALSE;
        }

        bHasCTM = TRUE;
        for(i=0;i<nLength;i++)
        {
            adfCTM[i] = Get(poCTM, i);
            /* Nullify rotation terms that are significantly smaller than */
            /* scaling termes */
            if ((i == 1 || i == 2) && fabs(adfCTM[i]) < fabs(adfCTM[0]) * 1e-10)
                adfCTM[i] = 0;
            CPLDebug("PDF", "CTM[%d] = %.16g", i, adfCTM[i]);
        }
    }

/* -------------------------------------------------------------------- */
/*      Extract Registration attribute                                  */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poRegistration;
    if ((poRegistration = poLGIDict->Get("Registration")) != NULL &&
        poRegistration->GetType() == PDFObjectType_Array)
    {
        GDALPDFArray* poRegistrationArray = poRegistration->GetArray();
        int nLength = poRegistrationArray->GetLength();
        if( nLength > 4 || (!bHasCTM && nLength >= 2) ||
            CSLTestBoolean(CPLGetConfigOption("PDF_REPORT_GCPS", "NO")) )
        {
            nGCPCount = 0;
            pasGCPList = (GDAL_GCP *) CPLCalloc(sizeof(GDAL_GCP),nLength);

            for(i=0;i<nLength;i++)
            {
                GDALPDFObject* poGCP = poRegistrationArray->Get(i);
                if ( poGCP != NULL &&
                    poGCP->GetType() == PDFObjectType_Array &&
                    poGCP->GetArray()->GetLength() == 4 )
                {
                    double dfUserX = Get(poGCP, 0);
                    double dfUserY = Get(poGCP, 1);
                    double dfX = Get(poGCP, 2);
                    double dfY = Get(poGCP, 3);
                    CPLDebug("PDF", "GCP[%d].userX = %.16g", i, dfUserX);
                    CPLDebug("PDF", "GCP[%d].userY = %.16g", i, dfUserY);
                    CPLDebug("PDF", "GCP[%d].x = %.16g", i, dfX);
                    CPLDebug("PDF", "GCP[%d].y = %.16g", i, dfY);

                    char    szID[32];
                    sprintf( szID, "%d", nGCPCount+1 );
                    pasGCPList[nGCPCount].pszId = CPLStrdup( szID );
                    pasGCPList[nGCPCount].pszInfo = CPLStrdup("");
                    pasGCPList[nGCPCount].dfGCPPixel = dfUserX;
                    pasGCPList[nGCPCount].dfGCPLine = dfUserY;
                    pasGCPList[nGCPCount].dfGCPX = dfX;
                    pasGCPList[nGCPCount].dfGCPY = dfY;
                    nGCPCount ++;
                }
            }
        }
    }

    if (!bHasCTM && nGCPCount == 0)
    {
        CPLDebug("PDF", "Neither CTM nor Registration found");
        return FALSE;
    }

/* -------------------------------------------------------------------- */
/*      Extract Projection attribute                                    */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poProjection;
    if ((poProjection = poLGIDict->Get("Projection")) == NULL ||
        poProjection->GetType() != PDFObjectType_Dictionary)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Could not find Projection");
        return FALSE;
    }

    return ParseProjDict(poProjection->GetDictionary());
}

/************************************************************************/
/*                         ParseProjDict()                               */
/************************************************************************/

int PDFDataset::ParseProjDict(GDALPDFDictionary* poProjDict)
{
    if (poProjDict == NULL)
        return FALSE;
    OGRSpatialReference oSRS;

/* -------------------------------------------------------------------- */
/*      Extract WKT attribute (GDAL extension)                          */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poWKT;
    if ( (poWKT = poProjDict->Get("WKT")) != NULL &&
         poWKT->GetType() == PDFObjectType_String &&
         CSLTestBoolean( CPLGetConfigOption("GDAL_PDF_OGC_BP_READ_WKT", "TRUE") ) )
    {
        CPLDebug("PDF", "Found WKT attribute (GDAL extension). Using it");
        const char* pszWKTRead = poWKT->GetString().c_str();
        CPLFree(pszWKT);
        pszWKT = CPLStrdup(pszWKTRead);
        return TRUE;
    }

/* -------------------------------------------------------------------- */
/*      Extract Type attribute                                          */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poType;
    if ((poType = poProjDict->Get("Type")) == NULL)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot find Type of Projection object");
        return FALSE;
    }

    if ( poType->GetType() != PDFObjectType_Name )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Invalid type for Type of Projection object");
        return FALSE;
    }

    if ( strcmp(poType->GetName().c_str(), "Projection") != 0 )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Invalid value for Type of Projection object : %s",
                 poType->GetName().c_str());
        return FALSE;
    }

/* -------------------------------------------------------------------- */
/*      Extract Datum attribute                                         */
/* -------------------------------------------------------------------- */
    int bIsWGS84 = FALSE;
    int bIsNAD83 = FALSE;
    /* int bIsNAD27 = FALSE; */

    GDALPDFObject* poDatum;
    if ((poDatum = poProjDict->Get("Datum")) != NULL)
    {
        if (poDatum->GetType() == PDFObjectType_String)
        {
            /* Using Annex A of http://portal.opengeospatial.org/files/?artifact_id=40537 */
            const char* pszDatum = poDatum->GetString().c_str();
            CPLDebug("PDF", "Datum = %s", pszDatum);
            if (EQUAL(pszDatum, "WE") || EQUAL(pszDatum, "WGE"))
            {
                bIsWGS84 = TRUE;
                oSRS.SetWellKnownGeogCS("WGS84");
            }
            else if (EQUAL(pszDatum, "NAR") || EQUALN(pszDatum, "NAR-", 4))
            {
                bIsNAD83 = TRUE;
                oSRS.SetWellKnownGeogCS("NAD83");
            }
            else if (EQUAL(pszDatum, "NAS") || EQUALN(pszDatum, "NAS-", 4))
            {
                /* bIsNAD27 = TRUE; */
                oSRS.SetWellKnownGeogCS("NAD27");
            }
            else if (EQUAL(pszDatum, "HEN")) /* HERAT North, Afghanistan */
            {
                oSRS.SetGeogCS( "unknown" /*const char * pszGeogName*/,
                                "unknown" /*const char * pszDatumName */,
                                "International 1924",
                                6378388,297);
                oSRS.SetTOWGS84(-333,-222,114);
            }
            else if (EQUAL(pszDatum, "ING-A")) /* INDIAN 1960, Vietnam 16N */
            {
                oSRS.importFromEPSG(4131);
            }
            else if (EQUAL(pszDatum, "GDS")) /* Geocentric Datum of Australia */
            {
                oSRS.importFromEPSG(4283);
            }
            else if (EQUALN(pszDatum, "OHA-", 4)) /* Old Hawaiian */
            {
                oSRS.importFromEPSG(4135); /* matches OHA-M (Mean) */
                if( !EQUAL(pszDatum, "OHA-M") )
                {
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "Using OHA-M (Old Hawaiian Mean) definition for %s. Potential issue with datum shift parameters",
                             pszDatum);
                    OGR_SRSNode *poNode = oSRS.GetRoot();
                    int iChild = poNode->FindChild( "AUTHORITY" );
                    if( iChild != -1 )
                        poNode->DestroyChild( iChild );
                    iChild = poNode->FindChild( "DATUM" );
                    if( iChild != -1 )
                    {
                        poNode = poNode->GetChild(iChild);
                        iChild = poNode->FindChild( "AUTHORITY" );
                        if( iChild != -1 )
                            poNode->DestroyChild( iChild );
                    }
                }
            }
            else
            {
                CPLError(CE_Warning, CPLE_AppDefined,
                        "Unhandled (yet) value for Datum : %s. Defaulting to WGS84...",
                        pszDatum);
                oSRS.SetGeogCS( "unknown" /*const char * pszGeogName*/,
                                "unknown" /*const char * pszDatumName */,
                                "unknown",
                                6378137,298.257223563);
            }
        }
        else if (poDatum->GetType() == PDFObjectType_Dictionary)
        {
            GDALPDFDictionary* poDatumDict = poDatum->GetDictionary();

            GDALPDFObject* poDatumDescription = poDatumDict->Get("Description");
            const char* pszDatumDescription = "unknown";
            if (poDatumDescription != NULL &&
                poDatumDescription->GetType() == PDFObjectType_String)
                pszDatumDescription  = poDatumDescription->GetString().c_str();
            CPLDebug("PDF", "Datum.Description = %s", pszDatumDescription);
                
            GDALPDFObject* poEllipsoid = poDatumDict->Get("Ellipsoid");
            if (poEllipsoid == NULL ||
                !(poEllipsoid->GetType() == PDFObjectType_String ||
                  poEllipsoid->GetType() == PDFObjectType_Dictionary))
            {
                CPLError(CE_Warning, CPLE_AppDefined,
                         "Cannot find Ellipsoid in Datum. Defaulting to WGS84...");
                oSRS.SetGeogCS( "unknown",
                                pszDatumDescription,
                                "unknown",
                                6378137,298.257223563);
            }
            else if (poEllipsoid->GetType() == PDFObjectType_String)
            {
                const char* pszEllipsoid = poEllipsoid->GetString().c_str();
                CPLDebug("PDF", "Datum.Ellipsoid = %s", pszEllipsoid);
                if( EQUAL(pszEllipsoid, "WE") )
                {
                    oSRS.SetGeogCS( "unknown",
                                    pszDatumDescription,
                                    "WGS 84",
                                    6378137,298.257223563);
                }
                else
                {
                    CPLError(CE_Warning, CPLE_AppDefined,
                            "Unhandled (yet) value for Ellipsoid : %s. Defaulting to WGS84...",
                            pszEllipsoid);
                    oSRS.SetGeogCS( "unknown",
                                    pszDatumDescription,
                                    pszEllipsoid,
                                    6378137,298.257223563);
                }
            }
            else// if (poEllipsoid->GetType() == PDFObjectType_Dictionary)
            {
                GDALPDFDictionary* poEllipsoidDict = poEllipsoid->GetDictionary();

                GDALPDFObject* poEllipsoidDescription = poEllipsoidDict->Get("Description");
                const char* pszEllipsoidDescription = "unknown";
                if (poEllipsoidDescription != NULL &&
                    poEllipsoidDescription->GetType() == PDFObjectType_String)
                    pszEllipsoidDescription = poEllipsoidDescription->GetString().c_str();
                CPLDebug("PDF", "Datum.Ellipsoid.Description = %s", pszEllipsoidDescription);
                    
                double dfSemiMajor = Get(poEllipsoidDict, "SemiMajorAxis");
                CPLDebug("PDF", "Datum.Ellipsoid.SemiMajorAxis = %.16g", dfSemiMajor);
                double dfInvFlattening = -1.0;
                
                if( poEllipsoidDict->Get("InvFlattening") )
                {
                    dfInvFlattening = Get(poEllipsoidDict, "InvFlattening");
                    CPLDebug("PDF", "Datum.Ellipsoid.InvFlattening = %.16g", dfInvFlattening);
                }
                else if( poEllipsoidDict->Get("SemiMinorAxis") )
                {
                    double dfSemiMinor = Get(poEllipsoidDict, "SemiMinorAxis");
                    CPLDebug("PDF", "Datum.Ellipsoid.SemiMinorAxis = %.16g", dfSemiMinor);
                    dfInvFlattening = OSRCalcInvFlattening(dfSemiMajor, dfSemiMinor);
                }
                
                if( dfSemiMajor != 0.0 && dfInvFlattening != -1.0 )
                {
                    oSRS.SetGeogCS( "unknown",
                                    pszDatumDescription,
                                    pszEllipsoidDescription,
                                    dfSemiMajor, dfInvFlattening);
                }
                else
                {
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "Invalid Ellipsoid object. Defaulting to WGS84...");
                    oSRS.SetGeogCS( "unknown",
                                    pszDatumDescription,
                                    pszEllipsoidDescription,
                                    6378137,298.257223563);
                }
                
                
            }
            
            GDALPDFObject* poTOWGS84 = poDatumDict->Get("ToWGS84");
            if( poTOWGS84 != NULL && poTOWGS84->GetType() == PDFObjectType_Dictionary )
            {
                GDALPDFDictionary* poTOWGS84Dict = poTOWGS84->GetDictionary();
                double dx = Get(poTOWGS84Dict, "dx");
                double dy = Get(poTOWGS84Dict, "dy");
                double dz = Get(poTOWGS84Dict, "dz");
                if (poTOWGS84Dict->Get("rx") && poTOWGS84Dict->Get("ry") &&
                    poTOWGS84Dict->Get("rz") && poTOWGS84Dict->Get("sf"))
                {
                    double rx = Get(poTOWGS84Dict, "rx");
                    double ry = Get(poTOWGS84Dict, "ry");
                    double rz = Get(poTOWGS84Dict, "rz");
                    double sf = Get(poTOWGS84Dict, "sf");
                    oSRS.SetTOWGS84(dx, dy, dz, rx, ry, rz, sf);
                }
                else
                {
                    oSRS.SetTOWGS84(dx, dy, dz);
                }
            }
        }
    }

/* -------------------------------------------------------------------- */
/*      Extract Hemisphere attribute                                    */
/* -------------------------------------------------------------------- */
    CPLString osHemisphere;
    GDALPDFObject* poHemisphere;
    if ((poHemisphere = poProjDict->Get("Hemisphere")) != NULL &&
        poHemisphere->GetType() == PDFObjectType_String)
    {
        osHemisphere = poHemisphere->GetString();
    }

/* -------------------------------------------------------------------- */
/*      Extract ProjectionType attribute                                */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poProjectionType;
    if ((poProjectionType = poProjDict->Get("ProjectionType")) == NULL ||
        poProjectionType->GetType() != PDFObjectType_String)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot find ProjectionType of Projection object");
        return FALSE;
    }
    CPLString osProjectionType(poProjectionType->GetString());
    CPLDebug("PDF", "Projection.ProjectionType = %s", osProjectionType.c_str());

    /* Unhandled: NONE, GEODETIC */

    if (EQUAL(osProjectionType, "GEOGRAPHIC"))
    {
        /* Nothing to do */
    }

    /* Unhandled: LOCAL CARTESIAN, MG (MGRS) */

    else if (EQUAL(osProjectionType, "UT")) /* UTM */
    {
        int nZone = (int)Get(poProjDict, "Zone");
        int bNorth = EQUAL(osHemisphere, "N");
        if (bIsWGS84)
            oSRS.importFromEPSG( ((bNorth) ? 32600 : 32700) + nZone );
        else
            oSRS.SetUTM( nZone, bNorth );
    }

    else if (EQUAL(osProjectionType, "UP")) /* Universal Polar Stereographic (UPS) */
    {
        int bNorth = EQUAL(osHemisphere, "N");
        if (bIsWGS84)
            oSRS.importFromEPSG( (bNorth) ? 32661 : 32761 );
        else
            oSRS.SetPS( (bNorth) ? 90 : -90, 0,
                        0.994, 200000, 200000 );
    }

    else if (EQUAL(osProjectionType, "SPCS")) /* State Plane */
    {
        int nZone = (int)Get(poProjDict, "Zone");
        oSRS.SetStatePlane( nZone, bIsNAD83 );
    }

    else if (EQUAL(osProjectionType, "AC")) /* Albers Equal Area Conic */
    {
        double dfStdP1 = Get(poProjDict, "StandardParallelOne");
        double dfStdP2 = Get(poProjDict, "StandardParallelTwo");
        double dfCenterLat = Get(poProjDict, "OriginLatitude");
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetACEA( dfStdP1, dfStdP2,
                     dfCenterLat, dfCenterLong,
                     dfFalseEasting, dfFalseNorthing );
    }
 
    else if (EQUAL(osProjectionType, "AL")) /* Azimuthal Equidistant */
    {
        double dfCenterLat = Get(poProjDict, "OriginLatitude");
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetAE(  dfCenterLat, dfCenterLong,
                     dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "BF")) /* Bonne */
    {
        double dfStdP1 = Get(poProjDict, "OriginLatitude");
        double dfCentralMeridian = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetBonne( dfStdP1, dfCentralMeridian,
                       dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "CS")) /* Cassini */
    {
        double dfCenterLat = Get(poProjDict, "OriginLatitude");
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetCS(  dfCenterLat, dfCenterLong,
                     dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "LI")) /* Cylindrical Equal Area */
    {
        double dfStdP1 = Get(poProjDict, "OriginLatitude");
        double dfCentralMeridian = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetCEA( dfStdP1, dfCentralMeridian,
                     dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "EF")) /* Eckert IV */
    {
        double dfCentralMeridian = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetEckertIV( dfCentralMeridian,
                          dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "ED")) /* Eckert VI */
    {
        double dfCentralMeridian = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetEckertVI( dfCentralMeridian,
                          dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "CP")) /* Equidistant Cylindrical */
    {
        double dfCenterLat = Get(poProjDict, "StandardParallel");
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetEquirectangular( dfCenterLat, dfCenterLong,
                                 dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "GN")) /* Gnomonic */
    {
        double dfCenterLat = Get(poProjDict, "OriginLatitude");
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetGnomonic(dfCenterLat, dfCenterLong,
                         dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "LE")) /* Lambert Conformal Conic */
    {
        double dfStdP1 = Get(poProjDict, "StandardParallelOne");
        double dfStdP2 = Get(poProjDict, "StandardParallelTwo");
        double dfCenterLat = Get(poProjDict, "OriginLatitude");
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetLCC( dfStdP1, dfStdP2,
                     dfCenterLat, dfCenterLong,
                     dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "MC")) /* Mercator */
    {
#ifdef not_supported
        if (poProjDict->Get("StandardParallelOne") == NULL)
        {
#endif
        double dfCenterLat = Get(poProjDict, "OriginLatitude");
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfScale = Get(poProjDict, "ScaleFactor");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetMercator( dfCenterLat, dfCenterLong,
                          dfScale,
                          dfFalseEasting, dfFalseNorthing );
#ifdef not_supported
        }
        else
        {
            double dfStdP1 = Get(poProjDict, "StandardParallelOne");
            double dfCenterLat = poProjDict->Get("OriginLatitude") ? Get(poProjDict, "OriginLatitude") : 0;
            double dfCenterLong = Get(poProjDict, "CentralMeridian");
            double dfFalseEasting = Get(poProjDict, "FalseEasting");
            double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
            oSRS.SetMercator2SP( dfStdP1,
                                 dfCenterLat, dfCenterLong,
                                 dfFalseEasting, dfFalseNorthing );
        }
#endif
    }

    else if (EQUAL(osProjectionType, "MH")) /* Miller Cylindrical */
    {
        double dfCenterLat = 0 /* ? */;
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetMC( dfCenterLat, dfCenterLong,
                    dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "MP")) /* Mollweide */
    {
        double dfCentralMeridian = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetMollweide( dfCentralMeridian,
                           dfFalseEasting, dfFalseNorthing );
    }

    /* Unhandled:  "NY" : Ney's (Modified Lambert Conformal Conic) */

    else if (EQUAL(osProjectionType, "NT")) /* New Zealand Map Grid */
    {
        /* No parameter specified in the PDF, so let's take the ones of EPSG:27200 */
        double dfCenterLat = -41;
        double dfCenterLong = 173;
        double dfFalseEasting = 2510000;
        double dfFalseNorthing = 6023150;
        oSRS.SetNZMG( dfCenterLat, dfCenterLong,
                      dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "OC")) /* Oblique Mercator */
    {
        double dfCenterLat = Get(poProjDict, "OriginLatitude");
        double dfLat1 = Get(poProjDict, "LatitudeOne");
        double dfLong1 = Get(poProjDict, "LongitudeOne");
        double dfLat2 = Get(poProjDict, "LatitudeTwo");
        double dfLong2 = Get(poProjDict, "LongitudeTwo");
        double dfScale = Get(poProjDict, "ScaleFactor");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetHOM2PNO( dfCenterLat,
                         dfLat1, dfLong1,
                         dfLat2, dfLong2,
                         dfScale,
                         dfFalseEasting,
                         dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "OD")) /* Orthographic */
    {
        double dfCenterLat = Get(poProjDict, "OriginLatitude");
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetOrthographic( dfCenterLat, dfCenterLong,
                           dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "PG")) /* Polar Stereographic */
    {
        double dfCenterLat = Get(poProjDict, "LatitudeTrueScale");
        double dfCenterLong = Get(poProjDict, "LongitudeDownFromPole");
        double dfScale = 1.0;
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetPS( dfCenterLat, dfCenterLong,
                    dfScale,
                    dfFalseEasting, dfFalseNorthing);
    }

    else if (EQUAL(osProjectionType, "PH")) /* Polyconic */
    {
        double dfCenterLat = Get(poProjDict, "OriginLatitude");
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetPolyconic( dfCenterLat, dfCenterLong,
                           dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "SA")) /* Sinusoidal */
    {
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetSinusoidal( dfCenterLong,
                           dfFalseEasting, dfFalseNorthing );
    }

    else if (EQUAL(osProjectionType, "SD")) /* Stereographic */
    {
        double dfCenterLat = Get(poProjDict, "OriginLatitude");
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfScale = 1.0;
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetStereographic( dfCenterLat, dfCenterLong,
                               dfScale,
                               dfFalseEasting, dfFalseNorthing);
    }

    else if (EQUAL(osProjectionType, "TC")) /* Transverse Mercator */
    {
        double dfCenterLat = Get(poProjDict, "OriginLatitude");
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfScale = Get(poProjDict, "ScaleFactor");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        if (dfCenterLat == 0.0 && dfScale == 0.9996 && dfFalseEasting == 500000 &&
            (dfFalseNorthing == 0.0 || dfFalseNorthing == 10000000.0))
        {
            int nZone = (int) floor( (dfCenterLong + 180.0) / 6.0 ) + 1;
            int bNorth = dfFalseNorthing == 0;
            if (bIsWGS84)
                oSRS.importFromEPSG( ((bNorth) ? 32600 : 32700) + nZone );
            else if (bIsNAD83 && bNorth)
                oSRS.importFromEPSG( 26900 + nZone );
            else
                oSRS.SetUTM( nZone, bNorth );
        }
        else
        {
            oSRS.SetTM( dfCenterLat, dfCenterLong,
                        dfScale,
                        dfFalseEasting, dfFalseNorthing );
        }
    }

    /* Unhandled TX : Transverse Cylindrical Equal Area */

    else if (EQUAL(osProjectionType, "VA")) /* Van der Grinten */
    {
        double dfCenterLong = Get(poProjDict, "CentralMeridian");
        double dfFalseEasting = Get(poProjDict, "FalseEasting");
        double dfFalseNorthing = Get(poProjDict, "FalseNorthing");
        oSRS.SetVDG( dfCenterLong,
                     dfFalseEasting, dfFalseNorthing );
    }

    else
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Unhandled (yet) value for ProjectionType : %s",
                 osProjectionType.c_str());
        return FALSE;
    }

/* -------------------------------------------------------------------- */
/*      Extract Units attribute                                         */
/* -------------------------------------------------------------------- */
    CPLString osUnits;
    GDALPDFObject* poUnits;
    if ((poUnits = poProjDict->Get("Units")) != NULL &&
        poUnits->GetType() == PDFObjectType_String)
    {
        osUnits = poUnits->GetString();
        CPLDebug("PDF", "Projection.Units = %s", osUnits.c_str());

        if (EQUAL(osUnits, "M"))
            oSRS.SetLinearUnits( "Meter", 1.0 );
        else if (EQUAL(osUnits, "FT"))
            oSRS.SetLinearUnits( "foot", 0.3048 );
    }

/* -------------------------------------------------------------------- */
/*      Export SpatialRef                                               */
/* -------------------------------------------------------------------- */
    CPLFree(pszWKT);
    pszWKT = NULL;
    if (oSRS.exportToWkt(&pszWKT) != OGRERR_NONE)
    {
        CPLFree(pszWKT);
        pszWKT = NULL;
    }

    return TRUE;
}

/************************************************************************/
/*                              ParseVP()                               */
/************************************************************************/

int PDFDataset::ParseVP(GDALPDFObject* poVP, double dfMediaBoxWidth, double dfMediaBoxHeight)
{
    int i;

    if (poVP->GetType() != PDFObjectType_Array)
        return FALSE;

    GDALPDFArray* poVPArray = poVP->GetArray();

    int nLength = poVPArray->GetLength();
    CPLDebug("PDF", "VP length = %d", nLength);
    if (nLength < 1)
        return FALSE;

/* -------------------------------------------------------------------- */
/*      Find the largest BBox                                           */
/* -------------------------------------------------------------------- */
    int iLargest = 0;
    double dfLargestArea = 0;

    for(i=0;i<nLength;i++)
    {
        GDALPDFObject* poVPElt = poVPArray->Get(i);
        if (poVPElt == NULL || poVPElt->GetType() != PDFObjectType_Dictionary)
        {
            return FALSE;
        }

        GDALPDFDictionary* poVPEltDict = poVPElt->GetDictionary();

        GDALPDFObject* poBBox;
        if( (poBBox = poVPEltDict->Get("BBox")) == NULL ||
            poBBox->GetType() != PDFObjectType_Array )
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                    "Cannot find Bbox object");
            return FALSE;
        }

        int nBboxLength = poBBox->GetArray()->GetLength();
        if (nBboxLength != 4)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                    "Invalid length for Bbox object");
            return FALSE;
        }

        double adfBBox[4];
        adfBBox[0] = Get(poBBox, 0);
        adfBBox[1] = Get(poBBox, 1);
        adfBBox[2] = Get(poBBox, 2);
        adfBBox[3] = Get(poBBox, 3);
        double dfArea = fabs(adfBBox[2] - adfBBox[0]) * fabs(adfBBox[3] - adfBBox[1]);
        if (dfArea > dfLargestArea)
        {
            iLargest = i;
            dfLargestArea = dfArea;
        }
    }

    if (nLength > 1)
    {
        CPLDebug("PDF", "Largest BBox in VP array is element %d", iLargest);
    }


    GDALPDFObject* poVPElt = poVPArray->Get(iLargest);
    if (poVPElt == NULL || poVPElt->GetType() != PDFObjectType_Dictionary)
    {
        return FALSE;
    }

    GDALPDFDictionary* poVPEltDict = poVPElt->GetDictionary();

    GDALPDFObject* poBBox;
    if( (poBBox = poVPEltDict->Get("BBox")) == NULL ||
        poBBox->GetType() != PDFObjectType_Array )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                "Cannot find Bbox object");
        return FALSE;
    }

    int nBboxLength = poBBox->GetArray()->GetLength();
    if (nBboxLength != 4)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                "Invalid length for Bbox object");
        return FALSE;
    }

    double dfULX = Get(poBBox, 0);
    double dfULY = dfMediaBoxHeight - Get(poBBox, 1);
    double dfLRX = Get(poBBox, 2);
    double dfLRY = dfMediaBoxHeight - Get(poBBox, 3);

/* -------------------------------------------------------------------- */
/*      Extract Measure attribute                                       */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poMeasure;
    if( (poMeasure = poVPEltDict->Get("Measure")) == NULL ||
        poMeasure->GetType() != PDFObjectType_Dictionary )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot find Measure object");
        return FALSE;
    }

    int bRet = ParseMeasure(poMeasure, dfMediaBoxWidth, dfMediaBoxHeight,
                            dfULX, dfULY, dfLRX, dfLRY);

/* -------------------------------------------------------------------- */
/*      Extract PointData attribute                                     */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poPointData;
    if( (poPointData = poVPEltDict->Get("PtData")) != NULL &&
        poPointData->GetType() == PDFObjectType_Dictionary )
    {
        CPLDebug("PDF", "Found PointData");
    }

    return bRet;
}

/************************************************************************/
/*                           ParseMeasure()                             */
/************************************************************************/

int PDFDataset::ParseMeasure(GDALPDFObject* poMeasure,
                             double dfMediaBoxWidth, double dfMediaBoxHeight,
                             double dfULX, double dfULY, double dfLRX, double dfLRY)
{
    int i;
    GDALPDFDictionary* poMeasureDict = poMeasure->GetDictionary();

/* -------------------------------------------------------------------- */
/*      Extract Subtype attribute                                       */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poSubtype;
    if( (poSubtype = poMeasureDict->Get("Subtype")) == NULL ||
        poSubtype->GetType() != PDFObjectType_Name )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot find Subtype object");
        return FALSE;
    }

    CPLDebug("PDF", "Subtype = %s", poSubtype->GetName().c_str());

/* -------------------------------------------------------------------- */
/*      Extract Bounds attribute (optional)                             */
/* -------------------------------------------------------------------- */

    /* http://acrobatusers.com/sites/default/files/gallery_pictures/SEVERODVINSK.pdf */
    /* has lgit:LPTS, lgit:GPTS and lgit:Bounds that have more precision than */
    /* LPTS, GPTS and Bounds. Use those ones */

    GDALPDFObject* poBounds;
    if( (poBounds = poMeasureDict->Get("lgit:Bounds")) != NULL &&
        poBounds->GetType() == PDFObjectType_Array )
    {
        CPLDebug("PDF", "Using lgit:Bounds");
    }
    else if( (poBounds = poMeasureDict->Get("Bounds")) == NULL ||
              poBounds->GetType() != PDFObjectType_Array )
    {
        poBounds = NULL;
    }

    if (poBounds != NULL)
    {
        int nBoundsLength = poBounds->GetArray()->GetLength();
        if (nBoundsLength == 8)
        {
            double adfBounds[8];
            for(i=0;i<8;i++)
            {
                adfBounds[i] = Get(poBounds, i);
                CPLDebug("PDF", "Bounds[%d] = %f", i, adfBounds[i]);
            }

            // TODO we should use it to restrict the neatline but
            // I have yet to set a sample where bounds are not the four
            // corners of the unit square.
        }
    }

/* -------------------------------------------------------------------- */
/*      Extract GPTS attribute                                          */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poGPTS;
    if( (poGPTS = poMeasureDict->Get("lgit:GPTS")) != NULL &&
        poGPTS->GetType() == PDFObjectType_Array )
    {
        CPLDebug("PDF", "Using lgit:GPTS");
    }
    else if( (poGPTS = poMeasureDict->Get("GPTS")) == NULL ||
              poGPTS->GetType() != PDFObjectType_Array )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot find GPTS object");
        return FALSE;
    }

    int nGPTSLength = poGPTS->GetArray()->GetLength();
    if (nGPTSLength != 8)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Invalid length for GPTS object");
        return FALSE;
    }

    double adfGPTS[8];
    for(i=0;i<8;i++)
    {
        adfGPTS[i] = Get(poGPTS, i);
        CPLDebug("PDF", "GPTS[%d] = %.18f", i, adfGPTS[i]);
    }

/* -------------------------------------------------------------------- */
/*      Extract LPTS attribute                                          */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poLPTS;
    if( (poLPTS = poMeasureDict->Get("lgit:LPTS")) != NULL &&
        poLPTS->GetType() == PDFObjectType_Array )
    {
        CPLDebug("PDF", "Using lgit:LPTS");
    }
    else if( (poLPTS = poMeasureDict->Get("LPTS")) == NULL ||
              poLPTS->GetType() != PDFObjectType_Array )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot find LPTS object");
        return FALSE;
    }

    int nLPTSLength = poLPTS->GetArray()->GetLength();
    if (nLPTSLength != 8)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Invalid length for LPTS object");
        return FALSE;
    }

    double adfLPTS[8];
    for(i=0;i<8;i++)
    {
        adfLPTS[i] = Get(poLPTS, i);
        CPLDebug("PDF", "LPTS[%d] = %f", i, adfLPTS[i]);
    }

/* -------------------------------------------------------------------- */
/*      Extract GCS attribute                                           */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poGCS;
    if( (poGCS = poMeasureDict->Get("GCS")) == NULL ||
        poGCS->GetType() != PDFObjectType_Dictionary )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot find GCS object");
        return FALSE;
    }

    GDALPDFDictionary* poGCSDict = poGCS->GetDictionary();

/* -------------------------------------------------------------------- */
/*      Extract GCS.Type attribute                                      */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poGCSType;
    if( (poGCSType = poGCSDict->Get("Type")) == NULL ||
        poGCSType->GetType() != PDFObjectType_Name )
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot find GCS.Type object");
        return FALSE;
    }

    CPLDebug("PDF", "GCS.Type = %s", poGCSType->GetName().c_str());

/* -------------------------------------------------------------------- */
/*      Extract EPSG attribute                                          */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poEPSG;
    int nEPSGCode = 0;
    if( (poEPSG = poGCSDict->Get("EPSG")) != NULL &&
        poEPSG->GetType() == PDFObjectType_Int )
    {
        nEPSGCode = poEPSG->GetInt();
        CPLDebug("PDF", "GCS.EPSG = %d", nEPSGCode);
    }

/* -------------------------------------------------------------------- */
/*      Extract GCS.WKT attribute                                       */
/* -------------------------------------------------------------------- */
    GDALPDFObject* poGCSWKT = poGCSDict->Get("WKT");
    if( poGCSWKT != NULL &&
        poGCSWKT->GetType() != PDFObjectType_String )
    {
        poGCSWKT = NULL;
    }

    if (poGCSWKT != NULL)
        CPLDebug("PDF", "GCS.WKT = %s", poGCSWKT->GetString().c_str());

    if (nEPSGCode <= 0 && poGCSWKT == NULL)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "Cannot find GCS.WKT or GCS.EPSG objects");
        return FALSE;
    }

    OGRSpatialReference oSRS;
    int bSRSOK = FALSE;
    if (nEPSGCode != 0 &&
        oSRS.importFromEPSG(nEPSGCode) == OGRERR_NONE)
    {
        bSRSOK = TRUE;
        CPLFree(pszWKT);
        pszWKT = NULL;
        oSRS.exportToWkt(&pszWKT);
    }
    else
    {
        if (poGCSWKT == NULL)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                    "Cannot resolve EPSG object, and GCS.WKT not found");
            return FALSE;
        }

        CPLFree(pszWKT);
        pszWKT = CPLStrdup(poGCSWKT->GetString().c_str());
    }

    if (!bSRSOK)
    {
        char* pszWktTemp = pszWKT;
        if (oSRS.importFromWkt(&pszWktTemp) != OGRERR_NONE)
        {
            CPLFree(pszWKT);
            pszWKT = NULL;
            return FALSE;
        }
    }

    /* For http://www.avenza.com/sites/default/files/spatialpdf/US_County_Populations.pdf */
    /* or http://www.agmkt.state.ny.us/soilwater/aem/gis_mapping_tools/HUC12_Albany.pdf */
    const char* pszDatum = oSRS.GetAttrValue("Datum");
    if (pszDatum && strncmp(pszDatum, "D_", 2) == 0)
    {
        oSRS.morphFromESRI();

        CPLFree(pszWKT);
        pszWKT = NULL;
        if (oSRS.exportToWkt(&pszWKT) != OGRERR_NONE)
        {
            CPLFree(pszWKT);
            pszWKT = NULL;
        }
        else
        {
            CPLDebug("PDF", "WKT after morphFromESRI() = %s", pszWKT);
        }
    }

/* -------------------------------------------------------------------- */
/*      Compute geotransform                                            */
/* -------------------------------------------------------------------- */
    OGRSpatialReference* poSRSGeog = oSRS.CloneGeogCS();

    /* Files found at http://carto.iict.ch/blog/publications-cartographiques-au-format-geospatial-pdf/ */
    /* are in a PROJCS. However the coordinates in GPTS array are not in (lat, long) as required by the */
    /* ISO 32000 supplement spec, but in (northing, easting). Adobe reader is able to understand that, */
    /* so let's also try to do it with a heuristics. */

    int bReproject = TRUE;
    if (oSRS.IsProjected() &&
        (fabs(adfGPTS[0]) > 91 || fabs(adfGPTS[2]) > 91 || fabs(adfGPTS[4]) > 91 || fabs(adfGPTS[6]) > 91 ||
         fabs(adfGPTS[1]) > 361 || fabs(adfGPTS[3]) > 361 || fabs(adfGPTS[5]) > 361 || fabs(adfGPTS[7]) > 361))
    {
        CPLDebug("PDF", "GPTS coordinates seems to be in (northing, easting), which is non-standard");
        bReproject = FALSE;
    }

    OGRCoordinateTransformation* poCT = NULL;
    if (bReproject)
    {
        poCT = OGRCreateCoordinateTransformation( poSRSGeog, &oSRS);
        if (poCT == NULL)
        {
            delete poSRSGeog;
            CPLFree(pszWKT);
            pszWKT = NULL;
            return FALSE;
        }
    }

    GDAL_GCP asGCPS[4];

    /* Create NEATLINE */
    poNeatLine = new OGRPolygon();
    OGRLinearRing* poRing = new OGRLinearRing();
    poNeatLine->addRingDirectly(poRing);

    for(i=0;i<4;i++)
    {
        /* We probably assume LPTS is 0 or 1 */
        asGCPS[i].dfGCPPixel = (dfULX * (1 - adfLPTS[2*i+0]) + dfLRX * adfLPTS[2*i+0]) / dfMediaBoxWidth * nRasterXSize;
        asGCPS[i].dfGCPLine  = (dfULY * (1 - adfLPTS[2*i+1]) + dfLRY * adfLPTS[2*i+1]) / dfMediaBoxHeight * nRasterYSize;

        double lat = adfGPTS[2*i], lon = adfGPTS[2*i+1];
        double x = lon, y = lat;
        if (bReproject)
        {
            if (!poCT->Transform(1, &x, &y, NULL))
            {
                CPLError(CE_Failure, CPLE_AppDefined,
                        "Cannot reproject (%f, %f)", lon, lat);
                delete poSRSGeog;
                delete poCT;
                CPLFree(pszWKT);
                pszWKT = NULL;
                return FALSE;
            }
        }

        x = ROUND_TO_INT_IF_CLOSE(x);
        y = ROUND_TO_INT_IF_CLOSE(y);

        asGCPS[i].dfGCPX     = x;
        asGCPS[i].dfGCPY     = y;

        poRing->addPoint(x, y);
    }

    delete poSRSGeog;
    delete poCT;

    if (!GDALGCPsToGeoTransform( 4, asGCPS,
                               adfGeoTransform, FALSE))
    {
        CPLDebug("PDF", "Could not compute GT with exact match. Try with approximate");
        if (!GDALGCPsToGeoTransform( 4, asGCPS,
                               adfGeoTransform, TRUE))
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "Could not compute GT with approximate match.");
            return FALSE;
        }
    }
    bGeoTransformValid = TRUE;

    /* If the non scaling terms of the geotransform are significantly smaller than */
    /* the pixel size, then nullify them as being just artifacts of reprojection and */
    /* GDALGCPsToGeoTransform() numerical imprecisions */
    double dfPixelSize = MIN(fabs(adfGeoTransform[1]), fabs(adfGeoTransform[5]));
    double dfRotationShearTerm = MAX(fabs(adfGeoTransform[2]), fabs(adfGeoTransform[4]));
    if (dfRotationShearTerm < 1e-5 * dfPixelSize)
    {
        double dfLRX = adfGeoTransform[0] + nRasterXSize * adfGeoTransform[1] + nRasterYSize * adfGeoTransform[2];
        double dfLRY = adfGeoTransform[3] + nRasterXSize * adfGeoTransform[4] + nRasterYSize * adfGeoTransform[5];
        adfGeoTransform[1] = (dfLRX - adfGeoTransform[0]) / nRasterXSize;
        adfGeoTransform[5] = (dfLRY - adfGeoTransform[3]) / nRasterYSize;
        adfGeoTransform[2] = adfGeoTransform[4] = 0;
    }

    return TRUE;
}

/************************************************************************/
/*                          GetProjectionRef()                          */
/************************************************************************/

const char* PDFDataset::GetProjectionRef()
{
    if (pszWKT && bGeoTransformValid)
        return pszWKT;
    return "";
}

/************************************************************************/
/*                          GetGeoTransform()                           */
/************************************************************************/

CPLErr PDFDataset::GetGeoTransform( double * padfTransform )

{
    memcpy(padfTransform, adfGeoTransform, 6 * sizeof(double));

    return( (bGeoTransformValid) ? CE_None : CE_Failure );
}

/************************************************************************/
/*                            SetProjection()                           */
/************************************************************************/

CPLErr PDFDataset::SetProjection(const char* pszWKTIn)
{
    CPLFree(pszWKT);
    pszWKT = pszWKTIn ? CPLStrdup(pszWKTIn) : CPLStrdup("");
    bProjDirty = TRUE;
    return CE_None;
}

/************************************************************************/
/*                          SetGeoTransform()                           */
/************************************************************************/

CPLErr PDFDataset::SetGeoTransform(double* padfGeoTransform)
{
    memcpy(adfGeoTransform, padfGeoTransform, 6 * sizeof(double));
    bGeoTransformValid = TRUE;
    bProjDirty = TRUE;

    /* Reset NEATLINE if not explicitly set by the user */
    if (!bNeatLineDirty)
        SetMetadataItem("NEATLINE", NULL);
    return CE_None;
}

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

char **PDFDataset::GetMetadataDomainList()
{
    return BuildMetadataDomainList(GDALPamDataset::GetMetadataDomainList(),
                                   TRUE,
                                   "xml:XMP", "LAYERS", "EMBEDDED_METADATA", NULL);
}

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

char      **PDFDataset::GetMetadata( const char * pszDomain )
{
    if( pszDomain != NULL && EQUAL(pszDomain, "EMBEDDED_METADATA") )
    {
        char** papszRet = oMDMD.GetMetadata(pszDomain);
        if( papszRet )
            return papszRet;

        GDALPDFObject* poCatalog = GetCatalog();
        if( poCatalog == NULL )
            return NULL;
        GDALPDFObject* poFirstElt = poCatalog->LookupObject("Names.EmbeddedFiles.Names[0]");
        GDALPDFObject* poF = poCatalog->LookupObject("Names.EmbeddedFiles.Names[1].EF.F");

        if( poFirstElt == NULL || poFirstElt->GetType() != PDFObjectType_String ||
            poFirstElt->GetString() != "Metadata" )
            return NULL;
        if( poF == NULL || poF->GetType() != PDFObjectType_Dictionary )
            return NULL;
        GDALPDFStream* poStream = poF->GetStream();
        if( poStream == NULL )
            return NULL;

        char* apszMetadata[2] = { NULL, NULL };
        apszMetadata[0] = poStream->GetBytes();
        oMDMD.SetMetadata(apszMetadata, pszDomain);
        VSIFree(apszMetadata[0]);
    }

    return oMDMD.GetMetadata(pszDomain);
}

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

CPLErr      PDFDataset::SetMetadata( char ** papszMetadata,
                                     const char * pszDomain )
{
    if (pszDomain == NULL || EQUAL(pszDomain, ""))
    {
        if (CSLFindString(papszMetadata, "NEATLINE") != -1)
        {
            bProjDirty = TRUE;
            bNeatLineDirty = TRUE;
        }
        bInfoDirty = TRUE;
    }
    else if (EQUAL(pszDomain, "xml:XMP"))
        bXMPDirty = TRUE;
    return oMDMD.SetMetadata(papszMetadata, pszDomain);
}

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

const char *PDFDataset::GetMetadataItem( const char * pszName,
                                         const char * pszDomain )
{
    return oMDMD.GetMetadataItem(pszName, pszDomain);
}

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

CPLErr      PDFDataset::SetMetadataItem( const char * pszName,
                                         const char * pszValue,
                                         const char * pszDomain )
{
    if (pszDomain == NULL || EQUAL(pszDomain, ""))
    {
        if (EQUAL(pszName, "NEATLINE"))
        {
            bProjDirty = TRUE;
            bNeatLineDirty = TRUE;
        }
        else
        {
            if (pszValue == NULL)
                pszValue = "";
            bInfoDirty = TRUE;
        }
    }
    else if (EQUAL(pszDomain, "xml:XMP"))
        bXMPDirty = TRUE;
    return oMDMD.SetMetadataItem(pszName, pszValue, pszDomain);
}

/************************************************************************/
/*                            GetGCPCount()                             */
/************************************************************************/

int PDFDataset::GetGCPCount()
{
    return nGCPCount;
}

/************************************************************************/
/*                          GetGCPProjection()                          */
/************************************************************************/

const char * PDFDataset::GetGCPProjection()
{
    if (pszWKT != NULL && nGCPCount != 0)
        return pszWKT;
    return "";
}

/************************************************************************/
/*                              GetGCPs()                               */
/************************************************************************/

const GDAL_GCP * PDFDataset::GetGCPs()
{
    return pasGCPList;
}

/************************************************************************/
/*                               SetGCPs()                              */
/************************************************************************/

CPLErr PDFDataset::SetGCPs( int nGCPCountIn, const GDAL_GCP *pasGCPListIn,
                            const char *pszGCPProjectionIn )
{
    const char* pszGEO_ENCODING =
        CPLGetConfigOption("GDAL_PDF_GEO_ENCODING", "ISO32000");
    if( nGCPCountIn != 4 && EQUAL(pszGEO_ENCODING, "ISO32000"))
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "PDF driver only supports writing 4 GCPs when "
                 "GDAL_PDF_GEO_ENCODING=ISO32000.");
        return CE_Failure;
    }
    
    /* Free previous GCPs */
    GDALDeinitGCPs( nGCPCount, pasGCPList );
    CPLFree( pasGCPList );
    
    /* Duplicate in GCPs */
    nGCPCount = nGCPCountIn;
    pasGCPList = GDALDuplicateGCPs(nGCPCount, pasGCPListIn);
    
    CPLFree(pszWKT);
    pszWKT = CPLStrdup(pszGCPProjectionIn);
    
    bProjDirty = TRUE;

    /* Reset NEATLINE if not explicitly set by the user */
    if (!bNeatLineDirty)
        SetMetadataItem("NEATLINE", NULL);

    return CE_None;
}

#endif // #if defined(HAVE_POPPLER) || defined(HAVE_PODOFO)

/************************************************************************/
/*                          GDALPDFOpen()                               */
/************************************************************************/

GDALDataset* GDALPDFOpen(
#if defined(HAVE_POPPLER) || defined(HAVE_PODOFO)
                         const char* pszFilename,
                         GDALAccess eAccess
#else
                         CPL_UNUSED const char* pszFilename,
                         CPL_UNUSED GDALAccess eAccess
#endif
                         )
{
#if defined(HAVE_POPPLER) || defined(HAVE_PODOFO)
    GDALOpenInfo oOpenInfo(pszFilename, eAccess);
    return PDFDataset::Open(&oOpenInfo);
#else
    return NULL;
#endif
}

/************************************************************************/
/*                       GDALPDFUnloadDriver()                          */
/************************************************************************/

static void GDALPDFUnloadDriver(CPL_UNUSED GDALDriver * poDriver)
{
#ifdef HAVE_POPPLER
    if( hGlobalParamsMutex != NULL )
        CPLDestroyMutex(hGlobalParamsMutex);
#endif
}

/************************************************************************/
/*                           PDFSanitizeLayerName()                     */
/************************************************************************/

CPLString PDFSanitizeLayerName(const char* pszName)
{
    CPLString osName;
    for(int i=0; pszName[i] != '\0'; i++)
    {
        if (pszName[i] == ' ' || pszName[i] == '.' || pszName[i] == ',')
            osName += "_";
        else if (pszName[i] != '"')
            osName += pszName[i];
    }
    return osName;
}

/************************************************************************/
/*                         GDALRegister_PDF()                           */
/************************************************************************/

void GDALRegister_PDF()

{
    GDALDriver  *poDriver;

    if (! GDAL_CHECK_VERSION("PDF driver"))
        return;

    if( GDALGetDriverByName( "PDF" ) == NULL )
    {
        poDriver = new GDALDriver();

        poDriver->SetDescription( "PDF" );
        poDriver->SetMetadataItem( GDAL_DCAP_RASTER, "YES" );
        poDriver->SetMetadataItem( GDAL_DCAP_VECTOR, "YES" );
        poDriver->SetMetadataItem( GDAL_DMD_LONGNAME,
                                   "Geospatial PDF" );
        poDriver->SetMetadataItem( GDAL_DMD_HELPTOPIC,
                                   "frmt_pdf.html" );
        poDriver->SetMetadataItem( GDAL_DMD_EXTENSION, "pdf" );
        poDriver->SetMetadataItem( GDAL_DMD_CREATIONDATATYPES,
                                   "Byte" );
#ifdef HAVE_POPPLER
        poDriver->SetMetadataItem( GDAL_DCAP_VIRTUALIO, "YES" );
        poDriver->SetMetadataItem( "HAVE_POPPLER", "YES" );
#endif // HAVE_POPPLER
#ifdef HAVE_PODOFO
        poDriver->SetMetadataItem( "HAVE_PODOFO", "YES" );
#endif // HAVE_PODOFO

        poDriver->SetMetadataItem( GDAL_DMD_CREATIONOPTIONLIST,
"<CreationOptionList>\n"
"   <Option name='COMPRESS' type='string-select' description='Compression method for raster data' default='DEFLATE'>\n"
"     <Value>NONE</Value>\n"
"     <Value>DEFLATE</Value>\n"
"     <Value>JPEG</Value>\n"
"     <Value>JPEG2000</Value>\n"
"   </Option>\n"
"   <Option name='STREAM_COMPRESS' type='string-select' description='Compression method for stream objects' default='DEFLATE'>\n"
"     <Value>NONE</Value>\n"
"     <Value>DEFLATE</Value>\n"
"   </Option>\n"
"   <Option name='GEO_ENCODING' type='string-select' description='Format of geo-encoding' default='ISO32000'>\n"
"     <Value>NONE</Value>\n"
"     <Value>ISO32000</Value>\n"
"     <Value>OGC_BP</Value>\n"
"     <Value>BOTH</Value>\n"
"   </Option>\n"
"   <Option name='NEATLINE' type='string' description='Neatline'/>\n"
"   <Option name='DPI' type='float' description='DPI' default='72'/>\n"
"   <Option name='PREDICTOR' type='int' description='Predictor Type (for DEFLATE compression)'/>\n"
"   <Option name='JPEG_QUALITY' type='int' description='JPEG quality 1-100' default='75'/>\n"
"   <Option name='JPEG2000_DRIVER' type='string'/>\n"
"   <Option name='TILED' type='boolean' description='Switch to tiled format' default='NO'/>\n"
"   <Option name='BLOCKXSIZE' type='int' description='Block Width'/>\n"
"   <Option name='BLOCKYSIZE' type='int' description='Block Height'/>\n"
"   <Option name='LAYER_NAME' type='string' description='Layer name for raster content'/>\n"
"   <Option name='CLIPPING_EXTENT' type='string' description='Clipping extent for main and extra rasters. Format: xmin,ymin,xmax,ymax'/>\n"
"   <Option name='EXTRA_RASTERS' type='string' description='List of extra (georeferenced) rasters.'/>\n"
"   <Option name='EXTRA_RASTERS_LAYER_NAME' type='string' description='List of layer names for the extra (georeferenced) rasters.'/>\n"
"   <Option name='EXTRA_STREAM' type='string' description='Extra data to insert into the page content stream'/>\n"
"   <Option name='EXTRA_IMAGES' type='string' description='List of image_file_name,x,y,scale[,link=some_url] (possibly repeated)'/>\n"
"   <Option name='EXTRA_LAYER_NAME' type='string' description='Layer name for extra content'/>\n"
"   <Option name='MARGIN' type='int' description='Margin around image in user units'/>\n"
"   <Option name='LEFT_MARGIN' type='int' description='Left margin in user units'/>\n"
"   <Option name='RIGHT_MARGIN' type='int' description='Right margin in user units'/>\n"
"   <Option name='TOP_MARGIN' type='int' description='Top margin in user units'/>\n"
"   <Option name='BOTTOM_MARGIN' type='int' description='Bottom margin in user units'/>\n"
"   <Option name='OGR_DATASOURCE' type='string' description='Name of OGR datasource to display on top of the raster layer'/>\n"
"   <Option name='OGR_DISPLAY_FIELD' type='string' description='Name of field to use as the display field in the feature tree'/>\n"
"   <Option name='OGR_DISPLAY_LAYER_NAMES' type='string' description='Comma separated list of OGR layer names to display in the feature tree'/>\n"
"   <Option name='OGR_WRITE_ATTRIBUTES' type='boolean' description='Whether to write attributes of OGR features' default='YES'/>\n"
"   <Option name='OGR_LINK_FIELD' type='string' description='Name of field to use as the URL field to make objects clickable.'/>\n"
"   <Option name='XMP' type='string' description='xml:XMP metadata'/>\n"
"   <Option name='WRITE_INFO' type='boolean' description='to control whether a Info block must be written' default='YES'/>\n"
"   <Option name='AUTHOR' type='string'/>\n"
"   <Option name='CREATOR' type='string'/>\n"
"   <Option name='CREATION_DATE' type='string'/>\n"
"   <Option name='KEYWORDS' type='string'/>\n"
"   <Option name='PRODUCER' type='string'/>\n"
"   <Option name='SUBJECT' type='string'/>\n"
"   <Option name='TITLE' type='string'/>\n"
"   <Option name='OFF_LAYERS' type='string' description='Comma separated list of layer names that should be initially hidden'/>\n"
"   <Option name='EXCLUSIVE_LAYERS' type='string' description='Comma separated list of layer names, such that only one of those layers can be ON at a time.'/>\n"
"   <Option name='JAVASCRIPT' type='string' description='Javascript script to embed and run at file opening'/>\n"
"   <Option name='JAVASCRIPT_FILE' type='string' description='Filename of the Javascript script to embed and run at file opening'/>\n"
"</CreationOptionList>\n" );

#if defined(HAVE_POPPLER) || defined(HAVE_PODOFO)
        poDriver->SetMetadataItem( GDAL_DMD_OPENOPTIONLIST, pszOpenOptionList );
        poDriver->pfnOpen = PDFDataset::Open;
        poDriver->pfnIdentify = PDFDataset::Identify;
        poDriver->SetMetadataItem( GDAL_DMD_SUBDATASETS, "YES" );
#endif // HAVE_POPPLER || HAVE_PODOFO

        poDriver->pfnCreateCopy = GDALPDFCreateCopy;
        poDriver->pfnCreate = PDFWritableVectorDataset::Create;
        poDriver->pfnUnloadDriver = GDALPDFUnloadDriver;

        GetGDALDriverManager()->RegisterDriver( poDriver );
    }
}
