/******************************************************************************
 *
 * Project:  PDF driver
 * Purpose:  GDALDataset driver for PDF dataset.
 * Author:   Even Rouault, <even dot rouault at spatialys.com>
 *
 ******************************************************************************
 * Copyright (c) 2012-2019, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "gdal_pdf.h"
#include "pdfcreatecopy.h"

#include "cpl_vsi_virtual.h"
#include "cpl_conv.h"
#include "cpl_error.h"
#include "ogr_spatialref.h"
#include "ogr_geometry.h"
#include "memdataset.h"
#include "vrtdataset.h"

#include "pdfobject.h"

#include <cmath>
#include <algorithm>
#include <utility>
#include <vector>

// #define HACK_TO_GENERATE_OCMD can be set to produce a (single layer)
// non-structured vector PDF with a OCMD (Optional Content Group Membership
// Dictionary) similar to test case of https://github.com/OSGeo/gdal/issues/8372
// like with "ogr2ogr poly.pdf poly.shp -dsco STREAM_COMPRESS=NONE -limit 1"

/************************************************************************/
/*                        GDALPDFBaseWriter()                           */
/************************************************************************/

GDALPDFBaseWriter::GDALPDFBaseWriter(VSILFILE *fp) : m_fp(fp)
{
}

/************************************************************************/
/*                       ~GDALPDFBaseWriter()                           */
/************************************************************************/

GDALPDFBaseWriter::~GDALPDFBaseWriter()
{
    Close();
}

/************************************************************************/
/*                              ~Close()                                */
/************************************************************************/

void GDALPDFBaseWriter::Close()
{
    if (m_fp)
    {
        VSIFCloseL(m_fp);
        m_fp = nullptr;
    }
}

/************************************************************************/
/*                           GDALPDFUpdateWriter()                      */
/************************************************************************/

GDALPDFUpdateWriter::GDALPDFUpdateWriter(VSILFILE *fp) : GDALPDFBaseWriter(fp)
{
}

/************************************************************************/
/*                          ~GDALPDFUpdateWriter()                      */
/************************************************************************/

GDALPDFUpdateWriter::~GDALPDFUpdateWriter()
{
    Close();
}

/************************************************************************/
/*                              ~Close()                                */
/************************************************************************/

void GDALPDFUpdateWriter::Close()
{
    if (m_fp)
    {
        CPLAssert(!m_bInWriteObj);
        if (m_bUpdateNeeded)
        {
            WriteXRefTableAndTrailer(true, m_nLastStartXRef);
        }
    }
    GDALPDFBaseWriter::Close();
}

/************************************************************************/
/*                          StartNewDoc()                               */
/************************************************************************/

void GDALPDFBaseWriter::StartNewDoc()
{
    VSIFPrintfL(m_fp, "%%PDF-1.6\n");

    // See PDF 1.7 reference, page 92. Write 4 non-ASCII bytes to indicate
    // that the content will be binary.
    VSIFPrintfL(m_fp, "%%%c%c%c%c\n", 0xFF, 0xFF, 0xFF, 0xFF);

    m_nPageResourceId = AllocNewObject();
    m_nCatalogId = AllocNewObject();
}

/************************************************************************/
/*                         GDALPDFWriter()                              */
/************************************************************************/

GDALPDFWriter::GDALPDFWriter(VSILFILE *fpIn) : GDALPDFBaseWriter(fpIn)
{
    StartNewDoc();
}

/************************************************************************/
/*                         ~GDALPDFWriter()                             */
/************************************************************************/

GDALPDFWriter::~GDALPDFWriter()
{
    Close();
}

/************************************************************************/
/*                          ParseIndirectRef()                          */
/************************************************************************/

static int ParseIndirectRef(const char *pszStr, GDALPDFObjectNum &nNum,
                            int &nGen)
{
    while (*pszStr == ' ')
        pszStr++;

    nNum = atoi(pszStr);
    while (*pszStr >= '0' && *pszStr <= '9')
        pszStr++;
    if (*pszStr != ' ')
        return FALSE;

    while (*pszStr == ' ')
        pszStr++;

    nGen = atoi(pszStr);
    while (*pszStr >= '0' && *pszStr <= '9')
        pszStr++;
    if (*pszStr != ' ')
        return FALSE;

    while (*pszStr == ' ')
        pszStr++;

    return *pszStr == 'R';
}

/************************************************************************/
/*                       ParseTrailerAndXRef()                          */
/************************************************************************/

int GDALPDFUpdateWriter::ParseTrailerAndXRef()
{
    VSIFSeekL(m_fp, 0, SEEK_END);
    char szBuf[1024 + 1];
    vsi_l_offset nOffset = VSIFTellL(m_fp);

    if (nOffset > 128)
        nOffset -= 128;
    else
        nOffset = 0;

    /* Find startxref section */
    VSIFSeekL(m_fp, nOffset, SEEK_SET);
    int nRead = static_cast<int>(VSIFReadL(szBuf, 1, 128, m_fp));
    szBuf[nRead] = 0;
    if (nRead < 9)
        return FALSE;

    const char *pszStartXRef = nullptr;
    int i;
    for (i = nRead - 9; i >= 0; i--)
    {
        if (STARTS_WITH(szBuf + i, "startxref"))
        {
            pszStartXRef = szBuf + i;
            break;
        }
    }
    if (pszStartXRef == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Cannot find startxref");
        return FALSE;
    }
    pszStartXRef += 9;
    while (*pszStartXRef == '\r' || *pszStartXRef == '\n')
        pszStartXRef++;
    if (*pszStartXRef == '\0')
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Cannot find startxref");
        return FALSE;
    }

    m_nLastStartXRef = CPLScanUIntBig(pszStartXRef, 16);

    /* Skip to beginning of xref section */
    VSIFSeekL(m_fp, m_nLastStartXRef, SEEK_SET);

    /* And skip to trailer */
    const char *pszLine = nullptr;
    while ((pszLine = CPLReadLineL(m_fp)) != nullptr)
    {
        if (STARTS_WITH(pszLine, "trailer"))
            break;
    }

    if (pszLine == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Cannot find trailer");
        return FALSE;
    }

    /* Read trailer content */
    nRead = static_cast<int>(VSIFReadL(szBuf, 1, 1024, m_fp));
    szBuf[nRead] = 0;

    /* Find XRef size */
    const char *pszSize = strstr(szBuf, "/Size");
    if (pszSize == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Cannot find trailer /Size");
        return FALSE;
    }
    pszSize += 5;
    while (*pszSize == ' ')
        pszSize++;
    m_nLastXRefSize = atoi(pszSize);

    /* Find Root object */
    const char *pszRoot = strstr(szBuf, "/Root");
    if (pszRoot == nullptr)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Cannot find trailer /Root");
        return FALSE;
    }
    pszRoot += 5;
    while (*pszRoot == ' ')
        pszRoot++;

    if (!ParseIndirectRef(pszRoot, m_nCatalogId, m_nCatalogGen))
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Cannot parse trailer /Root");
        return FALSE;
    }

    /* Find Info object */
    const char *pszInfo = strstr(szBuf, "/Info");
    if (pszInfo != nullptr)
    {
        pszInfo += 5;
        while (*pszInfo == ' ')
            pszInfo++;

        if (!ParseIndirectRef(pszInfo, m_nInfoId, m_nInfoGen))
        {
            CPLError(CE_Failure, CPLE_AppDefined, "Cannot parse trailer /Info");
            m_nInfoId = 0;
            m_nInfoGen = 0;
        }
    }

    VSIFSeekL(m_fp, 0, SEEK_END);

    return TRUE;
}

/************************************************************************/
/*                              Close()                                 */
/************************************************************************/

void GDALPDFWriter::Close()
{
    if (m_fp)
    {
        CPLAssert(!m_bInWriteObj);
        if (m_nPageResourceId.toBool())
        {
            WritePages();
            WriteXRefTableAndTrailer(false, 0);
        }
    }
    GDALPDFBaseWriter::Close();
}

/************************************************************************/
/*                           UpdateProj()                               */
/************************************************************************/

void GDALPDFUpdateWriter::UpdateProj(GDALDataset *poSrcDS, double dfDPI,
                                     GDALPDFDictionaryRW *poPageDict,
                                     const GDALPDFObjectNum &nPageId,
                                     int nPageGen)
{
    m_bUpdateNeeded = true;
    if (static_cast<int>(m_asXRefEntries.size()) < m_nLastXRefSize - 1)
        m_asXRefEntries.resize(m_nLastXRefSize - 1);

    GDALPDFObjectNum nViewportId;
    GDALPDFObjectNum nLGIDictId;

    CPLAssert(nPageId.toBool());
    CPLAssert(poPageDict != nullptr);

    PDFMargins sMargins;

    const char *pszGEO_ENCODING =
        CPLGetConfigOption("GDAL_PDF_GEO_ENCODING", "ISO32000");
    if (EQUAL(pszGEO_ENCODING, "ISO32000") || EQUAL(pszGEO_ENCODING, "BOTH"))
        nViewportId = WriteSRS_ISO32000(poSrcDS, dfDPI * USER_UNIT_IN_INCH,
                                        nullptr, &sMargins, TRUE);
    if (EQUAL(pszGEO_ENCODING, "OGC_BP") || EQUAL(pszGEO_ENCODING, "BOTH"))
        nLGIDictId = WriteSRS_OGC_BP(poSrcDS, dfDPI * USER_UNIT_IN_INCH,
                                     nullptr, &sMargins);

#ifdef invalidate_xref_entry
    GDALPDFObject *poVP = poPageDict->Get("VP");
    if (poVP)
    {
        if (poVP->GetType() == PDFObjectType_Array &&
            poVP->GetArray()->GetLength() == 1)
            poVP = poVP->GetArray()->Get(0);

        int nVPId = poVP->GetRefNum();
        if (nVPId)
        {
            m_asXRefEntries[nVPId - 1].bFree = TRUE;
            m_asXRefEntries[nVPId - 1].nGen++;
        }
    }
#endif

    poPageDict->Remove("VP");
    poPageDict->Remove("LGIDict");

    if (nViewportId.toBool())
    {
        poPageDict->Add("VP", &((new GDALPDFArrayRW())->Add(nViewportId, 0)));
    }

    if (nLGIDictId.toBool())
    {
        poPageDict->Add("LGIDict", nLGIDictId, 0);
    }

    StartObj(nPageId, nPageGen);
    VSIFPrintfL(m_fp, "%s\n", poPageDict->Serialize().c_str());
    EndObj();
}

/************************************************************************/
/*                           UpdateInfo()                               */
/************************************************************************/

void GDALPDFUpdateWriter::UpdateInfo(GDALDataset *poSrcDS)
{
    m_bUpdateNeeded = true;
    if (static_cast<int>(m_asXRefEntries.size()) < m_nLastXRefSize - 1)
        m_asXRefEntries.resize(m_nLastXRefSize - 1);

    auto nNewInfoId = SetInfo(poSrcDS, nullptr);
    /* Write empty info, because podofo driver will find the dangling info
     * instead */
    if (!nNewInfoId.toBool() && m_nInfoId.toBool())
    {
#ifdef invalidate_xref_entry
        m_asXRefEntries[m_nInfoId.toInt() - 1].bFree = TRUE;
        m_asXRefEntries[m_nInfoId.toInt() - 1].nGen++;
#else
        StartObj(m_nInfoId, m_nInfoGen);
        VSIFPrintfL(m_fp, "<< >>\n");
        EndObj();
#endif
    }
}

/************************************************************************/
/*                           UpdateXMP()                                */
/************************************************************************/

void GDALPDFUpdateWriter::UpdateXMP(GDALDataset *poSrcDS,
                                    GDALPDFDictionaryRW *poCatalogDict)
{
    m_bUpdateNeeded = true;
    if (static_cast<int>(m_asXRefEntries.size()) < m_nLastXRefSize - 1)
        m_asXRefEntries.resize(m_nLastXRefSize - 1);

    CPLAssert(m_nCatalogId.toBool());
    CPLAssert(poCatalogDict != nullptr);

    GDALPDFObject *poMetadata = poCatalogDict->Get("Metadata");
    if (poMetadata)
    {
        m_nXMPId = poMetadata->GetRefNum();
        m_nXMPGen = poMetadata->GetRefGen();
    }

    poCatalogDict->Remove("Metadata");
    auto nNewXMPId = SetXMP(poSrcDS, nullptr);

    /* Write empty metadata, because podofo driver will find the dangling info
     * instead */
    if (!nNewXMPId.toBool() && m_nXMPId.toBool())
    {
        StartObj(m_nXMPId, m_nXMPGen);
        VSIFPrintfL(m_fp, "<< >>\n");
        EndObj();
    }

    if (m_nXMPId.toBool())
        poCatalogDict->Add("Metadata", m_nXMPId, 0);

    StartObj(m_nCatalogId, m_nCatalogGen);
    VSIFPrintfL(m_fp, "%s\n", poCatalogDict->Serialize().c_str());
    EndObj();
}

/************************************************************************/
/*                           AllocNewObject()                           */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::AllocNewObject()
{
    m_asXRefEntries.push_back(GDALXRefEntry());
    return GDALPDFObjectNum(static_cast<int>(m_asXRefEntries.size()));
}

/************************************************************************/
/*                        WriteXRefTableAndTrailer()                    */
/************************************************************************/

void GDALPDFBaseWriter::WriteXRefTableAndTrailer(bool bUpdate,
                                                 vsi_l_offset nLastStartXRef)
{
    vsi_l_offset nOffsetXREF = VSIFTellL(m_fp);
    VSIFPrintfL(m_fp, "xref\n");

    char buffer[16];
    if (bUpdate)
    {
        VSIFPrintfL(m_fp, "0 1\n");
        VSIFPrintfL(m_fp, "0000000000 65535 f \n");
        for (size_t i = 0; i < m_asXRefEntries.size();)
        {
            if (m_asXRefEntries[i].nOffset != 0 || m_asXRefEntries[i].bFree)
            {
                /* Find number of consecutive objects */
                size_t nCount = 1;
                while (i + nCount < m_asXRefEntries.size() &&
                       (m_asXRefEntries[i + nCount].nOffset != 0 ||
                        m_asXRefEntries[i + nCount].bFree))
                    nCount++;

                VSIFPrintfL(m_fp, "%d %d\n", static_cast<int>(i) + 1,
                            static_cast<int>(nCount));
                size_t iEnd = i + nCount;
                for (; i < iEnd; i++)
                {
                    snprintf(buffer, sizeof(buffer),
                             "%010" CPL_FRMT_GB_WITHOUT_PREFIX "u",
                             m_asXRefEntries[i].nOffset);
                    VSIFPrintfL(m_fp, "%s %05d %c \n", buffer,
                                m_asXRefEntries[i].nGen,
                                m_asXRefEntries[i].bFree ? 'f' : 'n');
                }
            }
            else
            {
                i++;
            }
        }
    }
    else
    {
        VSIFPrintfL(m_fp, "%d %d\n", 0,
                    static_cast<int>(m_asXRefEntries.size()) + 1);
        VSIFPrintfL(m_fp, "0000000000 65535 f \n");
        for (size_t i = 0; i < m_asXRefEntries.size(); i++)
        {
            snprintf(buffer, sizeof(buffer),
                     "%010" CPL_FRMT_GB_WITHOUT_PREFIX "u",
                     m_asXRefEntries[i].nOffset);
            VSIFPrintfL(m_fp, "%s %05d n \n", buffer, m_asXRefEntries[i].nGen);
        }
    }

    VSIFPrintfL(m_fp, "trailer\n");
    GDALPDFDictionaryRW oDict;
    oDict.Add("Size", static_cast<int>(m_asXRefEntries.size()) + 1)
        .Add("Root", m_nCatalogId, m_nCatalogGen);
    if (m_nInfoId.toBool())
        oDict.Add("Info", m_nInfoId, m_nInfoGen);
    if (nLastStartXRef)
        oDict.Add("Prev", static_cast<double>(nLastStartXRef));
    VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());

    VSIFPrintfL(m_fp,
                "startxref\n" CPL_FRMT_GUIB "\n"
                "%%%%EOF\n",
                nOffsetXREF);
}

/************************************************************************/
/*                              StartObj()                              */
/************************************************************************/

void GDALPDFBaseWriter::StartObj(const GDALPDFObjectNum &nObjectId, int nGen)
{
    CPLAssert(!m_bInWriteObj);
    CPLAssert(nObjectId.toInt() - 1 < static_cast<int>(m_asXRefEntries.size()));
    CPLAssert(m_asXRefEntries[nObjectId.toInt() - 1].nOffset == 0);
    m_asXRefEntries[nObjectId.toInt() - 1].nOffset = VSIFTellL(m_fp);
    m_asXRefEntries[nObjectId.toInt() - 1].nGen = nGen;
    VSIFPrintfL(m_fp, "%d %d obj\n", nObjectId.toInt(), nGen);
    m_bInWriteObj = true;
}

/************************************************************************/
/*                               EndObj()                               */
/************************************************************************/

void GDALPDFBaseWriter::EndObj()
{
    CPLAssert(m_bInWriteObj);
    CPLAssert(!m_fpBack);
    VSIFPrintfL(m_fp, "endobj\n");
    m_bInWriteObj = false;
}

/************************************************************************/
/*                         StartObjWithStream()                         */
/************************************************************************/

void GDALPDFBaseWriter::StartObjWithStream(const GDALPDFObjectNum &nObjectId,
                                           GDALPDFDictionaryRW &oDict,
                                           bool bDeflate)
{
    CPLAssert(!m_nContentLengthId.toBool());
    CPLAssert(!m_fpGZip);
    CPLAssert(!m_fpBack);
    CPLAssert(m_nStreamStart == 0);

    m_nContentLengthId = AllocNewObject();

    StartObj(nObjectId);
    {
        oDict.Add("Length", m_nContentLengthId, 0);
        if (bDeflate)
        {
            oDict.Add("Filter", GDALPDFObjectRW::CreateName("FlateDecode"));
        }
        VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());
    }

    /* -------------------------------------------------------------- */
    /*  Write content stream                                          */
    /* -------------------------------------------------------------- */
    VSIFPrintfL(m_fp, "stream\n");
    m_nStreamStart = VSIFTellL(m_fp);

    m_fpGZip = nullptr;
    m_fpBack = m_fp;
    if (bDeflate)
    {
        m_fpGZip = VSICreateGZipWritable(m_fp, TRUE, FALSE);
        m_fp = m_fpGZip;
    }
}

/************************************************************************/
/*                          EndObjWithStream()                          */
/************************************************************************/

void GDALPDFBaseWriter::EndObjWithStream()
{
    if (m_fpGZip)
        VSIFCloseL(m_fpGZip);
    m_fp = m_fpBack;
    m_fpBack = nullptr;

    vsi_l_offset nStreamEnd = VSIFTellL(m_fp);
    if (m_fpGZip)
        VSIFPrintfL(m_fp, "\n");
    m_fpGZip = nullptr;
    VSIFPrintfL(m_fp, "endstream\n");
    EndObj();

    StartObj(m_nContentLengthId);
    VSIFPrintfL(m_fp, "   %ld\n",
                static_cast<long>(nStreamEnd - m_nStreamStart));
    EndObj();

    m_nContentLengthId = GDALPDFObjectNum();
    m_nStreamStart = 0;
}

/************************************************************************/
/*                         GDALPDFFind4Corners()                        */
/************************************************************************/

static void GDALPDFFind4Corners(const GDAL_GCP *pasGCPList, int &iUL, int &iUR,
                                int &iLR, int &iLL)
{
    double dfMeanX = 0.0;
    double dfMeanY = 0.0;
    int i;

    iUL = 0;
    iUR = 0;
    iLR = 0;
    iLL = 0;

    for (i = 0; i < 4; i++)
    {
        dfMeanX += pasGCPList[i].dfGCPPixel;
        dfMeanY += pasGCPList[i].dfGCPLine;
    }
    dfMeanX /= 4;
    dfMeanY /= 4;

    for (i = 0; i < 4; i++)
    {
        if (pasGCPList[i].dfGCPPixel < dfMeanX &&
            pasGCPList[i].dfGCPLine < dfMeanY)
            iUL = i;

        else if (pasGCPList[i].dfGCPPixel > dfMeanX &&
                 pasGCPList[i].dfGCPLine < dfMeanY)
            iUR = i;

        else if (pasGCPList[i].dfGCPPixel > dfMeanX &&
                 pasGCPList[i].dfGCPLine > dfMeanY)
            iLR = i;

        else if (pasGCPList[i].dfGCPPixel < dfMeanX &&
                 pasGCPList[i].dfGCPLine > dfMeanY)
            iLL = i;
    }
}

/************************************************************************/
/*                         WriteSRS_ISO32000()                          */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::WriteSRS_ISO32000(GDALDataset *poSrcDS,
                                                      double dfUserUnit,
                                                      const char *pszNEATLINE,
                                                      PDFMargins *psMargins,
                                                      int bWriteViewport)
{
    int nWidth = poSrcDS->GetRasterXSize();
    int nHeight = poSrcDS->GetRasterYSize();
    const char *pszWKT = poSrcDS->GetProjectionRef();
    double adfGeoTransform[6];

    int bHasGT = (poSrcDS->GetGeoTransform(adfGeoTransform) == CE_None);
    const GDAL_GCP *pasGCPList =
        (poSrcDS->GetGCPCount() == 4) ? poSrcDS->GetGCPs() : nullptr;
    if (pasGCPList != nullptr)
        pszWKT = poSrcDS->GetGCPProjection();

    if (!bHasGT && pasGCPList == nullptr)
        return GDALPDFObjectNum();

    if (pszWKT == nullptr || EQUAL(pszWKT, ""))
        return GDALPDFObjectNum();

    double adfGPTS[8];

    double dfULPixel = 0;
    double dfULLine = 0;
    double dfLRPixel = nWidth;
    double dfLRLine = nHeight;

    std::vector<gdal::GCP> asNeatLineGCPs(4);
    if (pszNEATLINE == nullptr)
        pszNEATLINE = poSrcDS->GetMetadataItem("NEATLINE");
    if (bHasGT && pszNEATLINE != nullptr && pszNEATLINE[0] != '\0')
    {
        OGRGeometry *poGeom = nullptr;
        OGRGeometryFactory::createFromWkt(pszNEATLINE, nullptr, &poGeom);
        if (poGeom != nullptr &&
            wkbFlatten(poGeom->getGeometryType()) == wkbPolygon)
        {
            OGRLineString *poLS = poGeom->toPolygon()->getExteriorRing();
            double adfGeoTransformInv[6];
            if (poLS != nullptr && poLS->getNumPoints() == 5 &&
                GDALInvGeoTransform(adfGeoTransform, adfGeoTransformInv))
            {
                for (int i = 0; i < 4; i++)
                {
                    const double X = poLS->getX(i);
                    const double Y = poLS->getY(i);
                    asNeatLineGCPs[i].X() = X;
                    asNeatLineGCPs[i].Y() = Y;
                    const double x = adfGeoTransformInv[0] +
                                     X * adfGeoTransformInv[1] +
                                     Y * adfGeoTransformInv[2];
                    const double y = adfGeoTransformInv[3] +
                                     X * adfGeoTransformInv[4] +
                                     Y * adfGeoTransformInv[5];
                    asNeatLineGCPs[i].Pixel() = x;
                    asNeatLineGCPs[i].Line() = y;
                }

                int iUL = 0;
                int iUR = 0;
                int iLR = 0;
                int iLL = 0;
                GDALPDFFind4Corners(gdal::GCP::c_ptr(asNeatLineGCPs), iUL, iUR,
                                    iLR, iLL);

                if (fabs(asNeatLineGCPs[iUL].Pixel() -
                         asNeatLineGCPs[iLL].Pixel()) > .5 ||
                    fabs(asNeatLineGCPs[iUR].Pixel() -
                         asNeatLineGCPs[iLR].Pixel()) > .5 ||
                    fabs(asNeatLineGCPs[iUL].Line() -
                         asNeatLineGCPs[iUR].Line()) > .5 ||
                    fabs(asNeatLineGCPs[iLL].Line() -
                         asNeatLineGCPs[iLR].Line()) > .5)
                {
                    CPLError(CE_Warning, CPLE_NotSupported,
                             "Neatline coordinates should form a rectangle in "
                             "pixel space. Ignoring it");
                    for (int i = 0; i < 4; i++)
                    {
                        CPLDebug("PDF", "pixel[%d] = %.1f, line[%d] = %.1f", i,
                                 asNeatLineGCPs[i].Pixel(), i,
                                 asNeatLineGCPs[i].Line());
                    }
                }
                else
                {
                    pasGCPList = gdal::GCP::c_ptr(asNeatLineGCPs);
                }
            }
        }
        delete poGeom;
    }

    if (pasGCPList)
    {
        int iUL = 0;
        int iUR = 0;
        int iLR = 0;
        int iLL = 0;
        GDALPDFFind4Corners(pasGCPList, iUL, iUR, iLR, iLL);

        if (fabs(pasGCPList[iUL].dfGCPPixel - pasGCPList[iLL].dfGCPPixel) >
                .5 ||
            fabs(pasGCPList[iUR].dfGCPPixel - pasGCPList[iLR].dfGCPPixel) >
                .5 ||
            fabs(pasGCPList[iUL].dfGCPLine - pasGCPList[iUR].dfGCPLine) > .5 ||
            fabs(pasGCPList[iLL].dfGCPLine - pasGCPList[iLR].dfGCPLine) > .5)
        {
            CPLError(CE_Failure, CPLE_NotSupported,
                     "GCPs should form a rectangle in pixel space");
            return GDALPDFObjectNum();
        }

        dfULPixel = pasGCPList[iUL].dfGCPPixel;
        dfULLine = pasGCPList[iUL].dfGCPLine;
        dfLRPixel = pasGCPList[iLR].dfGCPPixel;
        dfLRLine = pasGCPList[iLR].dfGCPLine;

        /* Upper-left */
        adfGPTS[0] = pasGCPList[iUL].dfGCPX;
        adfGPTS[1] = pasGCPList[iUL].dfGCPY;

        /* Lower-left */
        adfGPTS[2] = pasGCPList[iLL].dfGCPX;
        adfGPTS[3] = pasGCPList[iLL].dfGCPY;

        /* Lower-right */
        adfGPTS[4] = pasGCPList[iLR].dfGCPX;
        adfGPTS[5] = pasGCPList[iLR].dfGCPY;

        /* Upper-right */
        adfGPTS[6] = pasGCPList[iUR].dfGCPX;
        adfGPTS[7] = pasGCPList[iUR].dfGCPY;
    }
    else
    {
        /* Upper-left */
        adfGPTS[0] = APPLY_GT_X(adfGeoTransform, 0, 0);
        adfGPTS[1] = APPLY_GT_Y(adfGeoTransform, 0, 0);

        /* Lower-left */
        adfGPTS[2] = APPLY_GT_X(adfGeoTransform, 0, nHeight);
        adfGPTS[3] = APPLY_GT_Y(adfGeoTransform, 0, nHeight);

        /* Lower-right */
        adfGPTS[4] = APPLY_GT_X(adfGeoTransform, nWidth, nHeight);
        adfGPTS[5] = APPLY_GT_Y(adfGeoTransform, nWidth, nHeight);

        /* Upper-right */
        adfGPTS[6] = APPLY_GT_X(adfGeoTransform, nWidth, 0);
        adfGPTS[7] = APPLY_GT_Y(adfGeoTransform, nWidth, 0);
    }

    OGRSpatialReferenceH hSRS = OSRNewSpatialReference(pszWKT);
    if (hSRS == nullptr)
        return GDALPDFObjectNum();
    OSRSetAxisMappingStrategy(hSRS, OAMS_TRADITIONAL_GIS_ORDER);
    OGRSpatialReferenceH hSRSGeog = OSRCloneGeogCS(hSRS);
    if (hSRSGeog == nullptr)
    {
        OSRDestroySpatialReference(hSRS);
        return GDALPDFObjectNum();
    }
    OSRSetAxisMappingStrategy(hSRSGeog, OAMS_TRADITIONAL_GIS_ORDER);
    OGRCoordinateTransformationH hCT =
        OCTNewCoordinateTransformation(hSRS, hSRSGeog);
    if (hCT == nullptr)
    {
        OSRDestroySpatialReference(hSRS);
        OSRDestroySpatialReference(hSRSGeog);
        return GDALPDFObjectNum();
    }

    int bSuccess = TRUE;

    bSuccess &= (OCTTransform(hCT, 1, adfGPTS + 0, adfGPTS + 1, nullptr) == 1);
    bSuccess &= (OCTTransform(hCT, 1, adfGPTS + 2, adfGPTS + 3, nullptr) == 1);
    bSuccess &= (OCTTransform(hCT, 1, adfGPTS + 4, adfGPTS + 5, nullptr) == 1);
    bSuccess &= (OCTTransform(hCT, 1, adfGPTS + 6, adfGPTS + 7, nullptr) == 1);

    if (!bSuccess)
    {
        OSRDestroySpatialReference(hSRS);
        OSRDestroySpatialReference(hSRSGeog);
        OCTDestroyCoordinateTransformation(hCT);
        return GDALPDFObjectNum();
    }

    const char *pszAuthorityCode = OSRGetAuthorityCode(hSRS, nullptr);
    const char *pszAuthorityName = OSRGetAuthorityName(hSRS, nullptr);
    int nEPSGCode = 0;
    if (pszAuthorityName != nullptr && pszAuthorityCode != nullptr &&
        (EQUAL(pszAuthorityName, "EPSG") ||
         (EQUAL(pszAuthorityName, "ESRI") &&
          CPLTestBool(
              CPLGetConfigOption("GDAL_PDF_WRITE_ESRI_CODE_AS_EPSG", "NO")))))
    {
        nEPSGCode = atoi(pszAuthorityCode);
    }

    int bIsGeographic = OSRIsGeographic(hSRS);

    OSRMorphToESRI(hSRS);
    char *pszESRIWKT = nullptr;
    OSRExportToWkt(hSRS, &pszESRIWKT);

    OSRDestroySpatialReference(hSRS);
    OSRDestroySpatialReference(hSRSGeog);
    OCTDestroyCoordinateTransformation(hCT);
    hSRS = nullptr;
    hSRSGeog = nullptr;
    hCT = nullptr;

    if (pszESRIWKT == nullptr)
        return GDALPDFObjectNum();

    auto nViewportId = (bWriteViewport) ? AllocNewObject() : GDALPDFObjectNum();
    auto nMeasureId = AllocNewObject();
    auto nGCSId = AllocNewObject();

    if (nViewportId.toBool())
    {
        StartObj(nViewportId);
        GDALPDFDictionaryRW oViewPortDict;
        oViewPortDict.Add("Type", GDALPDFObjectRW::CreateName("Viewport"))
            .Add("Name", "Layer")
            .Add("BBox", &((new GDALPDFArrayRW())
                               ->Add(dfULPixel / dfUserUnit + psMargins->nLeft)
                               .Add((nHeight - dfLRLine) / dfUserUnit +
                                    psMargins->nBottom)
                               .Add(dfLRPixel / dfUserUnit + psMargins->nLeft)
                               .Add((nHeight - dfULLine) / dfUserUnit +
                                    psMargins->nBottom)))
            .Add("Measure", nMeasureId, 0);
        VSIFPrintfL(m_fp, "%s\n", oViewPortDict.Serialize().c_str());
        EndObj();
    }

    StartObj(nMeasureId);
    GDALPDFDictionaryRW oMeasureDict;
    oMeasureDict.Add("Type", GDALPDFObjectRW::CreateName("Measure"))
        .Add("Subtype", GDALPDFObjectRW::CreateName("GEO"))
        .Add("Bounds", &((new GDALPDFArrayRW())
                             ->Add(0)
                             .Add(1)
                             .Add(0)
                             .Add(0)
                             .Add(1)
                             .Add(0)
                             .Add(1)
                             .Add(1)))
        .Add("GPTS", &((new GDALPDFArrayRW())
                           ->Add(adfGPTS[1])
                           .Add(adfGPTS[0])
                           .Add(adfGPTS[3])
                           .Add(adfGPTS[2])
                           .Add(adfGPTS[5])
                           .Add(adfGPTS[4])
                           .Add(adfGPTS[7])
                           .Add(adfGPTS[6])))
        .Add("LPTS", &((new GDALPDFArrayRW())
                           ->Add(0)
                           .Add(1)
                           .Add(0)
                           .Add(0)
                           .Add(1)
                           .Add(0)
                           .Add(1)
                           .Add(1)))
        .Add("GCS", nGCSId, 0);
    VSIFPrintfL(m_fp, "%s\n", oMeasureDict.Serialize().c_str());
    EndObj();

    StartObj(nGCSId);
    GDALPDFDictionaryRW oGCSDict;
    oGCSDict
        .Add("Type",
             GDALPDFObjectRW::CreateName(bIsGeographic ? "GEOGCS" : "PROJCS"))
        .Add("WKT", pszESRIWKT);
    if (nEPSGCode)
        oGCSDict.Add("EPSG", nEPSGCode);
    VSIFPrintfL(m_fp, "%s\n", oGCSDict.Serialize().c_str());
    EndObj();

    CPLFree(pszESRIWKT);

    return nViewportId.toBool() ? nViewportId : nMeasureId;
}

/************************************************************************/
/*                     GDALPDFBuildOGC_BP_Datum()                       */
/************************************************************************/

static GDALPDFObject *GDALPDFBuildOGC_BP_Datum(const OGRSpatialReference *poSRS)
{
    const OGR_SRSNode *poDatumNode = poSRS->GetAttrNode("DATUM");
    const char *pszDatumDescription = nullptr;
    if (poDatumNode && poDatumNode->GetChildCount() > 0)
        pszDatumDescription = poDatumNode->GetChild(0)->GetValue();

    GDALPDFObjectRW *poPDFDatum = nullptr;

    if (pszDatumDescription)
    {
        double dfSemiMajor = poSRS->GetSemiMajor();
        double dfInvFlattening = poSRS->GetInvFlattening();
        int nEPSGDatum = -1;
        const char *pszAuthority = poSRS->GetAuthorityName("DATUM");
        if (pszAuthority != nullptr && EQUAL(pszAuthority, "EPSG"))
            nEPSGDatum = atoi(poSRS->GetAuthorityCode("DATUM"));

        if (EQUAL(pszDatumDescription, SRS_DN_WGS84) || nEPSGDatum == 6326)
            poPDFDatum = GDALPDFObjectRW::CreateString("WGE");
        else if (EQUAL(pszDatumDescription, SRS_DN_NAD27) || nEPSGDatum == 6267)
            poPDFDatum = GDALPDFObjectRW::CreateString("NAS");
        else if (EQUAL(pszDatumDescription, SRS_DN_NAD83) || nEPSGDatum == 6269)
            poPDFDatum = GDALPDFObjectRW::CreateString("NAR");
        else if (nEPSGDatum == 6135)
            poPDFDatum = GDALPDFObjectRW::CreateString("OHA-M");
        else
        {
            CPLDebug("PDF",
                     "Unhandled datum name (%s). Write datum parameters then.",
                     pszDatumDescription);

            GDALPDFDictionaryRW *poPDFDatumDict = new GDALPDFDictionaryRW();
            poPDFDatum = GDALPDFObjectRW::CreateDictionary(poPDFDatumDict);

            const OGR_SRSNode *poSpheroidNode = poSRS->GetAttrNode("SPHEROID");
            if (poSpheroidNode && poSpheroidNode->GetChildCount() >= 3)
            {
                poPDFDatumDict->Add("Description", pszDatumDescription);

#ifdef disabled_because_terrago_toolbar_does_not_like_it
                const char *pszEllipsoidCode = NULL;
                if (std::abs(dfSemiMajor - 6378249.145) < 0.01 &&
                    std::abs(dfInvFlattening - 293.465) < 0.0001)
                {
                    pszEllipsoidCode = "CD"; /* Clark 1880 */
                }
                else if (std::abs(dfSemiMajor - 6378245.0) < 0.01 x &&
                         std::abs(dfInvFlattening - 298.3) < 0.0001)
                {
                    pszEllipsoidCode = "KA"; /* Krassovsky */
                }
                else if (std::abs(dfSemiMajor - 6378388.0) < 0.01 &&
                         std::abs(dfInvFlattening - 297.0) < 0.0001)
                {
                    pszEllipsoidCode = "IN"; /* International 1924 */
                }
                else if (std::abs(dfSemiMajor - 6378160.0) < 0.01 &&
                         std::abs(dfInvFlattening - 298.25) < 0.0001)
                {
                    pszEllipsoidCode = "AN"; /* Australian */
                }
                else if (std::abs(dfSemiMajor - 6377397.155) < 0.01 &&
                         std::abs(dfInvFlattening - 299.1528128) < 0.0001)
                {
                    pszEllipsoidCode = "BR"; /* Bessel 1841 */
                }
                else if (std::abs(dfSemiMajor - 6377483.865) < 0.01 &&
                         std::abs(dfInvFlattening - 299.1528128) < 0.0001)
                {
                    pszEllipsoidCode =
                        "BN"; /* Bessel 1841 (Namibia / Schwarzeck)*/
                }
#if 0
                else if( std::abs(dfSemiMajor-6378160.0) < 0.01
                         && std::abs(dfInvFlattening-298.247167427) < 0.0001 )
                {
                    pszEllipsoidCode = "GRS67";      /* GRS 1967 */
                }
#endif
                else if (std::abs(dfSemiMajor - 6378137) < 0.01 &&
                         std::abs(dfInvFlattening - 298.257222101) < 0.000001)
                {
                    pszEllipsoidCode = "RF"; /* GRS 1980 */
                }
                else if (std::abs(dfSemiMajor - 6378206.4) < 0.01 &&
                         std::abs(dfInvFlattening - 294.9786982) < 0.0001)
                {
                    pszEllipsoidCode = "CC"; /* Clarke 1866 */
                }
                else if (std::abs(dfSemiMajor - 6377340.189) < 0.01 &&
                         std::abs(dfInvFlattening - 299.3249646) < 0.0001)
                {
                    pszEllipsoidCode = "AM"; /* Modified Airy */
                }
                else if (std::abs(dfSemiMajor - 6377563.396) < 0.01 &&
                         std::abs(dfInvFlattening - 299.3249646) < 0.0001)
                {
                    pszEllipsoidCode = "AA"; /* Airy */
                }
                else if (std::abs(dfSemiMajor - 6378200) < 0.01 &&
                         std::abs(dfInvFlattening - 298.3) < 0.0001)
                {
                    pszEllipsoidCode = "HE"; /* Helmert 1906 */
                }
                else if (std::abs(dfSemiMajor - 6378155) < 0.01 &&
                         std::abs(dfInvFlattening - 298.3) < 0.0001)
                {
                    pszEllipsoidCode = "FA"; /* Modified Fischer 1960 */
                }
#if 0
                else if( std::abs(dfSemiMajor-6377298.556) < 0.01
                         && std::abs(dfInvFlattening-300.8017) < 0.0001 )
                {
                    pszEllipsoidCode = "evrstSS";    /* Everest (Sabah & Sarawak) */
                }
                else if( std::abs(dfSemiMajor-6378165.0) < 0.01
                         && std::abs(dfInvFlattening-298.3) < 0.0001 )
                {
                    pszEllipsoidCode = "WGS60";
                }
                else if( std::abs(dfSemiMajor-6378145.0) < 0.01
                         && std::abs(dfInvFlattening-298.25) < 0.0001 )
                {
                    pszEllipsoidCode = "WGS66";
                }
#endif
                else if (std::abs(dfSemiMajor - 6378135.0) < 0.01 &&
                         std::abs(dfInvFlattening - 298.26) < 0.0001)
                {
                    pszEllipsoidCode = "WD";
                }
                else if (std::abs(dfSemiMajor - 6378137.0) < 0.01 &&
                         std::abs(dfInvFlattening - 298.257223563) < 0.000001)
                {
                    pszEllipsoidCode = "WE";
                }

                if (pszEllipsoidCode != NULL)
                {
                    poPDFDatumDict->Add("Ellipsoid", pszEllipsoidCode);
                }
                else
#endif /* disabled_because_terrago_toolbar_does_not_like_it */
                {
                    const char *pszEllipsoidDescription =
                        poSpheroidNode->GetChild(0)->GetValue();

                    CPLDebug("PDF",
                             "Unhandled ellipsoid name (%s). Write ellipsoid "
                             "parameters then.",
                             pszEllipsoidDescription);

                    poPDFDatumDict->Add(
                        "Ellipsoid",
                        &((new GDALPDFDictionaryRW())
                              ->Add("Description", pszEllipsoidDescription)
                              .Add("SemiMajorAxis", dfSemiMajor, TRUE)
                              .Add("InvFlattening", dfInvFlattening, TRUE)));
                }

                const OGR_SRSNode *poTOWGS84 = poSRS->GetAttrNode("TOWGS84");
                if (poTOWGS84 != nullptr && poTOWGS84->GetChildCount() >= 3 &&
                    (poTOWGS84->GetChildCount() < 7 ||
                     (EQUAL(poTOWGS84->GetChild(3)->GetValue(), "") &&
                      EQUAL(poTOWGS84->GetChild(4)->GetValue(), "") &&
                      EQUAL(poTOWGS84->GetChild(5)->GetValue(), "") &&
                      EQUAL(poTOWGS84->GetChild(6)->GetValue(), ""))))
                {
                    poPDFDatumDict->Add(
                        "ToWGS84",
                        &((new GDALPDFDictionaryRW())
                              ->Add("dx", poTOWGS84->GetChild(0)->GetValue())
                              .Add("dy", poTOWGS84->GetChild(1)->GetValue())
                              .Add("dz", poTOWGS84->GetChild(2)->GetValue())));
                }
                else if (poTOWGS84 != nullptr &&
                         poTOWGS84->GetChildCount() >= 7)
                {
                    poPDFDatumDict->Add(
                        "ToWGS84",
                        &((new GDALPDFDictionaryRW())
                              ->Add("dx", poTOWGS84->GetChild(0)->GetValue())
                              .Add("dy", poTOWGS84->GetChild(1)->GetValue())
                              .Add("dz", poTOWGS84->GetChild(2)->GetValue())
                              .Add("rx", poTOWGS84->GetChild(3)->GetValue())
                              .Add("ry", poTOWGS84->GetChild(4)->GetValue())
                              .Add("rz", poTOWGS84->GetChild(5)->GetValue())
                              .Add("sf", poTOWGS84->GetChild(6)->GetValue())));
                }
            }
        }
    }
    else
    {
        CPLError(CE_Warning, CPLE_NotSupported,
                 "No datum name. Defaulting to WGS84.");
    }

    if (poPDFDatum == nullptr)
        poPDFDatum = GDALPDFObjectRW::CreateString("WGE");

    return poPDFDatum;
}

/************************************************************************/
/*                   GDALPDFBuildOGC_BP_Projection()                    */
/************************************************************************/

GDALPDFDictionaryRW *GDALPDFBaseWriter::GDALPDFBuildOGC_BP_Projection(
    const OGRSpatialReference *poSRS)
{

    const char *pszProjectionOGCBP = "GEOGRAPHIC";
    const char *pszProjection = poSRS->GetAttrValue("PROJECTION");

    GDALPDFDictionaryRW *poProjectionDict = new GDALPDFDictionaryRW();
    poProjectionDict->Add("Type", GDALPDFObjectRW::CreateName("Projection"));
    poProjectionDict->Add("Datum", GDALPDFBuildOGC_BP_Datum(poSRS));

    if (pszProjection == nullptr)
    {
        if (poSRS->IsGeographic())
            pszProjectionOGCBP = "GEOGRAPHIC";
        else if (poSRS->IsLocal())
            pszProjectionOGCBP = "LOCAL CARTESIAN";
        else
        {
            CPLError(CE_Warning, CPLE_NotSupported, "Unsupported SRS type");
            delete poProjectionDict;
            return nullptr;
        }
    }
    else if (EQUAL(pszProjection, SRS_PT_TRANSVERSE_MERCATOR))
    {
        int bNorth;
        int nZone = poSRS->GetUTMZone(&bNorth);

        if (nZone != 0)
        {
            pszProjectionOGCBP = "UT";
            poProjectionDict->Add("Hemisphere", (bNorth) ? "N" : "S");
            poProjectionDict->Add("Zone", nZone);
        }
        else
        {
            double dfCenterLat =
                poSRS->GetNormProjParm(SRS_PP_LATITUDE_OF_ORIGIN, 90.L);
            double dfCenterLong =
                poSRS->GetNormProjParm(SRS_PP_CENTRAL_MERIDIAN, 0.0);
            double dfScale = poSRS->GetNormProjParm(SRS_PP_SCALE_FACTOR, 1.0);
            double dfFalseEasting =
                poSRS->GetNormProjParm(SRS_PP_FALSE_EASTING, 0.0);
            double dfFalseNorthing =
                poSRS->GetNormProjParm(SRS_PP_FALSE_NORTHING, 0.0);

            /* OGC_BP supports representing numbers as strings for better
             * precision */
            /* so use it */

            pszProjectionOGCBP = "TC";
            poProjectionDict->Add("OriginLatitude", dfCenterLat, TRUE);
            poProjectionDict->Add("CentralMeridian", dfCenterLong, TRUE);
            poProjectionDict->Add("ScaleFactor", dfScale, TRUE);
            poProjectionDict->Add("FalseEasting", dfFalseEasting, TRUE);
            poProjectionDict->Add("FalseNorthing", dfFalseNorthing, TRUE);
        }
    }
    else if (EQUAL(pszProjection, SRS_PT_POLAR_STEREOGRAPHIC))
    {
        double dfCenterLat =
            poSRS->GetNormProjParm(SRS_PP_LATITUDE_OF_ORIGIN, 0.0);
        double dfCenterLong =
            poSRS->GetNormProjParm(SRS_PP_CENTRAL_MERIDIAN, 0.0);
        double dfScale = poSRS->GetNormProjParm(SRS_PP_SCALE_FACTOR, 1.0);
        double dfFalseEasting =
            poSRS->GetNormProjParm(SRS_PP_FALSE_EASTING, 0.0);
        double dfFalseNorthing =
            poSRS->GetNormProjParm(SRS_PP_FALSE_NORTHING, 0.0);

        if (fabs(dfCenterLat) == 90.0 && dfCenterLong == 0.0 &&
            dfScale == 0.994 && dfFalseEasting == 200000.0 &&
            dfFalseNorthing == 200000.0)
        {
            pszProjectionOGCBP = "UP";
            poProjectionDict->Add("Hemisphere", (dfCenterLat > 0) ? "N" : "S");
        }
        else
        {
            pszProjectionOGCBP = "PG";
            poProjectionDict->Add("LatitudeTrueScale", dfCenterLat, TRUE);
            poProjectionDict->Add("LongitudeDownFromPole", dfCenterLong, TRUE);
            poProjectionDict->Add("ScaleFactor", dfScale, TRUE);
            poProjectionDict->Add("FalseEasting", dfFalseEasting, TRUE);
            poProjectionDict->Add("FalseNorthing", dfFalseNorthing, TRUE);
        }
    }

    else if (EQUAL(pszProjection, SRS_PT_LAMBERT_CONFORMAL_CONIC_2SP))
    {
        double dfStdP1 =
            poSRS->GetNormProjParm(SRS_PP_STANDARD_PARALLEL_1, 0.0);
        double dfStdP2 =
            poSRS->GetNormProjParm(SRS_PP_STANDARD_PARALLEL_2, 0.0);
        double dfCenterLat =
            poSRS->GetNormProjParm(SRS_PP_LATITUDE_OF_ORIGIN, 0.0);
        double dfCenterLong =
            poSRS->GetNormProjParm(SRS_PP_CENTRAL_MERIDIAN, 0.0);
        double dfFalseEasting =
            poSRS->GetNormProjParm(SRS_PP_FALSE_EASTING, 0.0);
        double dfFalseNorthing =
            poSRS->GetNormProjParm(SRS_PP_FALSE_NORTHING, 0.0);

        pszProjectionOGCBP = "LE";
        poProjectionDict->Add("StandardParallelOne", dfStdP1, TRUE);
        poProjectionDict->Add("StandardParallelTwo", dfStdP2, TRUE);
        poProjectionDict->Add("OriginLatitude", dfCenterLat, TRUE);
        poProjectionDict->Add("CentralMeridian", dfCenterLong, TRUE);
        poProjectionDict->Add("FalseEasting", dfFalseEasting, TRUE);
        poProjectionDict->Add("FalseNorthing", dfFalseNorthing, TRUE);
    }

    else if (EQUAL(pszProjection, SRS_PT_MERCATOR_1SP))
    {
        double dfCenterLong =
            poSRS->GetNormProjParm(SRS_PP_CENTRAL_MERIDIAN, 0.0);
        double dfCenterLat =
            poSRS->GetNormProjParm(SRS_PP_LATITUDE_OF_ORIGIN, 0.0);
        double dfScale = poSRS->GetNormProjParm(SRS_PP_SCALE_FACTOR, 1.0);
        double dfFalseEasting =
            poSRS->GetNormProjParm(SRS_PP_FALSE_EASTING, 0.0);
        double dfFalseNorthing =
            poSRS->GetNormProjParm(SRS_PP_FALSE_NORTHING, 0.0);

        pszProjectionOGCBP = "MC";
        poProjectionDict->Add("CentralMeridian", dfCenterLong, TRUE);
        poProjectionDict->Add("OriginLatitude", dfCenterLat, TRUE);
        poProjectionDict->Add("ScaleFactor", dfScale, TRUE);
        poProjectionDict->Add("FalseEasting", dfFalseEasting, TRUE);
        poProjectionDict->Add("FalseNorthing", dfFalseNorthing, TRUE);
    }

#ifdef not_supported
    else if (EQUAL(pszProjection, SRS_PT_MERCATOR_2SP))
    {
        double dfStdP1 =
            poSRS->GetNormProjParm(SRS_PP_STANDARD_PARALLEL_1, 0.0);
        double dfCenterLong =
            poSRS->GetNormProjParm(SRS_PP_CENTRAL_MERIDIAN, 0.0);
        double dfFalseEasting =
            poSRS->GetNormProjParm(SRS_PP_FALSE_EASTING, 0.0);
        double dfFalseNorthing =
            poSRS->GetNormProjParm(SRS_PP_FALSE_NORTHING, 0.0);

        pszProjectionOGCBP = "MC";
        poProjectionDict->Add("StandardParallelOne", dfStdP1, TRUE);
        poProjectionDict->Add("CentralMeridian", dfCenterLong, TRUE);
        poProjectionDict->Add("FalseEasting", dfFalseEasting, TRUE);
        poProjectionDict->Add("FalseNorthing", dfFalseNorthing, TRUE);
    }
#endif

    else
    {
        CPLError(CE_Warning, CPLE_NotSupported,
                 "Unhandled projection type (%s) for now", pszProjection);
    }

    poProjectionDict->Add("ProjectionType", pszProjectionOGCBP);

    if (poSRS->IsProjected())
    {
        const char *pszUnitName = nullptr;
        double dfLinearUnits = poSRS->GetLinearUnits(&pszUnitName);
        if (dfLinearUnits == 1.0)
            poProjectionDict->Add("Units", "M");
        else if (dfLinearUnits == 0.3048)
            poProjectionDict->Add("Units", "FT");
    }

    return poProjectionDict;
}

/************************************************************************/
/*                           WriteSRS_OGC_BP()                          */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::WriteSRS_OGC_BP(GDALDataset *poSrcDS,
                                                    double dfUserUnit,
                                                    const char *pszNEATLINE,
                                                    PDFMargins *psMargins)
{
    int nWidth = poSrcDS->GetRasterXSize();
    int nHeight = poSrcDS->GetRasterYSize();
    const char *pszWKT = poSrcDS->GetProjectionRef();
    double adfGeoTransform[6];

    int bHasGT = (poSrcDS->GetGeoTransform(adfGeoTransform) == CE_None);
    int nGCPCount = poSrcDS->GetGCPCount();
    const GDAL_GCP *pasGCPList =
        (nGCPCount >= 4) ? poSrcDS->GetGCPs() : nullptr;
    if (pasGCPList != nullptr)
        pszWKT = poSrcDS->GetGCPProjection();

    if (!bHasGT && pasGCPList == nullptr)
        return GDALPDFObjectNum();

    if (pszWKT == nullptr || EQUAL(pszWKT, ""))
        return GDALPDFObjectNum();

    if (!bHasGT)
    {
        if (!GDALGCPsToGeoTransform(nGCPCount, pasGCPList, adfGeoTransform,
                                    FALSE))
        {
            CPLDebug("PDF", "Could not compute GT with exact match. Writing "
                            "Registration then");
        }
        else
        {
            bHasGT = TRUE;
        }
    }

    OGRSpatialReferenceH hSRS = OSRNewSpatialReference(pszWKT);
    if (hSRS == nullptr)
        return GDALPDFObjectNum();
    OSRSetAxisMappingStrategy(hSRS, OAMS_TRADITIONAL_GIS_ORDER);

    const OGRSpatialReference *poSRS = OGRSpatialReference::FromHandle(hSRS);
    GDALPDFDictionaryRW *poProjectionDict =
        GDALPDFBuildOGC_BP_Projection(poSRS);
    if (poProjectionDict == nullptr)
    {
        OSRDestroySpatialReference(hSRS);
        return GDALPDFObjectNum();
    }

    GDALPDFArrayRW *poNeatLineArray = nullptr;

    if (pszNEATLINE == nullptr)
        pszNEATLINE = poSrcDS->GetMetadataItem("NEATLINE");
    if (bHasGT && pszNEATLINE != nullptr && !EQUAL(pszNEATLINE, "NO") &&
        pszNEATLINE[0] != '\0')
    {
        OGRGeometry *poGeom = nullptr;
        OGRGeometryFactory::createFromWkt(pszNEATLINE, nullptr, &poGeom);
        if (poGeom != nullptr &&
            wkbFlatten(poGeom->getGeometryType()) == wkbPolygon)
        {
            OGRLineString *poLS = poGeom->toPolygon()->getExteriorRing();
            double adfGeoTransformInv[6];
            if (poLS != nullptr && poLS->getNumPoints() >= 5 &&
                GDALInvGeoTransform(adfGeoTransform, adfGeoTransformInv))
            {
                poNeatLineArray = new GDALPDFArrayRW();

                // FIXME : ensure that they are in clockwise order ?
                for (int i = 0; i < poLS->getNumPoints() - 1; i++)
                {
                    double X = poLS->getX(i);
                    double Y = poLS->getY(i);
                    double x = adfGeoTransformInv[0] +
                               X * adfGeoTransformInv[1] +
                               Y * adfGeoTransformInv[2];
                    double y = adfGeoTransformInv[3] +
                               X * adfGeoTransformInv[4] +
                               Y * adfGeoTransformInv[5];
                    poNeatLineArray->Add(x / dfUserUnit + psMargins->nLeft,
                                         TRUE);
                    poNeatLineArray->Add(
                        (nHeight - y) / dfUserUnit + psMargins->nBottom, TRUE);
                }
            }
        }
        delete poGeom;
    }

    if (pszNEATLINE != nullptr && EQUAL(pszNEATLINE, "NO"))
    {
        // Do nothing
    }
    else if (pasGCPList && poNeatLineArray == nullptr)
    {
        if (nGCPCount == 4)
        {
            int iUL = 0;
            int iUR = 0;
            int iLR = 0;
            int iLL = 0;
            GDALPDFFind4Corners(pasGCPList, iUL, iUR, iLR, iLL);

            double adfNL[8];
            adfNL[0] =
                pasGCPList[iUL].dfGCPPixel / dfUserUnit + psMargins->nLeft;
            adfNL[1] = (nHeight - pasGCPList[iUL].dfGCPLine) / dfUserUnit +
                       psMargins->nBottom;
            adfNL[2] =
                pasGCPList[iLL].dfGCPPixel / dfUserUnit + psMargins->nLeft;
            adfNL[3] = (nHeight - pasGCPList[iLL].dfGCPLine) / dfUserUnit +
                       psMargins->nBottom;
            adfNL[4] =
                pasGCPList[iLR].dfGCPPixel / dfUserUnit + psMargins->nLeft;
            adfNL[5] = (nHeight - pasGCPList[iLR].dfGCPLine) / dfUserUnit +
                       psMargins->nBottom;
            adfNL[6] =
                pasGCPList[iUR].dfGCPPixel / dfUserUnit + psMargins->nLeft;
            adfNL[7] = (nHeight - pasGCPList[iUR].dfGCPLine) / dfUserUnit +
                       psMargins->nBottom;

            poNeatLineArray = new GDALPDFArrayRW();
            poNeatLineArray->Add(adfNL, 8, TRUE);
        }
        else
        {
            poNeatLineArray = new GDALPDFArrayRW();

            // FIXME : ensure that they are in clockwise order ?
            int i;
            for (i = 0; i < nGCPCount; i++)
            {
                poNeatLineArray->Add(pasGCPList[i].dfGCPPixel / dfUserUnit +
                                         psMargins->nLeft,
                                     TRUE);
                poNeatLineArray->Add((nHeight - pasGCPList[i].dfGCPLine) /
                                             dfUserUnit +
                                         psMargins->nBottom,
                                     TRUE);
            }
        }
    }
    else if (poNeatLineArray == nullptr)
    {
        poNeatLineArray = new GDALPDFArrayRW();

        poNeatLineArray->Add(0 / dfUserUnit + psMargins->nLeft, TRUE);
        poNeatLineArray->Add((nHeight - 0) / dfUserUnit + psMargins->nBottom,
                             TRUE);

        poNeatLineArray->Add(0 / dfUserUnit + psMargins->nLeft, TRUE);
        poNeatLineArray->Add(
            (/*nHeight -nHeight*/ 0) / dfUserUnit + psMargins->nBottom, TRUE);

        poNeatLineArray->Add(nWidth / dfUserUnit + psMargins->nLeft, TRUE);
        poNeatLineArray->Add(
            (/*nHeight -nHeight*/ 0) / dfUserUnit + psMargins->nBottom, TRUE);

        poNeatLineArray->Add(nWidth / dfUserUnit + psMargins->nLeft, TRUE);
        poNeatLineArray->Add((nHeight - 0) / dfUserUnit + psMargins->nBottom,
                             TRUE);
    }

    auto nLGIDictId = AllocNewObject();
    StartObj(nLGIDictId);
    GDALPDFDictionaryRW oLGIDict;
    oLGIDict.Add("Type", GDALPDFObjectRW::CreateName("LGIDict"))
        .Add("Version", "2.1");
    if (bHasGT)
    {
        double adfCTM[6];
        double dfX1 = psMargins->nLeft;
        double dfY2 = nHeight / dfUserUnit + psMargins->nBottom;

        adfCTM[0] = adfGeoTransform[1] * dfUserUnit;
        adfCTM[1] = adfGeoTransform[2] * dfUserUnit;
        adfCTM[2] = -adfGeoTransform[4] * dfUserUnit;
        adfCTM[3] = -adfGeoTransform[5] * dfUserUnit;
        adfCTM[4] = adfGeoTransform[0] - (adfCTM[0] * dfX1 + adfCTM[2] * dfY2);
        adfCTM[5] = adfGeoTransform[3] - (adfCTM[1] * dfX1 + adfCTM[3] * dfY2);

        oLGIDict.Add("CTM", &((new GDALPDFArrayRW())->Add(adfCTM, 6, TRUE)));
    }
    else
    {
        GDALPDFArrayRW *poRegistrationArray = new GDALPDFArrayRW();
        int i;
        for (i = 0; i < nGCPCount; i++)
        {
            GDALPDFArrayRW *poPTArray = new GDALPDFArrayRW();
            poPTArray->Add(
                pasGCPList[i].dfGCPPixel / dfUserUnit + psMargins->nLeft, TRUE);
            poPTArray->Add((nHeight - pasGCPList[i].dfGCPLine) / dfUserUnit +
                               psMargins->nBottom,
                           TRUE);
            poPTArray->Add(pasGCPList[i].dfGCPX, TRUE);
            poPTArray->Add(pasGCPList[i].dfGCPY, TRUE);
            poRegistrationArray->Add(poPTArray);
        }
        oLGIDict.Add("Registration", poRegistrationArray);
    }
    if (poNeatLineArray)
    {
        oLGIDict.Add("Neatline", poNeatLineArray);
    }

    const OGR_SRSNode *poNode = poSRS->GetRoot();
    if (poNode != nullptr)
        poNode = poNode->GetChild(0);
    const char *pszDescription = nullptr;
    if (poNode != nullptr)
        pszDescription = poNode->GetValue();
    if (pszDescription)
    {
        oLGIDict.Add("Description", pszDescription);
    }

    oLGIDict.Add("Projection", poProjectionDict);

    /* GDAL extension */
    if (CPLTestBool(CPLGetConfigOption("GDAL_PDF_OGC_BP_WRITE_WKT", "TRUE")))
        poProjectionDict->Add("WKT", pszWKT);

    VSIFPrintfL(m_fp, "%s\n", oLGIDict.Serialize().c_str());
    EndObj();

    OSRDestroySpatialReference(hSRS);

    return nLGIDictId;
}

/************************************************************************/
/*                     GDALPDFGetValueFromDSOrOption()                  */
/************************************************************************/

static const char *GDALPDFGetValueFromDSOrOption(GDALDataset *poSrcDS,
                                                 char **papszOptions,
                                                 const char *pszKey)
{
    const char *pszValue = CSLFetchNameValue(papszOptions, pszKey);
    if (pszValue == nullptr)
        pszValue = poSrcDS->GetMetadataItem(pszKey);
    if (pszValue != nullptr && pszValue[0] == '\0')
        return nullptr;
    else
        return pszValue;
}

/************************************************************************/
/*                             SetInfo()                                */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::SetInfo(GDALDataset *poSrcDS,
                                            char **papszOptions)
{
    const char *pszAUTHOR =
        GDALPDFGetValueFromDSOrOption(poSrcDS, papszOptions, "AUTHOR");
    const char *pszPRODUCER =
        GDALPDFGetValueFromDSOrOption(poSrcDS, papszOptions, "PRODUCER");
    const char *pszCREATOR =
        GDALPDFGetValueFromDSOrOption(poSrcDS, papszOptions, "CREATOR");
    const char *pszCREATION_DATE =
        GDALPDFGetValueFromDSOrOption(poSrcDS, papszOptions, "CREATION_DATE");
    const char *pszSUBJECT =
        GDALPDFGetValueFromDSOrOption(poSrcDS, papszOptions, "SUBJECT");
    const char *pszTITLE =
        GDALPDFGetValueFromDSOrOption(poSrcDS, papszOptions, "TITLE");
    const char *pszKEYWORDS =
        GDALPDFGetValueFromDSOrOption(poSrcDS, papszOptions, "KEYWORDS");
    return SetInfo(pszAUTHOR, pszPRODUCER, pszCREATOR, pszCREATION_DATE,
                   pszSUBJECT, pszTITLE, pszKEYWORDS);
}

/************************************************************************/
/*                             SetInfo()                                */
/************************************************************************/

GDALPDFObjectNum
GDALPDFBaseWriter::SetInfo(const char *pszAUTHOR, const char *pszPRODUCER,
                           const char *pszCREATOR, const char *pszCREATION_DATE,
                           const char *pszSUBJECT, const char *pszTITLE,
                           const char *pszKEYWORDS)
{
    if (pszAUTHOR == nullptr && pszPRODUCER == nullptr &&
        pszCREATOR == nullptr && pszCREATION_DATE == nullptr &&
        pszSUBJECT == nullptr && pszTITLE == nullptr && pszKEYWORDS == nullptr)
        return GDALPDFObjectNum();

    if (!m_nInfoId.toBool())
        m_nInfoId = AllocNewObject();
    StartObj(m_nInfoId, m_nInfoGen);
    GDALPDFDictionaryRW oDict;
    if (pszAUTHOR != nullptr)
        oDict.Add("Author", pszAUTHOR);
    if (pszPRODUCER != nullptr)
        oDict.Add("Producer", pszPRODUCER);
    if (pszCREATOR != nullptr)
        oDict.Add("Creator", pszCREATOR);
    if (pszCREATION_DATE != nullptr)
        oDict.Add("CreationDate", pszCREATION_DATE);
    if (pszSUBJECT != nullptr)
        oDict.Add("Subject", pszSUBJECT);
    if (pszTITLE != nullptr)
        oDict.Add("Title", pszTITLE);
    if (pszKEYWORDS != nullptr)
        oDict.Add("Keywords", pszKEYWORDS);
    VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());
    EndObj();

    return m_nInfoId;
}

/************************************************************************/
/*                             SetXMP()                                 */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::SetXMP(GDALDataset *poSrcDS,
                                           const char *pszXMP)
{
    if (pszXMP != nullptr && STARTS_WITH_CI(pszXMP, "NO"))
        return GDALPDFObjectNum();
    if (pszXMP != nullptr && pszXMP[0] == '\0')
        return GDALPDFObjectNum();

    if (poSrcDS && pszXMP == nullptr)
    {
        char **papszXMP = poSrcDS->GetMetadata("xml:XMP");
        if (papszXMP != nullptr && papszXMP[0] != nullptr)
            pszXMP = papszXMP[0];
    }

    if (pszXMP == nullptr)
        return GDALPDFObjectNum();

    CPLXMLNode *psNode = CPLParseXMLString(pszXMP);
    if (psNode == nullptr)
        return GDALPDFObjectNum();
    CPLDestroyXMLNode(psNode);

    if (!m_nXMPId.toBool())
        m_nXMPId = AllocNewObject();
    StartObj(m_nXMPId, m_nXMPGen);
    GDALPDFDictionaryRW oDict;
    oDict.Add("Type", GDALPDFObjectRW::CreateName("Metadata"))
        .Add("Subtype", GDALPDFObjectRW::CreateName("XML"))
        .Add("Length", static_cast<int>(strlen(pszXMP)));
    VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());
    VSIFPrintfL(m_fp, "stream\n");
    VSIFPrintfL(m_fp, "%s\n", pszXMP);
    VSIFPrintfL(m_fp, "endstream\n");
    EndObj();
    return m_nXMPId;
}

/************************************************************************/
/*                              WriteOCG()                              */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::WriteOCG(const char *pszLayerName,
                                             const GDALPDFObjectNum &nParentId)
{
    if (pszLayerName == nullptr || pszLayerName[0] == '\0')
        return GDALPDFObjectNum();

    auto nOCGId = AllocNewObject();

    GDALPDFOCGDesc oOCGDesc;
    oOCGDesc.nId = nOCGId;
    oOCGDesc.nParentId = nParentId;
    oOCGDesc.osLayerName = pszLayerName;

    m_asOCGs.push_back(oOCGDesc);

    StartObj(nOCGId);
    {
        GDALPDFDictionaryRW oDict;
        oDict.Add("Type", GDALPDFObjectRW::CreateName("OCG"));
        oDict.Add("Name", pszLayerName);
        VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());
    }
    EndObj();

    return nOCGId;
}

/************************************************************************/
/*                              StartPage()                             */
/************************************************************************/

bool GDALPDFWriter::StartPage(GDALDataset *poClippingDS, double dfDPI,
                              bool bWriteUserUnit, const char *pszGEO_ENCODING,
                              const char *pszNEATLINE, PDFMargins *psMargins,
                              PDFCompressMethod eStreamCompressMethod,
                              int bHasOGRData)
{
    int nWidth = poClippingDS->GetRasterXSize();
    int nHeight = poClippingDS->GetRasterYSize();
    int nBands = poClippingDS->GetRasterCount();

    double dfUserUnit = dfDPI * USER_UNIT_IN_INCH;
    double dfWidthInUserUnit =
        nWidth / dfUserUnit + psMargins->nLeft + psMargins->nRight;
    double dfHeightInUserUnit =
        nHeight / dfUserUnit + psMargins->nBottom + psMargins->nTop;

    auto nPageId = AllocNewObject();
    m_asPageId.push_back(nPageId);

    auto nContentId = AllocNewObject();
    auto nResourcesId = AllocNewObject();

    auto nAnnotsId = AllocNewObject();

    const bool bISO32000 =
        EQUAL(pszGEO_ENCODING, "ISO32000") || EQUAL(pszGEO_ENCODING, "BOTH");
    const bool bOGC_BP =
        EQUAL(pszGEO_ENCODING, "OGC_BP") || EQUAL(pszGEO_ENCODING, "BOTH");

    GDALPDFObjectNum nViewportId;
    if (bISO32000)
        nViewportId = WriteSRS_ISO32000(poClippingDS, dfUserUnit, pszNEATLINE,
                                        psMargins, TRUE);

    GDALPDFObjectNum nLGIDictId;
    if (bOGC_BP)
        nLGIDictId =
            WriteSRS_OGC_BP(poClippingDS, dfUserUnit, pszNEATLINE, psMargins);

    StartObj(nPageId);
    GDALPDFDictionaryRW oDictPage;
    oDictPage.Add("Type", GDALPDFObjectRW::CreateName("Page"))
        .Add("Parent", m_nPageResourceId, 0)
        .Add("MediaBox", &((new GDALPDFArrayRW())
                               ->Add(0)
                               .Add(0)
                               .Add(dfWidthInUserUnit)
                               .Add(dfHeightInUserUnit)));
    if (bWriteUserUnit)
        oDictPage.Add("UserUnit", dfUserUnit);
    oDictPage.Add("Contents", nContentId, 0)
        .Add("Resources", nResourcesId, 0)
        .Add("Annots", nAnnotsId, 0);

    if (nBands == 4)
    {
        oDictPage.Add(
            "Group",
            &((new GDALPDFDictionaryRW())
                  ->Add("Type", GDALPDFObjectRW::CreateName("Group"))
                  .Add("S", GDALPDFObjectRW::CreateName("Transparency"))
                  .Add("CS", GDALPDFObjectRW::CreateName("DeviceRGB"))));
    }
    if (nViewportId.toBool())
    {
        oDictPage.Add("VP", &((new GDALPDFArrayRW())->Add(nViewportId, 0)));
    }
    if (nLGIDictId.toBool())
    {
        oDictPage.Add("LGIDict", nLGIDictId, 0);
    }

#ifndef HACK_TO_GENERATE_OCMD
    if (bHasOGRData)
        oDictPage.Add("StructParents", 0);
#endif

    VSIFPrintfL(m_fp, "%s\n", oDictPage.Serialize().c_str());
    EndObj();

    oPageContext.poClippingDS = poClippingDS;
    oPageContext.nPageId = nPageId;
    oPageContext.nContentId = nContentId;
    oPageContext.nResourcesId = nResourcesId;
    oPageContext.nAnnotsId = nAnnotsId;
    oPageContext.dfDPI = dfDPI;
    oPageContext.sMargins = *psMargins;
    oPageContext.eStreamCompressMethod = eStreamCompressMethod;

    return true;
}

/************************************************************************/
/*                             WriteColorTable()                        */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::WriteColorTable(GDALDataset *poSrcDS)
{
    /* Does the source image has a color table ? */
    GDALColorTable *poCT = nullptr;
    if (poSrcDS->GetRasterCount() > 0)
        poCT = poSrcDS->GetRasterBand(1)->GetColorTable();
    GDALPDFObjectNum nColorTableId;
    if (poCT != nullptr && poCT->GetColorEntryCount() <= 256)
    {
        int nColors = poCT->GetColorEntryCount();
        nColorTableId = AllocNewObject();

        auto nLookupTableId = AllocNewObject();

        /* Index object */
        StartObj(nColorTableId);
        {
            GDALPDFArrayRW oArray;
            oArray.Add(GDALPDFObjectRW::CreateName("Indexed"))
                .Add(&((new GDALPDFArrayRW())
                           ->Add(GDALPDFObjectRW::CreateName("DeviceRGB"))))
                .Add(nColors - 1)
                .Add(nLookupTableId, 0);
            VSIFPrintfL(m_fp, "%s\n", oArray.Serialize().c_str());
        }
        EndObj();

        /* Lookup table object */
        StartObj(nLookupTableId);
        {
            GDALPDFDictionaryRW oDict;
            oDict.Add("Length", nColors * 3);
            VSIFPrintfL(m_fp, "%s %% Lookup table\n",
                        oDict.Serialize().c_str());
        }
        VSIFPrintfL(m_fp, "stream\n");
        GByte pabyLookup[768];
        for (int i = 0; i < nColors; i++)
        {
            const GDALColorEntry *poEntry = poCT->GetColorEntry(i);
            pabyLookup[3 * i + 0] = static_cast<GByte>(poEntry->c1);
            pabyLookup[3 * i + 1] = static_cast<GByte>(poEntry->c2);
            pabyLookup[3 * i + 2] = static_cast<GByte>(poEntry->c3);
        }
        VSIFWriteL(pabyLookup, 3 * nColors, 1, m_fp);
        VSIFPrintfL(m_fp, "\n");
        VSIFPrintfL(m_fp, "endstream\n");
        EndObj();
    }

    return nColorTableId;
}

/************************************************************************/
/*                             WriteImagery()                           */
/************************************************************************/

bool GDALPDFWriter::WriteImagery(GDALDataset *poDS, const char *pszLayerName,
                                 PDFCompressMethod eCompressMethod,
                                 int nPredictor, int nJPEGQuality,
                                 const char *pszJPEG2000_DRIVER,
                                 int nBlockXSize, int nBlockYSize,
                                 GDALProgressFunc pfnProgress,
                                 void *pProgressData)
{
    int nWidth = poDS->GetRasterXSize();
    int nHeight = poDS->GetRasterYSize();
    double dfUserUnit = oPageContext.dfDPI * USER_UNIT_IN_INCH;

    GDALPDFRasterDesc oRasterDesc;

    if (pfnProgress == nullptr)
        pfnProgress = GDALDummyProgress;

    oRasterDesc.nOCGRasterId = WriteOCG(pszLayerName);

    /* Does the source image has a color table ? */
    auto nColorTableId = WriteColorTable(poDS);

    int nXBlocks = DIV_ROUND_UP(nWidth, nBlockXSize);
    int nYBlocks = DIV_ROUND_UP(nHeight, nBlockYSize);
    int nBlocks = nXBlocks * nYBlocks;
    int nBlockXOff, nBlockYOff;
    for (nBlockYOff = 0; nBlockYOff < nYBlocks; nBlockYOff++)
    {
        for (nBlockXOff = 0; nBlockXOff < nXBlocks; nBlockXOff++)
        {
            const int nReqWidth =
                std::min(nBlockXSize, nWidth - nBlockXOff * nBlockXSize);
            const int nReqHeight =
                std::min(nBlockYSize, nHeight - nBlockYOff * nBlockYSize);
            int iImage = nBlockYOff * nXBlocks + nBlockXOff;

            void *pScaledData = GDALCreateScaledProgress(
                iImage / double(nBlocks), (iImage + 1) / double(nBlocks),
                pfnProgress, pProgressData);
            int nX = nBlockXOff * nBlockXSize;
            int nY = nBlockYOff * nBlockYSize;

            auto nImageId =
                WriteBlock(poDS, nX, nY, nReqWidth, nReqHeight, nColorTableId,
                           eCompressMethod, nPredictor, nJPEGQuality,
                           pszJPEG2000_DRIVER, GDALScaledProgress, pScaledData);

            GDALDestroyScaledProgress(pScaledData);

            if (!nImageId.toBool())
                return false;

            GDALPDFImageDesc oImageDesc;
            oImageDesc.nImageId = nImageId;
            oImageDesc.dfXOff = nX / dfUserUnit + oPageContext.sMargins.nLeft;
            oImageDesc.dfYOff = (nHeight - nY - nReqHeight) / dfUserUnit +
                                oPageContext.sMargins.nBottom;
            oImageDesc.dfXSize = nReqWidth / dfUserUnit;
            oImageDesc.dfYSize = nReqHeight / dfUserUnit;

            oRasterDesc.asImageDesc.push_back(oImageDesc);
        }
    }

    oPageContext.asRasterDesc.push_back(oRasterDesc);

    return true;
}

/************************************************************************/
/*                        WriteClippedImagery()                         */
/************************************************************************/

bool GDALPDFWriter::WriteClippedImagery(
    GDALDataset *poDS, const char *pszLayerName,
    PDFCompressMethod eCompressMethod, int nPredictor, int nJPEGQuality,
    const char *pszJPEG2000_DRIVER, int nBlockXSize, int nBlockYSize,
    GDALProgressFunc pfnProgress, void *pProgressData)
{
    double dfUserUnit = oPageContext.dfDPI * USER_UNIT_IN_INCH;

    GDALPDFRasterDesc oRasterDesc;

    /* Get clipping dataset bounding-box */
    double adfClippingGeoTransform[6];
    GDALDataset *poClippingDS = oPageContext.poClippingDS;
    poClippingDS->GetGeoTransform(adfClippingGeoTransform);
    int nClippingWidth = poClippingDS->GetRasterXSize();
    int nClippingHeight = poClippingDS->GetRasterYSize();
    double dfClippingMinX = adfClippingGeoTransform[0];
    double dfClippingMaxX =
        dfClippingMinX + nClippingWidth * adfClippingGeoTransform[1];
    double dfClippingMaxY = adfClippingGeoTransform[3];
    double dfClippingMinY =
        dfClippingMaxY + nClippingHeight * adfClippingGeoTransform[5];

    if (dfClippingMaxY < dfClippingMinY)
    {
        std::swap(dfClippingMinY, dfClippingMaxY);
    }

    /* Get current dataset dataset bounding-box */
    double adfGeoTransform[6];
    poDS->GetGeoTransform(adfGeoTransform);
    int nWidth = poDS->GetRasterXSize();
    int nHeight = poDS->GetRasterYSize();
    double dfRasterMinX = adfGeoTransform[0];
    // double dfRasterMaxX = dfRasterMinX + nWidth * adfGeoTransform[1];
    double dfRasterMaxY = adfGeoTransform[3];
    double dfRasterMinY = dfRasterMaxY + nHeight * adfGeoTransform[5];

    if (dfRasterMaxY < dfRasterMinY)
    {
        std::swap(dfRasterMinY, dfRasterMaxY);
    }

    if (pfnProgress == nullptr)
        pfnProgress = GDALDummyProgress;

    oRasterDesc.nOCGRasterId = WriteOCG(pszLayerName);

    /* Does the source image has a color table ? */
    auto nColorTableId = WriteColorTable(poDS);

    int nXBlocks = DIV_ROUND_UP(nWidth, nBlockXSize);
    int nYBlocks = DIV_ROUND_UP(nHeight, nBlockYSize);
    int nBlocks = nXBlocks * nYBlocks;
    int nBlockXOff, nBlockYOff;
    for (nBlockYOff = 0; nBlockYOff < nYBlocks; nBlockYOff++)
    {
        for (nBlockXOff = 0; nBlockXOff < nXBlocks; nBlockXOff++)
        {
            int nReqWidth =
                std::min(nBlockXSize, nWidth - nBlockXOff * nBlockXSize);
            int nReqHeight =
                std::min(nBlockYSize, nHeight - nBlockYOff * nBlockYSize);
            int iImage = nBlockYOff * nXBlocks + nBlockXOff;

            void *pScaledData = GDALCreateScaledProgress(
                iImage / double(nBlocks), (iImage + 1) / double(nBlocks),
                pfnProgress, pProgressData);

            int nX = nBlockXOff * nBlockXSize;
            int nY = nBlockYOff * nBlockYSize;

            /* Compute extent of block to write */
            double dfBlockMinX = adfGeoTransform[0] + nX * adfGeoTransform[1];
            double dfBlockMaxX =
                adfGeoTransform[0] + (nX + nReqWidth) * adfGeoTransform[1];
            double dfBlockMinY =
                adfGeoTransform[3] + (nY + nReqHeight) * adfGeoTransform[5];
            double dfBlockMaxY = adfGeoTransform[3] + nY * adfGeoTransform[5];

            if (dfBlockMaxY < dfBlockMinY)
            {
                std::swap(dfBlockMinY, dfBlockMaxY);
            }

            // Clip the extent of the block with the extent of the main raster.
            const double dfIntersectMinX =
                std::max(dfBlockMinX, dfClippingMinX);
            const double dfIntersectMinY =
                std::max(dfBlockMinY, dfClippingMinY);
            const double dfIntersectMaxX =
                std::min(dfBlockMaxX, dfClippingMaxX);
            const double dfIntersectMaxY =
                std::min(dfBlockMaxY, dfClippingMaxY);

            if (dfIntersectMinX < dfIntersectMaxX &&
                dfIntersectMinY < dfIntersectMaxY)
            {
                /* Re-compute (x,y,width,height) subwindow of current raster
                 * from */
                /* the extent of the clipped block */
                nX = static_cast<int>((dfIntersectMinX - dfRasterMinX) /
                                          adfGeoTransform[1] +
                                      0.5);
                if (adfGeoTransform[5] < 0)
                    nY = static_cast<int>((dfRasterMaxY - dfIntersectMaxY) /
                                              (-adfGeoTransform[5]) +
                                          0.5);
                else
                    nY = static_cast<int>((dfIntersectMinY - dfRasterMinY) /
                                              adfGeoTransform[5] +
                                          0.5);
                nReqWidth = static_cast<int>((dfIntersectMaxX - dfRasterMinX) /
                                                 adfGeoTransform[1] +
                                             0.5) -
                            nX;
                if (adfGeoTransform[5] < 0)
                    nReqHeight =
                        static_cast<int>((dfRasterMaxY - dfIntersectMinY) /
                                             (-adfGeoTransform[5]) +
                                         0.5) -
                        nY;
                else
                    nReqHeight =
                        static_cast<int>((dfIntersectMaxY - dfRasterMinY) /
                                             adfGeoTransform[5] +
                                         0.5) -
                        nY;

                if (nReqWidth > 0 && nReqHeight > 0)
                {
                    auto nImageId = WriteBlock(
                        poDS, nX, nY, nReqWidth, nReqHeight, nColorTableId,
                        eCompressMethod, nPredictor, nJPEGQuality,
                        pszJPEG2000_DRIVER, GDALScaledProgress, pScaledData);

                    if (!nImageId.toBool())
                    {
                        GDALDestroyScaledProgress(pScaledData);
                        return false;
                    }

                    /* Compute the subwindow in image coordinates of the main
                     * raster corresponding */
                    /* to the extent of the clipped block */
                    double dfXInClippingUnits, dfYInClippingUnits,
                        dfReqWidthInClippingUnits, dfReqHeightInClippingUnits;

                    dfXInClippingUnits = (dfIntersectMinX - dfClippingMinX) /
                                         adfClippingGeoTransform[1];
                    if (adfClippingGeoTransform[5] < 0)
                        dfYInClippingUnits =
                            (dfClippingMaxY - dfIntersectMaxY) /
                            (-adfClippingGeoTransform[5]);
                    else
                        dfYInClippingUnits =
                            (dfIntersectMinY - dfClippingMinY) /
                            adfClippingGeoTransform[5];
                    dfReqWidthInClippingUnits =
                        (dfIntersectMaxX - dfClippingMinX) /
                            adfClippingGeoTransform[1] -
                        dfXInClippingUnits;
                    if (adfClippingGeoTransform[5] < 0)
                        dfReqHeightInClippingUnits =
                            (dfClippingMaxY - dfIntersectMinY) /
                                (-adfClippingGeoTransform[5]) -
                            dfYInClippingUnits;
                    else
                        dfReqHeightInClippingUnits =
                            (dfIntersectMaxY - dfClippingMinY) /
                                adfClippingGeoTransform[5] -
                            dfYInClippingUnits;

                    GDALPDFImageDesc oImageDesc;
                    oImageDesc.nImageId = nImageId;
                    oImageDesc.dfXOff = dfXInClippingUnits / dfUserUnit +
                                        oPageContext.sMargins.nLeft;
                    oImageDesc.dfYOff = (nClippingHeight - dfYInClippingUnits -
                                         dfReqHeightInClippingUnits) /
                                            dfUserUnit +
                                        oPageContext.sMargins.nBottom;
                    oImageDesc.dfXSize = dfReqWidthInClippingUnits / dfUserUnit;
                    oImageDesc.dfYSize =
                        dfReqHeightInClippingUnits / dfUserUnit;

                    oRasterDesc.asImageDesc.push_back(oImageDesc);
                }
            }

            GDALDestroyScaledProgress(pScaledData);
        }
    }

    oPageContext.asRasterDesc.push_back(oRasterDesc);

    return true;
}

/************************************************************************/
/*                          WriteOGRDataSource()                        */
/************************************************************************/

bool GDALPDFWriter::WriteOGRDataSource(const char *pszOGRDataSource,
                                       const char *pszOGRDisplayField,
                                       const char *pszOGRDisplayLayerNames,
                                       const char *pszOGRLinkField,
                                       int bWriteOGRAttributes)
{
    GDALDatasetH hDS =
        GDALOpenEx(pszOGRDataSource, GDAL_OF_VECTOR | GDAL_OF_VERBOSE_ERROR,
                   nullptr, nullptr, nullptr);
    if (hDS == nullptr)
        return false;

    int iObj = 0;

    int nLayers = GDALDatasetGetLayerCount(hDS);

    char **papszLayerNames =
        CSLTokenizeString2(pszOGRDisplayLayerNames, ",", 0);

    for (int iLayer = 0; iLayer < nLayers; iLayer++)
    {
        CPLString osLayerName;
        if (CSLCount(papszLayerNames) < nLayers)
            osLayerName = OGR_L_GetName(GDALDatasetGetLayer(hDS, iLayer));
        else
            osLayerName = papszLayerNames[iLayer];

        WriteOGRLayer(hDS, iLayer, pszOGRDisplayField, pszOGRLinkField,
                      osLayerName, bWriteOGRAttributes, iObj);
    }

    GDALClose(hDS);

    CSLDestroy(papszLayerNames);

    return true;
}

/************************************************************************/
/*                           StartOGRLayer()                            */
/************************************************************************/

GDALPDFLayerDesc GDALPDFWriter::StartOGRLayer(const std::string &osLayerName,
                                              int bWriteOGRAttributes)
{
    GDALPDFLayerDesc osVectorDesc;
    osVectorDesc.osLayerName = osLayerName;
#ifdef HACK_TO_GENERATE_OCMD
    osVectorDesc.bWriteOGRAttributes = false;
    auto nParentOCGId = WriteOCG("parent");
    osVectorDesc.nOCGId = WriteOCG(osLayerName.c_str(), nParentOCGId);
#else
    osVectorDesc.bWriteOGRAttributes = bWriteOGRAttributes;
    osVectorDesc.nOCGId = WriteOCG(osLayerName.c_str());
#endif
    if (bWriteOGRAttributes)
        osVectorDesc.nFeatureLayerId = AllocNewObject();

    return osVectorDesc;
}

/************************************************************************/
/*                           EndOGRLayer()                              */
/************************************************************************/

void GDALPDFWriter::EndOGRLayer(GDALPDFLayerDesc &osVectorDesc)
{
    if (osVectorDesc.bWriteOGRAttributes)
    {
        StartObj(osVectorDesc.nFeatureLayerId);

        GDALPDFDictionaryRW oDict;
        oDict.Add("A", &(new GDALPDFDictionaryRW())
                            ->Add("O", GDALPDFObjectRW::CreateName(
                                           "UserProperties")));

        GDALPDFArrayRW *poArray = new GDALPDFArrayRW();
        oDict.Add("K", poArray);

        for (const auto &prop : osVectorDesc.aUserPropertiesIds)
        {
            poArray->Add(prop, 0);
        }

        if (!m_nStructTreeRootId.toBool())
            m_nStructTreeRootId = AllocNewObject();

        oDict.Add("P", m_nStructTreeRootId, 0);
        oDict.Add("S", GDALPDFObjectRW::CreateName("Feature"));
        oDict.Add("T", osVectorDesc.osLayerName);

        VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());

        EndObj();
    }

    oPageContext.asVectorDesc.push_back(osVectorDesc);
}

/************************************************************************/
/*                           WriteOGRLayer()                            */
/************************************************************************/

int GDALPDFWriter::WriteOGRLayer(GDALDatasetH hDS, int iLayer,
                                 const char *pszOGRDisplayField,
                                 const char *pszOGRLinkField,
                                 const std::string &osLayerName,
                                 int bWriteOGRAttributes, int &iObj)
{
    GDALDataset *poClippingDS = oPageContext.poClippingDS;
    double adfGeoTransform[6];
    if (poClippingDS->GetGeoTransform(adfGeoTransform) != CE_None)
        return FALSE;

    GDALPDFLayerDesc osVectorDesc =
        StartOGRLayer(osLayerName, bWriteOGRAttributes);
    OGRLayerH hLyr = GDALDatasetGetLayer(hDS, iLayer);

    const auto poLayerDefn = OGRLayer::FromHandle(hLyr)->GetLayerDefn();
    for (int i = 0; i < poLayerDefn->GetFieldCount(); i++)
    {
        const auto poFieldDefn = poLayerDefn->GetFieldDefn(i);
        const char *pszName = poFieldDefn->GetNameRef();
        osVectorDesc.aosIncludedFields.push_back(pszName);
    }

    OGRSpatialReferenceH hGDAL_SRS = OGRSpatialReference::ToHandle(
        const_cast<OGRSpatialReference *>(poClippingDS->GetSpatialRef()));
    OGRSpatialReferenceH hOGR_SRS = OGR_L_GetSpatialRef(hLyr);
    OGRCoordinateTransformationH hCT = nullptr;

    if (hGDAL_SRS == nullptr && hOGR_SRS != nullptr)
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "Vector layer has a SRS set, but Raster layer has no SRS set. "
                 "Assuming they are the same.");
    }
    else if (hGDAL_SRS != nullptr && hOGR_SRS == nullptr)
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "Vector layer has no SRS set, but Raster layer has a SRS set. "
                 "Assuming they are the same.");
    }
    else if (hGDAL_SRS != nullptr && hOGR_SRS != nullptr)
    {
        if (!OSRIsSame(hGDAL_SRS, hOGR_SRS))
        {
            hCT = OCTNewCoordinateTransformation(hOGR_SRS, hGDAL_SRS);
            if (hCT == nullptr)
            {
                CPLError(CE_Warning, CPLE_AppDefined,
                         "Cannot compute coordinate transformation from vector "
                         "SRS to raster SRS");
            }
        }
    }

    if (hCT == nullptr)
    {
        double dfXMin = adfGeoTransform[0];
        double dfYMin = adfGeoTransform[3] +
                        poClippingDS->GetRasterYSize() * adfGeoTransform[5];
        double dfXMax = adfGeoTransform[0] +
                        poClippingDS->GetRasterXSize() * adfGeoTransform[1];
        double dfYMax = adfGeoTransform[3];
        OGR_L_SetSpatialFilterRect(hLyr, dfXMin, dfYMin, dfXMax, dfYMax);
    }

    OGRFeatureH hFeat;

    while ((hFeat = OGR_L_GetNextFeature(hLyr)) != nullptr)
    {
        WriteOGRFeature(osVectorDesc, hFeat, hCT, pszOGRDisplayField,
                        pszOGRLinkField, bWriteOGRAttributes, iObj);

        OGR_F_Destroy(hFeat);
    }

    EndOGRLayer(osVectorDesc);

    if (hCT != nullptr)
        OCTDestroyCoordinateTransformation(hCT);

    return TRUE;
}

/************************************************************************/
/*                             DrawGeometry()                           */
/************************************************************************/

static void DrawGeometry(CPLString &osDS, OGRGeometryH hGeom,
                         const double adfMatrix[4], bool bPaint = true)
{
    switch (wkbFlatten(OGR_G_GetGeometryType(hGeom)))
    {
        case wkbLineString:
        {
            int nPoints = OGR_G_GetPointCount(hGeom);
            for (int i = 0; i < nPoints; i++)
            {
                double dfX = OGR_G_GetX(hGeom, i) * adfMatrix[1] + adfMatrix[0];
                double dfY = OGR_G_GetY(hGeom, i) * adfMatrix[3] + adfMatrix[2];
                osDS +=
                    CPLOPrintf("%f %f %c\n", dfX, dfY, (i == 0) ? 'm' : 'l');
            }
            if (bPaint)
                osDS += CPLOPrintf("S\n");
            break;
        }

        case wkbPolygon:
        {
            int nParts = OGR_G_GetGeometryCount(hGeom);
            for (int i = 0; i < nParts; i++)
            {
                DrawGeometry(osDS, OGR_G_GetGeometryRef(hGeom, i), adfMatrix,
                             false);
                osDS += CPLOPrintf("h\n");
            }
            if (bPaint)
                osDS += CPLOPrintf("b*\n");
            break;
        }

        case wkbMultiLineString:
        {
            int nParts = OGR_G_GetGeometryCount(hGeom);
            for (int i = 0; i < nParts; i++)
            {
                DrawGeometry(osDS, OGR_G_GetGeometryRef(hGeom, i), adfMatrix,
                             false);
            }
            if (bPaint)
                osDS += CPLOPrintf("S\n");
            break;
        }

        case wkbMultiPolygon:
        {
            int nParts = OGR_G_GetGeometryCount(hGeom);
            for (int i = 0; i < nParts; i++)
            {
                DrawGeometry(osDS, OGR_G_GetGeometryRef(hGeom, i), adfMatrix,
                             false);
            }
            if (bPaint)
                osDS += CPLOPrintf("b*\n");
            break;
        }

        default:
            break;
    }
}

/************************************************************************/
/*                           CalculateText()                            */
/************************************************************************/

static void CalculateText(const CPLString &osText, CPLString &osFont,
                          const double dfSize, const bool bBold,
                          const bool bItalic, double &dfWidth, double &dfHeight)
{
    // Character widths of Helvetica, Win-1252 characters 32 to 255
    // Helvetica bold, oblique and bold oblique have their own widths,
    // but for now we will put up with these widths on all Helvetica variants
    constexpr GUInt16 anHelveticaCharWidths[] = {
        569,  569,  727,  1139, 1139, 1821, 1366, 391,  682,  682,  797,  1196,
        569,  682,  569,  569,  1139, 1139, 1139, 1139, 1139, 1139, 1139, 1139,
        1139, 1139, 569,  569,  1196, 1196, 1196, 1139, 2079, 1366, 1366, 1479,
        1479, 1366, 1251, 1593, 1479, 569,  1024, 1366, 1139, 1706, 1479, 1593,
        1366, 1593, 1479, 1366, 1251, 1479, 1366, 1933, 1366, 1366, 1251, 569,
        569,  569,  961,  1139, 682,  1139, 1139, 1024, 1139, 1139, 569,  1139,
        1139, 455,  455,  1024, 455,  1706, 1139, 1139, 1139, 1139, 682,  1024,
        569,  1139, 1024, 1479, 1024, 1024, 1024, 684,  532,  684,  1196, 1536,
        1139, 2048, 455,  1139, 682,  2048, 1139, 1139, 682,  2048, 1366, 682,
        2048, 2048, 1251, 2048, 2048, 455,  455,  682,  682,  717,  1139, 2048,
        682,  2048, 1024, 682,  1933, 2048, 1024, 1366, 569,  682,  1139, 1139,
        1139, 1139, 532,  1139, 682,  1509, 758,  1139, 1196, 682,  1509, 1131,
        819,  1124, 682,  682,  682,  1180, 1100, 682,  682,  682,  748,  1139,
        1708, 1708, 1708, 1251, 1366, 1366, 1366, 1366, 1366, 1366, 2048, 1479,
        1366, 1366, 1366, 1366, 569,  569,  569,  569,  1479, 1479, 1593, 1593,
        1593, 1593, 1593, 1196, 1593, 1479, 1479, 1479, 1479, 1366, 1366, 1251,
        1139, 1139, 1139, 1139, 1139, 1139, 1821, 1024, 1139, 1139, 1139, 1139,
        569,  569,  569,  569,  1139, 1139, 1139, 1139, 1139, 1139, 1139, 1124,
        1251, 1139, 1139, 1139, 1139, 1024, 1139, 1024};

    // Character widths of Times-Roman, Win-1252 characters 32 to 255
    // Times bold, italic and bold italic have their own widths,
    // but for now we will put up with these widths on all Times variants
    constexpr GUInt16 anTimesCharWidths[] = {
        512,  682,  836,  1024, 1024, 1706, 1593, 369,  682,  682,  1024, 1155,
        512,  682,  512,  569,  1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024,
        1024, 1024, 569,  569,  1155, 1155, 1155, 909,  1886, 1479, 1366, 1366,
        1479, 1251, 1139, 1479, 1479, 682,  797,  1479, 1251, 1821, 1479, 1479,
        1139, 1479, 1366, 1139, 1251, 1479, 1479, 1933, 1479, 1479, 1251, 682,
        569,  682,  961,  1024, 682,  909,  1024, 909,  1024, 909,  682,  1024,
        1024, 569,  569,  1024, 569,  1593, 1024, 1024, 1024, 1024, 682,  797,
        569,  1024, 1024, 1479, 1024, 1024, 909,  983,  410,  983,  1108, 0,
        1024, 2048, 682,  1024, 909,  2048, 1024, 1024, 682,  2048, 1139, 682,
        1821, 2048, 1251, 2048, 2048, 682,  682,  909,  909,  717,  1024, 2048,
        682,  2007, 797,  682,  1479, 2048, 909,  1479, 512,  682,  1024, 1024,
        1024, 1024, 410,  1024, 682,  1556, 565,  1024, 1155, 682,  1556, 1024,
        819,  1124, 614,  614,  682,  1180, 928,  682,  682,  614,  635,  1024,
        1536, 1536, 1536, 909,  1479, 1479, 1479, 1479, 1479, 1479, 1821, 1366,
        1251, 1251, 1251, 1251, 682,  682,  682,  682,  1479, 1479, 1479, 1479,
        1479, 1479, 1479, 1155, 1479, 1479, 1479, 1479, 1479, 1479, 1139, 1024,
        909,  909,  909,  909,  909,  909,  1366, 909,  909,  909,  909,  909,
        569,  569,  569,  569,  1024, 1024, 1024, 1024, 1024, 1024, 1024, 1124,
        1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024};

    const GUInt16 *panCharacterWidths = nullptr;

    if (STARTS_WITH_CI(osFont, "times") ||
        osFont.find("Serif", 0) != std::string::npos)
    {
        if (bBold && bItalic)
            osFont = "Times-BoldItalic";
        else if (bBold)
            osFont = "Times-Bold";
        else if (bItalic)
            osFont = "Times-Italic";
        else
            osFont = "Times-Roman";

        panCharacterWidths = anTimesCharWidths;
        dfHeight = dfSize * 1356.0 / 2048;
    }
    else if (STARTS_WITH_CI(osFont, "courier") ||
             osFont.find("Mono", 0) != std::string::npos)
    {
        if (bBold && bItalic)
            osFont = "Courier-BoldOblique";
        else if (bBold)
            osFont = "Courier-Bold";
        else if (bItalic)
            osFont = "Courier-Oblique";
        else
            osFont = "Courier";

        dfHeight = dfSize * 1170.0 / 2048;
    }
    else
    {
        if (bBold && bItalic)
            osFont = "Helvetica-BoldOblique";
        else if (bBold)
            osFont = "Helvetica-Bold";
        else if (bItalic)
            osFont = "Helvetica-Oblique";
        else
            osFont = "Helvetica";

        panCharacterWidths = anHelveticaCharWidths;
        dfHeight = dfSize * 1467.0 / 2048;
    }

    dfWidth = 0.0;
    for (const char &ch : osText)
    {
        const int nCh = static_cast<int>(ch);
        if (nCh < 32)
            continue;

        dfWidth +=
            (panCharacterWidths ? panCharacterWidths[nCh - 32]
                                : 1229);  // Courier's fixed character width
    }
    dfWidth *= dfSize / 2048;
}

/************************************************************************/
/*                          GetObjectStyle()                            */
/************************************************************************/

void GDALPDFBaseWriter::GetObjectStyle(
    const char *pszStyleString, OGRFeatureH hFeat, const double adfMatrix[4],
    std::map<CPLString, GDALPDFImageDesc> oMapSymbolFilenameToDesc,
    ObjectStyle &os)
{
    OGRStyleMgrH hSM = OGR_SM_Create(nullptr);
    if (pszStyleString)
        OGR_SM_InitStyleString(hSM, pszStyleString);
    else
        OGR_SM_InitFromFeature(hSM, hFeat);
    int nCount = OGR_SM_GetPartCount(hSM, nullptr);
    for (int iPart = 0; iPart < nCount; iPart++)
    {
        OGRStyleToolH hTool = OGR_SM_GetPart(hSM, iPart, nullptr);
        if (hTool)
        {
            // Figure out how to involve adfMatrix[3] here and below
            OGR_ST_SetUnit(hTool, OGRSTUMM, 1000.0 / adfMatrix[1]);
            if (OGR_ST_GetType(hTool) == OGRSTCPen)
            {
                os.bHasPenBrushOrSymbol = true;

                int bIsNull = TRUE;
                const char *pszColor =
                    OGR_ST_GetParamStr(hTool, OGRSTPenColor, &bIsNull);
                if (pszColor && !bIsNull)
                {
                    unsigned int nRed = 0;
                    unsigned int nGreen = 0;
                    unsigned int nBlue = 0;
                    unsigned int nAlpha = 255;
                    int nVals = sscanf(pszColor, "#%2x%2x%2x%2x", &nRed,
                                       &nGreen, &nBlue, &nAlpha);
                    if (nVals >= 3)
                    {
                        os.nPenR = nRed;
                        os.nPenG = nGreen;
                        os.nPenB = nBlue;
                        if (nVals == 4)
                            os.nPenA = nAlpha;
                    }
                }

                const char *pszDash =
                    OGR_ST_GetParamStr(hTool, OGRSTPenPattern, &bIsNull);
                if (pszDash && !bIsNull)
                {
                    char **papszTokens = CSLTokenizeString2(pszDash, " ", 0);
                    int nTokens = CSLCount(papszTokens);
                    if ((nTokens % 2) == 0)
                    {
                        for (int i = 0; i < nTokens; i++)
                        {
                            double dfElement = CPLAtof(papszTokens[i]);
                            dfElement *= adfMatrix[1];  // should involve
                                                        // adfMatrix[3] too
                            os.osDashArray += CPLSPrintf("%f ", dfElement);
                        }
                    }
                    CSLDestroy(papszTokens);
                }

                // OGRSTUnitId eUnit = OGR_ST_GetUnit(hTool);
                double dfWidth =
                    OGR_ST_GetParamDbl(hTool, OGRSTPenWidth, &bIsNull);
                if (!bIsNull)
                    os.dfPenWidth = dfWidth;
            }
            else if (OGR_ST_GetType(hTool) == OGRSTCBrush)
            {
                os.bHasPenBrushOrSymbol = true;

                int bIsNull;
                const char *pszColor =
                    OGR_ST_GetParamStr(hTool, OGRSTBrushFColor, &bIsNull);
                if (pszColor)
                {
                    unsigned int nRed = 0;
                    unsigned int nGreen = 0;
                    unsigned int nBlue = 0;
                    unsigned int nAlpha = 255;
                    int nVals = sscanf(pszColor, "#%2x%2x%2x%2x", &nRed,
                                       &nGreen, &nBlue, &nAlpha);
                    if (nVals >= 3)
                    {
                        os.nBrushR = nRed;
                        os.nBrushG = nGreen;
                        os.nBrushB = nBlue;
                        if (nVals == 4)
                            os.nBrushA = nAlpha;
                    }
                }
            }
            else if (OGR_ST_GetType(hTool) == OGRSTCLabel)
            {
                int bIsNull;
                const char *pszStr =
                    OGR_ST_GetParamStr(hTool, OGRSTLabelTextString, &bIsNull);
                if (pszStr)
                {
                    os.osLabelText = pszStr;

                    /* If the text is of the form {stuff}, then it means we want
                     * to fetch */
                    /* the value of the field "stuff" in the feature */
                    if (!os.osLabelText.empty() && os.osLabelText[0] == '{' &&
                        os.osLabelText.back() == '}')
                    {
                        os.osLabelText = pszStr + 1;
                        os.osLabelText.resize(os.osLabelText.size() - 1);

                        int nIdxField =
                            OGR_F_GetFieldIndex(hFeat, os.osLabelText);
                        if (nIdxField >= 0)
                            os.osLabelText =
                                OGR_F_GetFieldAsString(hFeat, nIdxField);
                        else
                            os.osLabelText = "";
                    }
                }

                const char *pszColor =
                    OGR_ST_GetParamStr(hTool, OGRSTLabelFColor, &bIsNull);
                if (pszColor && !bIsNull)
                {
                    unsigned int nRed = 0;
                    unsigned int nGreen = 0;
                    unsigned int nBlue = 0;
                    unsigned int nAlpha = 255;
                    int nVals = sscanf(pszColor, "#%2x%2x%2x%2x", &nRed,
                                       &nGreen, &nBlue, &nAlpha);
                    if (nVals >= 3)
                    {
                        os.nTextR = nRed;
                        os.nTextG = nGreen;
                        os.nTextB = nBlue;
                        if (nVals == 4)
                            os.nTextA = nAlpha;
                    }
                }

                pszStr =
                    OGR_ST_GetParamStr(hTool, OGRSTLabelFontName, &bIsNull);
                if (pszStr && !bIsNull)
                    os.osTextFont = pszStr;

                double dfVal =
                    OGR_ST_GetParamDbl(hTool, OGRSTLabelSize, &bIsNull);
                if (!bIsNull)
                    os.dfTextSize = dfVal;

                dfVal = OGR_ST_GetParamDbl(hTool, OGRSTLabelAngle, &bIsNull);
                if (!bIsNull)
                    os.dfTextAngle = dfVal * M_PI / 180.0;

                dfVal = OGR_ST_GetParamDbl(hTool, OGRSTLabelStretch, &bIsNull);
                if (!bIsNull)
                    os.dfTextStretch = dfVal / 100.0;

                dfVal = OGR_ST_GetParamDbl(hTool, OGRSTLabelDx, &bIsNull);
                if (!bIsNull)
                    os.dfTextDx = dfVal;

                dfVal = OGR_ST_GetParamDbl(hTool, OGRSTLabelDy, &bIsNull);
                if (!bIsNull)
                    os.dfTextDy = dfVal;

                int nVal =
                    OGR_ST_GetParamNum(hTool, OGRSTLabelAnchor, &bIsNull);
                if (!bIsNull)
                    os.nTextAnchor = nVal;

                nVal = OGR_ST_GetParamNum(hTool, OGRSTLabelBold, &bIsNull);
                if (!bIsNull)
                    os.bTextBold = (nVal != 0);

                nVal = OGR_ST_GetParamNum(hTool, OGRSTLabelItalic, &bIsNull);
                if (!bIsNull)
                    os.bTextItalic = (nVal != 0);
            }
            else if (OGR_ST_GetType(hTool) == OGRSTCSymbol)
            {
                os.bHasPenBrushOrSymbol = true;

                int bIsNull;
                const char *pszSymbolId =
                    OGR_ST_GetParamStr(hTool, OGRSTSymbolId, &bIsNull);
                if (pszSymbolId && !bIsNull)
                {
                    os.osSymbolId = pszSymbolId;

                    if (strstr(pszSymbolId, "ogr-sym-") == nullptr)
                    {
                        if (oMapSymbolFilenameToDesc.find(os.osSymbolId) ==
                            oMapSymbolFilenameToDesc.end())
                        {
                            CPLPushErrorHandler(CPLQuietErrorHandler);
                            GDALDatasetH hImageDS =
                                GDALOpen(os.osSymbolId, GA_ReadOnly);
                            CPLPopErrorHandler();
                            if (hImageDS != nullptr)
                            {
                                os.nImageWidth = GDALGetRasterXSize(hImageDS);
                                os.nImageHeight = GDALGetRasterYSize(hImageDS);

                                os.nImageSymbolId = WriteBlock(
                                    GDALDataset::FromHandle(hImageDS), 0, 0,
                                    os.nImageWidth, os.nImageHeight,
                                    GDALPDFObjectNum(), COMPRESS_DEFAULT, 0, -1,
                                    nullptr, nullptr, nullptr);
                                GDALClose(hImageDS);
                            }

                            GDALPDFImageDesc oDesc;
                            oDesc.nImageId = os.nImageSymbolId;
                            oDesc.dfXOff = 0;
                            oDesc.dfYOff = 0;
                            oDesc.dfXSize = os.nImageWidth;
                            oDesc.dfYSize = os.nImageHeight;
                            oMapSymbolFilenameToDesc[os.osSymbolId] = oDesc;
                        }
                        else
                        {
                            const GDALPDFImageDesc &oDesc =
                                oMapSymbolFilenameToDesc[os.osSymbolId];
                            os.nImageSymbolId = oDesc.nImageId;
                            os.nImageWidth = static_cast<int>(oDesc.dfXSize);
                            os.nImageHeight = static_cast<int>(oDesc.dfYSize);
                        }
                    }
                }

                double dfVal =
                    OGR_ST_GetParamDbl(hTool, OGRSTSymbolSize, &bIsNull);
                if (!bIsNull)
                {
                    os.dfSymbolSize = dfVal;
                }

                const char *pszColor =
                    OGR_ST_GetParamStr(hTool, OGRSTSymbolColor, &bIsNull);
                if (pszColor && !bIsNull)
                {
                    unsigned int nRed = 0;
                    unsigned int nGreen = 0;
                    unsigned int nBlue = 0;
                    unsigned int nAlpha = 255;
                    int nVals = sscanf(pszColor, "#%2x%2x%2x%2x", &nRed,
                                       &nGreen, &nBlue, &nAlpha);
                    if (nVals >= 3)
                    {
                        os.bSymbolColorDefined = TRUE;
                        os.nSymbolR = nRed;
                        os.nSymbolG = nGreen;
                        os.nSymbolB = nBlue;
                        if (nVals == 4)
                            os.nSymbolA = nAlpha;
                    }
                }
            }

            OGR_ST_Destroy(hTool);
        }
    }
    OGR_SM_Destroy(hSM);

    OGRGeometryH hGeom = OGR_F_GetGeometryRef(hFeat);
    if (wkbFlatten(OGR_G_GetGeometryType(hGeom)) == wkbPoint &&
        os.bSymbolColorDefined)
    {
        os.nPenR = os.nSymbolR;
        os.nPenG = os.nSymbolG;
        os.nPenB = os.nSymbolB;
        os.nPenA = os.nSymbolA;
        os.nBrushR = os.nSymbolR;
        os.nBrushG = os.nSymbolG;
        os.nBrushB = os.nSymbolB;
        os.nBrushA = os.nSymbolA;
    }
}

/************************************************************************/
/*                           ComputeIntBBox()                           */
/************************************************************************/

void GDALPDFBaseWriter::ComputeIntBBox(
    OGRGeometryH hGeom, const OGREnvelope &sEnvelope, const double adfMatrix[4],
    const GDALPDFWriter::ObjectStyle &os, double dfRadius, int &bboxXMin,
    int &bboxYMin, int &bboxXMax, int &bboxYMax)
{
    if (wkbFlatten(OGR_G_GetGeometryType(hGeom)) == wkbPoint &&
        os.nImageSymbolId.toBool())
    {
        const double dfSemiWidth =
            (os.nImageWidth >= os.nImageHeight)
                ? dfRadius
                : dfRadius * os.nImageWidth / os.nImageHeight;
        const double dfSemiHeight =
            (os.nImageWidth >= os.nImageHeight)
                ? dfRadius * os.nImageHeight / os.nImageWidth
                : dfRadius;
        bboxXMin = static_cast<int>(
            floor(sEnvelope.MinX * adfMatrix[1] + adfMatrix[0] - dfSemiWidth));
        bboxYMin = static_cast<int>(
            floor(sEnvelope.MinY * adfMatrix[3] + adfMatrix[2] - dfSemiHeight));
        bboxXMax = static_cast<int>(
            ceil(sEnvelope.MaxX * adfMatrix[1] + adfMatrix[0] + dfSemiWidth));
        bboxYMax = static_cast<int>(
            ceil(sEnvelope.MaxY * adfMatrix[3] + adfMatrix[2] + dfSemiHeight));
    }
    else
    {
        double dfMargin = os.dfPenWidth;
        if (wkbFlatten(OGR_G_GetGeometryType(hGeom)) == wkbPoint)
        {
            if (os.osSymbolId == "ogr-sym-6" || os.osSymbolId == "ogr-sym-7")
            {
                const double dfSqrt3 = 1.73205080757;
                dfMargin += dfRadius * 2 * dfSqrt3 / 3;
            }
            else
                dfMargin += dfRadius;
        }
        bboxXMin = static_cast<int>(
            floor(sEnvelope.MinX * adfMatrix[1] + adfMatrix[0] - dfMargin));
        bboxYMin = static_cast<int>(
            floor(sEnvelope.MinY * adfMatrix[3] + adfMatrix[2] - dfMargin));
        bboxXMax = static_cast<int>(
            ceil(sEnvelope.MaxX * adfMatrix[1] + adfMatrix[0] + dfMargin));
        bboxYMax = static_cast<int>(
            ceil(sEnvelope.MaxY * adfMatrix[3] + adfMatrix[2] + dfMargin));
    }
}

/************************************************************************/
/*                              WriteLink()                             */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::WriteLink(OGRFeatureH hFeat,
                                              const char *pszOGRLinkField,
                                              const double adfMatrix[4],
                                              int bboxXMin, int bboxYMin,
                                              int bboxXMax, int bboxYMax)
{
    GDALPDFObjectNum nAnnotId;
    int iField = -1;
    const char *pszLinkVal = nullptr;
    if (pszOGRLinkField != nullptr &&
        (iField = OGR_FD_GetFieldIndex(OGR_F_GetDefnRef(hFeat),
                                       pszOGRLinkField)) >= 0 &&
        OGR_F_IsFieldSetAndNotNull(hFeat, iField) &&
        strcmp((pszLinkVal = OGR_F_GetFieldAsString(hFeat, iField)), "") != 0)
    {
        nAnnotId = AllocNewObject();
        StartObj(nAnnotId);
        {
            GDALPDFDictionaryRW oDict;
            oDict.Add("Type", GDALPDFObjectRW::CreateName("Annot"));
            oDict.Add("Subtype", GDALPDFObjectRW::CreateName("Link"));
            oDict.Add("Rect", &(new GDALPDFArrayRW())
                                   ->Add(bboxXMin)
                                   .Add(bboxYMin)
                                   .Add(bboxXMax)
                                   .Add(bboxYMax));
            oDict.Add("A", &(new GDALPDFDictionaryRW())
                                ->Add("S", GDALPDFObjectRW::CreateName("URI"))
                                .Add("URI", pszLinkVal));
            oDict.Add("BS",
                      &(new GDALPDFDictionaryRW())
                           ->Add("Type", GDALPDFObjectRW::CreateName("Border"))
                           .Add("S", GDALPDFObjectRW::CreateName("S"))
                           .Add("W", 0));
            oDict.Add("Border", &(new GDALPDFArrayRW())->Add(0).Add(0).Add(0));
            oDict.Add("H", GDALPDFObjectRW::CreateName("I"));

            OGRGeometryH hGeom = OGR_F_GetGeometryRef(hFeat);
            if (wkbFlatten(OGR_G_GetGeometryType(hGeom)) == wkbPolygon &&
                OGR_G_GetGeometryCount(hGeom) == 1)
            {
                OGRGeometryH hSubGeom = OGR_G_GetGeometryRef(hGeom, 0);
                int nPoints = OGR_G_GetPointCount(hSubGeom);
                if (nPoints == 4 || nPoints == 5)
                {
                    std::vector<double> adfX, adfY;
                    for (int i = 0; i < nPoints; i++)
                    {
                        double dfX = OGR_G_GetX(hSubGeom, i) * adfMatrix[1] +
                                     adfMatrix[0];
                        double dfY = OGR_G_GetY(hSubGeom, i) * adfMatrix[3] +
                                     adfMatrix[2];
                        adfX.push_back(dfX);
                        adfY.push_back(dfY);
                    }
                    if (nPoints == 4)
                    {
                        oDict.Add("QuadPoints", &(new GDALPDFArrayRW())
                                                     ->Add(adfX[0])
                                                     .Add(adfY[0])
                                                     .Add(adfX[1])
                                                     .Add(adfY[1])
                                                     .Add(adfX[2])
                                                     .Add(adfY[2])
                                                     .Add(adfX[0])
                                                     .Add(adfY[0]));
                    }
                    else if (nPoints == 5)
                    {
                        oDict.Add("QuadPoints", &(new GDALPDFArrayRW())
                                                     ->Add(adfX[0])
                                                     .Add(adfY[0])
                                                     .Add(adfX[1])
                                                     .Add(adfY[1])
                                                     .Add(adfX[2])
                                                     .Add(adfY[2])
                                                     .Add(adfX[3])
                                                     .Add(adfY[3]));
                    }
                }
            }

            VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());
        }
        EndObj();
    }
    return nAnnotId;
}

/************************************************************************/
/*                        GenerateDrawingStream()                       */
/************************************************************************/

CPLString GDALPDFBaseWriter::GenerateDrawingStream(OGRGeometryH hGeom,
                                                   const double adfMatrix[4],
                                                   ObjectStyle &os,
                                                   double dfRadius)
{
    CPLString osDS;

    if (!os.nImageSymbolId.toBool())
    {
        osDS += CPLOPrintf("%f w\n"
                           "0 J\n"
                           "0 j\n"
                           "10 M\n"
                           "[%s]0 d\n",
                           os.dfPenWidth, os.osDashArray.c_str());

        osDS += CPLOPrintf("%f %f %f RG\n", os.nPenR / 255.0, os.nPenG / 255.0,
                           os.nPenB / 255.0);
        osDS += CPLOPrintf("%f %f %f rg\n", os.nBrushR / 255.0,
                           os.nBrushG / 255.0, os.nBrushB / 255.0);
    }

    if ((os.bHasPenBrushOrSymbol || os.osLabelText.empty()) &&
        wkbFlatten(OGR_G_GetGeometryType(hGeom)) == wkbPoint)
    {
        double dfX = OGR_G_GetX(hGeom, 0) * adfMatrix[1] + adfMatrix[0];
        double dfY = OGR_G_GetY(hGeom, 0) * adfMatrix[3] + adfMatrix[2];

        if (os.nImageSymbolId.toBool())
        {
            const double dfSemiWidth =
                (os.nImageWidth >= os.nImageHeight)
                    ? dfRadius
                    : dfRadius * os.nImageWidth / os.nImageHeight;
            const double dfSemiHeight =
                (os.nImageWidth >= os.nImageHeight)
                    ? dfRadius * os.nImageHeight / os.nImageWidth
                    : dfRadius;
            osDS += CPLOPrintf("%f 0 0 %f %f %f cm\n", 2 * dfSemiWidth,
                               2 * dfSemiHeight, dfX - dfSemiWidth,
                               dfY - dfSemiHeight);
            osDS += CPLOPrintf("/SymImage%d Do\n", os.nImageSymbolId.toInt());
        }
        else if (os.osSymbolId == "")
            os.osSymbolId = "ogr-sym-3"; /* symbol by default */
        else if (!(os.osSymbolId == "ogr-sym-0" ||
                   os.osSymbolId == "ogr-sym-1" ||
                   os.osSymbolId == "ogr-sym-2" ||
                   os.osSymbolId == "ogr-sym-3" ||
                   os.osSymbolId == "ogr-sym-4" ||
                   os.osSymbolId == "ogr-sym-5" ||
                   os.osSymbolId == "ogr-sym-6" ||
                   os.osSymbolId == "ogr-sym-7" ||
                   os.osSymbolId == "ogr-sym-8" ||
                   os.osSymbolId == "ogr-sym-9"))
        {
            CPLDebug("PDF", "Unhandled symbol id : %s. Using ogr-sym-3 instead",
                     os.osSymbolId.c_str());
            os.osSymbolId = "ogr-sym-3";
        }

        if (os.osSymbolId == "ogr-sym-0") /* cross (+)  */
        {
            osDS += CPLOPrintf("%f %f m\n", dfX - dfRadius, dfY);
            osDS += CPLOPrintf("%f %f l\n", dfX + dfRadius, dfY);
            osDS += CPLOPrintf("%f %f m\n", dfX, dfY - dfRadius);
            osDS += CPLOPrintf("%f %f l\n", dfX, dfY + dfRadius);
            osDS += CPLOPrintf("S\n");
        }
        else if (os.osSymbolId == "ogr-sym-1") /* diagcross (X) */
        {
            osDS += CPLOPrintf("%f %f m\n", dfX - dfRadius, dfY - dfRadius);
            osDS += CPLOPrintf("%f %f l\n", dfX + dfRadius, dfY + dfRadius);
            osDS += CPLOPrintf("%f %f m\n", dfX - dfRadius, dfY + dfRadius);
            osDS += CPLOPrintf("%f %f l\n", dfX + dfRadius, dfY - dfRadius);
            osDS += CPLOPrintf("S\n");
        }
        else if (os.osSymbolId == "ogr-sym-2" ||
                 os.osSymbolId == "ogr-sym-3") /* circle */
        {
            /* See http://www.whizkidtech.redprince.net/bezier/circle/kappa/ */
            const double dfKappa = 0.5522847498;

            osDS += CPLOPrintf("%f %f m\n", dfX - dfRadius, dfY);
            osDS +=
                CPLOPrintf("%f %f %f %f %f %f c\n", dfX - dfRadius,
                           dfY - dfRadius * dfKappa, dfX - dfRadius * dfKappa,
                           dfY - dfRadius, dfX, dfY - dfRadius);
            osDS +=
                CPLOPrintf("%f %f %f %f %f %f c\n", dfX + dfRadius * dfKappa,
                           dfY - dfRadius, dfX + dfRadius,
                           dfY - dfRadius * dfKappa, dfX + dfRadius, dfY);
            osDS +=
                CPLOPrintf("%f %f %f %f %f %f c\n", dfX + dfRadius,
                           dfY + dfRadius * dfKappa, dfX + dfRadius * dfKappa,
                           dfY + dfRadius, dfX, dfY + dfRadius);
            osDS +=
                CPLOPrintf("%f %f %f %f %f %f c\n", dfX - dfRadius * dfKappa,
                           dfY + dfRadius, dfX - dfRadius,
                           dfY + dfRadius * dfKappa, dfX - dfRadius, dfY);
            if (os.osSymbolId == "ogr-sym-2")
                osDS += CPLOPrintf("s\n"); /* not filled */
            else
                osDS += CPLOPrintf("b*\n"); /* filled */
        }
        else if (os.osSymbolId == "ogr-sym-4" ||
                 os.osSymbolId == "ogr-sym-5") /* square */
        {
            osDS += CPLOPrintf("%f %f m\n", dfX - dfRadius, dfY + dfRadius);
            osDS += CPLOPrintf("%f %f l\n", dfX + dfRadius, dfY + dfRadius);
            osDS += CPLOPrintf("%f %f l\n", dfX + dfRadius, dfY - dfRadius);
            osDS += CPLOPrintf("%f %f l\n", dfX - dfRadius, dfY - dfRadius);
            if (os.osSymbolId == "ogr-sym-4")
                osDS += CPLOPrintf("s\n"); /* not filled */
            else
                osDS += CPLOPrintf("b*\n"); /* filled */
        }
        else if (os.osSymbolId == "ogr-sym-6" ||
                 os.osSymbolId == "ogr-sym-7") /* triangle */
        {
            const double dfSqrt3 = 1.73205080757;
            osDS += CPLOPrintf("%f %f m\n", dfX - dfRadius,
                               dfY - dfRadius * dfSqrt3 / 3);
            osDS +=
                CPLOPrintf("%f %f l\n", dfX, dfY + 2 * dfRadius * dfSqrt3 / 3);
            osDS += CPLOPrintf("%f %f l\n", dfX + dfRadius,
                               dfY - dfRadius * dfSqrt3 / 3);
            if (os.osSymbolId == "ogr-sym-6")
                osDS += CPLOPrintf("s\n"); /* not filled */
            else
                osDS += CPLOPrintf("b*\n"); /* filled */
        }
        else if (os.osSymbolId == "ogr-sym-8" ||
                 os.osSymbolId == "ogr-sym-9") /* star */
        {
            const double dfSin18divSin126 = 0.38196601125;
            osDS += CPLOPrintf("%f %f m\n", dfX, dfY + dfRadius);
            for (int i = 1; i < 10; i++)
            {
                double dfFactor = ((i % 2) == 1) ? dfSin18divSin126 : 1.0;
                osDS += CPLOPrintf("%f %f l\n",
                                   dfX + cos(M_PI / 2 - i * M_PI * 36 / 180) *
                                             dfRadius * dfFactor,
                                   dfY + sin(M_PI / 2 - i * M_PI * 36 / 180) *
                                             dfRadius * dfFactor);
            }
            if (os.osSymbolId == "ogr-sym-8")
                osDS += CPLOPrintf("s\n"); /* not filled */
            else
                osDS += CPLOPrintf("b*\n"); /* filled */
        }
    }
    else
    {
        DrawGeometry(osDS, hGeom, adfMatrix);
    }

    return osDS;
}

/************************************************************************/
/*                          WriteAttributes()                           */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::WriteAttributes(
    OGRFeatureH hFeat, const std::vector<CPLString> &aosIncludedFields,
    const char *pszOGRDisplayField, int nMCID, const GDALPDFObjectNum &oParent,
    const GDALPDFObjectNum &oPage, CPLString &osOutFeatureName)
{

    int iField = -1;
    if (pszOGRDisplayField)
        iField =
            OGR_FD_GetFieldIndex(OGR_F_GetDefnRef(hFeat), pszOGRDisplayField);
    if (iField >= 0)
        osOutFeatureName = OGR_F_GetFieldAsString(hFeat, iField);
    else
        osOutFeatureName =
            CPLSPrintf("feature" CPL_FRMT_GIB, OGR_F_GetFID(hFeat));

    auto nFeatureUserProperties = AllocNewObject();
    StartObj(nFeatureUserProperties);

    GDALPDFDictionaryRW oDict;

    GDALPDFDictionaryRW *poDictA = new GDALPDFDictionaryRW();
    oDict.Add("A", poDictA);
    poDictA->Add("O", GDALPDFObjectRW::CreateName("UserProperties"));

    GDALPDFArrayRW *poArray = new GDALPDFArrayRW();
    for (const auto &fieldName : aosIncludedFields)
    {
        int i = OGR_F_GetFieldIndex(hFeat, fieldName);
        if (i >= 0 && OGR_F_IsFieldSetAndNotNull(hFeat, i))
        {
            OGRFieldDefnH hFDefn = OGR_F_GetFieldDefnRef(hFeat, i);
            GDALPDFDictionaryRW *poKV = new GDALPDFDictionaryRW();
            poKV->Add("N", OGR_Fld_GetNameRef(hFDefn));
            if (OGR_Fld_GetType(hFDefn) == OFTInteger)
                poKV->Add("V", OGR_F_GetFieldAsInteger(hFeat, i));
            else if (OGR_Fld_GetType(hFDefn) == OFTReal)
                poKV->Add("V", OGR_F_GetFieldAsDouble(hFeat, i));
            else
                poKV->Add("V", OGR_F_GetFieldAsString(hFeat, i));
            poArray->Add(poKV);
        }
    }

    poDictA->Add("P", poArray);

    oDict.Add("K", nMCID);
    oDict.Add("P", oParent, 0);
    oDict.Add("Pg", oPage, 0);
    oDict.Add("S", GDALPDFObjectRW::CreateName("feature"));
    oDict.Add("T", osOutFeatureName);

    VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());

    EndObj();

    return nFeatureUserProperties;
}

/************************************************************************/
/*                            WriteLabel()                              */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::WriteLabel(
    OGRGeometryH hGeom, const double adfMatrix[4], ObjectStyle &os,
    PDFCompressMethod eStreamCompressMethod, double bboxXMin, double bboxYMin,
    double bboxXMax, double bboxYMax)
{
    /* -------------------------------------------------------------- */
    /*  Work out the text metrics for alignment purposes              */
    /* -------------------------------------------------------------- */
    double dfWidth, dfHeight;
    CalculateText(os.osLabelText, os.osTextFont, os.dfTextSize, os.bTextBold,
                  os.bTextItalic, dfWidth, dfHeight);
    dfWidth *= os.dfTextStretch;

    if (os.nTextAnchor % 3 == 2)  // horizontal center
    {
        os.dfTextDx -= (dfWidth / 2) * cos(os.dfTextAngle);
        os.dfTextDy -= (dfWidth / 2) * sin(os.dfTextAngle);
    }
    else if (os.nTextAnchor % 3 == 0)  // right
    {
        os.dfTextDx -= dfWidth * cos(os.dfTextAngle);
        os.dfTextDy -= dfWidth * sin(os.dfTextAngle);
    }

    if (os.nTextAnchor >= 4 && os.nTextAnchor <= 6)  // vertical center
    {
        os.dfTextDx += (dfHeight / 2) * sin(os.dfTextAngle);
        os.dfTextDy -= (dfHeight / 2) * cos(os.dfTextAngle);
    }
    else if (os.nTextAnchor >= 7 && os.nTextAnchor <= 9)  // top
    {
        os.dfTextDx += dfHeight * sin(os.dfTextAngle);
        os.dfTextDy -= dfHeight * cos(os.dfTextAngle);
    }
    // modes 10,11,12 (baseline) unsupported for the time being

    /* -------------------------------------------------------------- */
    /*  Write object dictionary                                       */
    /* -------------------------------------------------------------- */
    auto nObjectId = AllocNewObject();
    GDALPDFDictionaryRW oDict;

    oDict.Add("Type", GDALPDFObjectRW::CreateName("XObject"))
        .Add("BBox", &((new GDALPDFArrayRW())->Add(bboxXMin).Add(bboxYMin))
                          .Add(bboxXMax)
                          .Add(bboxYMax))
        .Add("Subtype", GDALPDFObjectRW::CreateName("Form"));

    GDALPDFDictionaryRW *poResources = new GDALPDFDictionaryRW();

    if (os.nTextA != 255)
    {
        GDALPDFDictionaryRW *poGS1 = new GDALPDFDictionaryRW();
        poGS1->Add("Type", GDALPDFObjectRW::CreateName("ExtGState"));
        poGS1->Add("ca", (os.nTextA == 127 || os.nTextA == 128)
                             ? 0.5
                             : os.nTextA / 255.0);

        GDALPDFDictionaryRW *poExtGState = new GDALPDFDictionaryRW();
        poExtGState->Add("GS1", poGS1);

        poResources->Add("ExtGState", poExtGState);
    }

    GDALPDFDictionaryRW *poDictF1 = new GDALPDFDictionaryRW();
    poDictF1->Add("Type", GDALPDFObjectRW::CreateName("Font"));
    poDictF1->Add("BaseFont", GDALPDFObjectRW::CreateName(os.osTextFont));
    poDictF1->Add("Encoding", GDALPDFObjectRW::CreateName("WinAnsiEncoding"));
    poDictF1->Add("Subtype", GDALPDFObjectRW::CreateName("Type1"));

    GDALPDFDictionaryRW *poDictFont = new GDALPDFDictionaryRW();
    poDictFont->Add("F1", poDictF1);
    poResources->Add("Font", poDictFont);

    oDict.Add("Resources", poResources);

    StartObjWithStream(nObjectId, oDict,
                       eStreamCompressMethod != COMPRESS_NONE);

    /* -------------------------------------------------------------- */
    /*  Write object stream                                           */
    /* -------------------------------------------------------------- */

    double dfX =
        OGR_G_GetX(hGeom, 0) * adfMatrix[1] + adfMatrix[0] + os.dfTextDx;
    double dfY =
        OGR_G_GetY(hGeom, 0) * adfMatrix[3] + adfMatrix[2] + os.dfTextDy;

    VSIFPrintfL(m_fp, "q\n");
    VSIFPrintfL(m_fp, "BT\n");
    if (os.nTextA != 255)
    {
        VSIFPrintfL(m_fp, "/GS1 gs\n");
    }

    VSIFPrintfL(m_fp, "%f %f %f %f %f %f Tm\n",
                cos(os.dfTextAngle) * adfMatrix[1] * os.dfTextStretch,
                sin(os.dfTextAngle) * adfMatrix[3] * os.dfTextStretch,
                -sin(os.dfTextAngle) * adfMatrix[1],
                cos(os.dfTextAngle) * adfMatrix[3], dfX, dfY);

    VSIFPrintfL(m_fp, "%f %f %f rg\n", os.nTextR / 255.0, os.nTextG / 255.0,
                os.nTextB / 255.0);
    // The factor of adfMatrix[1] is introduced in the call to SetUnit near the
    // top of this function. Because we are handling the 2D stretch correctly in
    // Tm above, we don't need that factor here
    VSIFPrintfL(m_fp, "/F1 %f Tf\n", os.dfTextSize / adfMatrix[1]);
    VSIFPrintfL(m_fp, "(");
    for (size_t i = 0; i < os.osLabelText.size(); i++)
    {
        if (os.osLabelText[i] == '(' || os.osLabelText[i] == ')' ||
            os.osLabelText[i] == '\\')
        {
            VSIFPrintfL(m_fp, "\\%c", os.osLabelText[i]);
        }
        else
        {
            VSIFPrintfL(m_fp, "%c", os.osLabelText[i]);
        }
    }
    VSIFPrintfL(m_fp, ") Tj\n");
    VSIFPrintfL(m_fp, "ET\n");
    VSIFPrintfL(m_fp, "Q");

    EndObjWithStream();

    return nObjectId;
}

/************************************************************************/
/*                          WriteOGRFeature()                           */
/************************************************************************/

int GDALPDFWriter::WriteOGRFeature(GDALPDFLayerDesc &osVectorDesc,
                                   OGRFeatureH hFeat,
                                   OGRCoordinateTransformationH hCT,
                                   const char *pszOGRDisplayField,
                                   const char *pszOGRLinkField,
                                   int bWriteOGRAttributes, int &iObj)
{
    GDALDataset *const poClippingDS = oPageContext.poClippingDS;
    const int nHeight = poClippingDS->GetRasterYSize();
    const double dfUserUnit = oPageContext.dfDPI * USER_UNIT_IN_INCH;
    double adfGeoTransform[6];
    poClippingDS->GetGeoTransform(adfGeoTransform);

    double adfMatrix[4];
    adfMatrix[0] = -adfGeoTransform[0] / (adfGeoTransform[1] * dfUserUnit) +
                   oPageContext.sMargins.nLeft;
    adfMatrix[1] = 1.0 / (adfGeoTransform[1] * dfUserUnit);
    adfMatrix[2] = -(adfGeoTransform[3] + adfGeoTransform[5] * nHeight) /
                       (-adfGeoTransform[5] * dfUserUnit) +
                   oPageContext.sMargins.nBottom;
    adfMatrix[3] = 1.0 / (-adfGeoTransform[5] * dfUserUnit);

    OGRGeometryH hGeom = OGR_F_GetGeometryRef(hFeat);
    if (hGeom == nullptr)
    {
        return TRUE;
    }

    OGREnvelope sEnvelope;

    if (hCT != nullptr)
    {
        /* Reproject */
        if (OGR_G_Transform(hGeom, hCT) != OGRERR_NONE)
        {
            return TRUE;
        }

        OGREnvelope sRasterEnvelope;
        sRasterEnvelope.MinX = adfGeoTransform[0];
        sRasterEnvelope.MinY =
            adfGeoTransform[3] +
            poClippingDS->GetRasterYSize() * adfGeoTransform[5];
        sRasterEnvelope.MaxX =
            adfGeoTransform[0] +
            poClippingDS->GetRasterXSize() * adfGeoTransform[1];
        sRasterEnvelope.MaxY = adfGeoTransform[3];

        // Check that the reprojected geometry intersects the raster envelope.
        OGR_G_GetEnvelope(hGeom, &sEnvelope);
        if (!(sRasterEnvelope.Intersects(sEnvelope)))
        {
            return TRUE;
        }
    }
    else
    {
        OGR_G_GetEnvelope(hGeom, &sEnvelope);
    }

    /* -------------------------------------------------------------- */
    /*  Get style                                                     */
    /* -------------------------------------------------------------- */
    ObjectStyle os;
    GetObjectStyle(nullptr, hFeat, adfMatrix, m_oMapSymbolFilenameToDesc, os);

    double dfRadius = os.dfSymbolSize * dfUserUnit;

    // For a POINT with only a LABEL style string and non-empty text, we do not
    // output any geometry other than the text itself.
    const bool bLabelOnly =
        wkbFlatten(OGR_G_GetGeometryType(hGeom)) == wkbPoint &&
        !os.bHasPenBrushOrSymbol && !os.osLabelText.empty();

    /* -------------------------------------------------------------- */
    /*  Write object dictionary                                       */
    /* -------------------------------------------------------------- */
    if (!bLabelOnly)
    {
        auto nObjectId = AllocNewObject();

        osVectorDesc.aIds.push_back(nObjectId);

        int bboxXMin, bboxYMin, bboxXMax, bboxYMax;
        ComputeIntBBox(hGeom, sEnvelope, adfMatrix, os, dfRadius, bboxXMin,
                       bboxYMin, bboxXMax, bboxYMax);

        auto nLinkId = WriteLink(hFeat, pszOGRLinkField, adfMatrix, bboxXMin,
                                 bboxYMin, bboxXMax, bboxYMax);
        if (nLinkId.toBool())
            oPageContext.anAnnotationsId.push_back(nLinkId);

        GDALPDFDictionaryRW oDict;
        GDALPDFArrayRW *poBBOX = new GDALPDFArrayRW();
        poBBOX->Add(bboxXMin).Add(bboxYMin).Add(bboxXMax).Add(bboxYMax);
        oDict.Add("Type", GDALPDFObjectRW::CreateName("XObject"))
            .Add("BBox", poBBOX)
            .Add("Subtype", GDALPDFObjectRW::CreateName("Form"));

        GDALPDFDictionaryRW *poGS1 = new GDALPDFDictionaryRW();
        poGS1->Add("Type", GDALPDFObjectRW::CreateName("ExtGState"));
        if (os.nPenA != 255)
            poGS1->Add("CA", (os.nPenA == 127 || os.nPenA == 128)
                                 ? 0.5
                                 : os.nPenA / 255.0);
        if (os.nBrushA != 255)
            poGS1->Add("ca", (os.nBrushA == 127 || os.nBrushA == 128)
                                 ? 0.5
                                 : os.nBrushA / 255.0);

        GDALPDFDictionaryRW *poExtGState = new GDALPDFDictionaryRW();
        poExtGState->Add("GS1", poGS1);

        GDALPDFDictionaryRW *poResources = new GDALPDFDictionaryRW();
        poResources->Add("ExtGState", poExtGState);

        if (os.nImageSymbolId.toBool())
        {
            GDALPDFDictionaryRW *poDictXObject = new GDALPDFDictionaryRW();
            poResources->Add("XObject", poDictXObject);

            poDictXObject->Add(
                CPLSPrintf("SymImage%d", os.nImageSymbolId.toInt()),
                os.nImageSymbolId, 0);
        }

        oDict.Add("Resources", poResources);

        StartObjWithStream(nObjectId, oDict,
                           oPageContext.eStreamCompressMethod != COMPRESS_NONE);

        /* -------------------------------------------------------------- */
        /*  Write object stream                                           */
        /* -------------------------------------------------------------- */
        VSIFPrintfL(m_fp, "q\n");

        VSIFPrintfL(m_fp, "/GS1 gs\n");

        VSIFPrintfL(
            m_fp, "%s",
            GenerateDrawingStream(hGeom, adfMatrix, os, dfRadius).c_str());

        VSIFPrintfL(m_fp, "Q");

        EndObjWithStream();
    }
    else
    {
        osVectorDesc.aIds.push_back(GDALPDFObjectNum());
    }

    /* -------------------------------------------------------------- */
    /*  Write label                                                   */
    /* -------------------------------------------------------------- */
    if (!os.osLabelText.empty() &&
        wkbFlatten(OGR_G_GetGeometryType(hGeom)) == wkbPoint)
    {
        if (!osVectorDesc.nOCGTextId.toBool())
            osVectorDesc.nOCGTextId = WriteOCG("Text", osVectorDesc.nOCGId);

        int nWidth = poClippingDS->GetRasterXSize();
        double dfWidthInUserUnit = nWidth / dfUserUnit +
                                   oPageContext.sMargins.nLeft +
                                   oPageContext.sMargins.nRight;
        double dfHeightInUserUnit = nHeight / dfUserUnit +
                                    oPageContext.sMargins.nBottom +
                                    oPageContext.sMargins.nTop;
        auto nObjectId =
            WriteLabel(hGeom, adfMatrix, os, oPageContext.eStreamCompressMethod,
                       0, 0, dfWidthInUserUnit, dfHeightInUserUnit);

        osVectorDesc.aIdsText.push_back(nObjectId);
    }
    else
    {
        osVectorDesc.aIdsText.push_back(GDALPDFObjectNum());
    }

    /* -------------------------------------------------------------- */
    /*  Write feature attributes                                      */
    /* -------------------------------------------------------------- */
    GDALPDFObjectNum nFeatureUserProperties;

    CPLString osFeatureName;

    if (bWriteOGRAttributes)
    {
        nFeatureUserProperties = WriteAttributes(
            hFeat, osVectorDesc.aosIncludedFields, pszOGRDisplayField, iObj,
            osVectorDesc.nFeatureLayerId, oPageContext.nPageId, osFeatureName);
    }

    iObj++;

    osVectorDesc.aUserPropertiesIds.push_back(nFeatureUserProperties);
    osVectorDesc.aFeatureNames.push_back(osFeatureName);

    return TRUE;
}

/************************************************************************/
/*                               EndPage()                              */
/************************************************************************/

int GDALPDFWriter::EndPage(const char *pszExtraImages,
                           const char *pszExtraStream,
                           const char *pszExtraLayerName,
                           const char *pszOffLayers,
                           const char *pszExclusiveLayers)
{
    auto nLayerExtraId = WriteOCG(pszExtraLayerName);
    if (pszOffLayers)
        m_osOffLayers = pszOffLayers;
    if (pszExclusiveLayers)
        m_osExclusiveLayers = pszExclusiveLayers;

    /* -------------------------------------------------------------- */
    /*  Write extra images                                            */
    /* -------------------------------------------------------------- */
    std::vector<GDALPDFImageDesc> asExtraImageDesc;
    if (pszExtraImages)
    {
        if (GDALGetDriverCount() == 0)
            GDALAllRegister();

        char **papszExtraImagesTokens =
            CSLTokenizeString2(pszExtraImages, ",", 0);
        double dfUserUnit = oPageContext.dfDPI * USER_UNIT_IN_INCH;
        int nCount = CSLCount(papszExtraImagesTokens);
        for (int i = 0; i + 4 <= nCount; /* */)
        {
            const char *pszImageFilename = papszExtraImagesTokens[i + 0];
            double dfX = CPLAtof(papszExtraImagesTokens[i + 1]);
            double dfY = CPLAtof(papszExtraImagesTokens[i + 2]);
            double dfScale = CPLAtof(papszExtraImagesTokens[i + 3]);
            const char *pszLinkVal = nullptr;
            i += 4;
            if (i < nCount &&
                STARTS_WITH_CI(papszExtraImagesTokens[i], "link="))
            {
                pszLinkVal = papszExtraImagesTokens[i] + 5;
                i++;
            }
            auto poImageDS = std::unique_ptr<GDALDataset>(GDALDataset::Open(
                pszImageFilename, GDAL_OF_RASTER | GDAL_OF_VERBOSE_ERROR,
                nullptr, nullptr, nullptr));
            if (poImageDS)
            {
                auto nImageId = WriteBlock(
                    poImageDS.get(), 0, 0, poImageDS->GetRasterXSize(),
                    poImageDS->GetRasterYSize(), GDALPDFObjectNum(),
                    COMPRESS_DEFAULT, 0, -1, nullptr, nullptr, nullptr);

                if (nImageId.toBool())
                {
                    GDALPDFImageDesc oImageDesc;
                    oImageDesc.nImageId = nImageId;
                    oImageDesc.dfXSize =
                        poImageDS->GetRasterXSize() / dfUserUnit * dfScale;
                    oImageDesc.dfYSize =
                        poImageDS->GetRasterYSize() / dfUserUnit * dfScale;
                    oImageDesc.dfXOff = dfX;
                    oImageDesc.dfYOff = dfY;

                    asExtraImageDesc.push_back(oImageDesc);

                    if (pszLinkVal != nullptr)
                    {
                        auto nAnnotId = AllocNewObject();
                        oPageContext.anAnnotationsId.push_back(nAnnotId);
                        StartObj(nAnnotId);
                        {
                            GDALPDFDictionaryRW oDict;
                            oDict.Add("Type",
                                      GDALPDFObjectRW::CreateName("Annot"));
                            oDict.Add("Subtype",
                                      GDALPDFObjectRW::CreateName("Link"));
                            oDict.Add("Rect", &(new GDALPDFArrayRW())
                                                   ->Add(oImageDesc.dfXOff)
                                                   .Add(oImageDesc.dfYOff)
                                                   .Add(oImageDesc.dfXOff +
                                                        oImageDesc.dfXSize)
                                                   .Add(oImageDesc.dfYOff +
                                                        oImageDesc.dfYSize));
                            oDict.Add(
                                "A",
                                &(new GDALPDFDictionaryRW())
                                     ->Add("S",
                                           GDALPDFObjectRW::CreateName("URI"))
                                     .Add("URI", pszLinkVal));
                            oDict.Add(
                                "BS",
                                &(new GDALPDFDictionaryRW())
                                     ->Add("Type", GDALPDFObjectRW::CreateName(
                                                       "Border"))
                                     .Add("S", GDALPDFObjectRW::CreateName("S"))
                                     .Add("W", 0));
                            oDict.Add(
                                "Border",
                                &(new GDALPDFArrayRW())->Add(0).Add(0).Add(0));
                            oDict.Add("H", GDALPDFObjectRW::CreateName("I"));

                            VSIFPrintfL(m_fp, "%s\n",
                                        oDict.Serialize().c_str());
                        }
                        EndObj();
                    }
                }
            }
        }
        CSLDestroy(papszExtraImagesTokens);
    }

    /* -------------------------------------------------------------- */
    /*  Write content stream                                          */
    /* -------------------------------------------------------------- */
    GDALPDFDictionaryRW oDictContent;
    StartObjWithStream(oPageContext.nContentId, oDictContent,
                       oPageContext.eStreamCompressMethod != COMPRESS_NONE);

    /* -------------------------------------------------------------- */
    /*  Write drawing instructions for raster blocks                  */
    /* -------------------------------------------------------------- */
    for (size_t iRaster = 0; iRaster < oPageContext.asRasterDesc.size();
         iRaster++)
    {
        const GDALPDFRasterDesc &oDesc = oPageContext.asRasterDesc[iRaster];
        if (oDesc.nOCGRasterId.toBool())
            VSIFPrintfL(m_fp, "/OC /Lyr%d BDC\n", oDesc.nOCGRasterId.toInt());

        for (size_t iImage = 0; iImage < oDesc.asImageDesc.size(); iImage++)
        {
            VSIFPrintfL(m_fp, "q\n");
            GDALPDFObjectRW *poXSize =
                GDALPDFObjectRW::CreateReal(oDesc.asImageDesc[iImage].dfXSize);
            GDALPDFObjectRW *poYSize =
                GDALPDFObjectRW::CreateReal(oDesc.asImageDesc[iImage].dfYSize);
            GDALPDFObjectRW *poXOff =
                GDALPDFObjectRW::CreateReal(oDesc.asImageDesc[iImage].dfXOff);
            GDALPDFObjectRW *poYOff =
                GDALPDFObjectRW::CreateReal(oDesc.asImageDesc[iImage].dfYOff);
            VSIFPrintfL(
                m_fp, "%s 0 0 %s %s %s cm\n", poXSize->Serialize().c_str(),
                poYSize->Serialize().c_str(), poXOff->Serialize().c_str(),
                poYOff->Serialize().c_str());
            delete poXSize;
            delete poYSize;
            delete poXOff;
            delete poYOff;
            VSIFPrintfL(m_fp, "/Image%d Do\n",
                        oDesc.asImageDesc[iImage].nImageId.toInt());
            VSIFPrintfL(m_fp, "Q\n");
        }

        if (oDesc.nOCGRasterId.toBool())
            VSIFPrintfL(m_fp, "EMC\n");
    }

    /* -------------------------------------------------------------- */
    /*  Write drawing instructions for vector features                */
    /* -------------------------------------------------------------- */
    int iObj = 0;
    for (size_t iLayer = 0; iLayer < oPageContext.asVectorDesc.size(); iLayer++)
    {
        const GDALPDFLayerDesc &oLayerDesc = oPageContext.asVectorDesc[iLayer];

        VSIFPrintfL(m_fp, "/OC /Lyr%d BDC\n", oLayerDesc.nOCGId.toInt());

        for (size_t iVector = 0; iVector < oLayerDesc.aIds.size(); iVector++)
        {
            if (oLayerDesc.aIds[iVector].toBool())
            {
                CPLString osName = oLayerDesc.aFeatureNames[iVector];
                if (!osName.empty())
                {
                    VSIFPrintfL(m_fp, "/feature <</MCID %d>> BDC\n", iObj);
                }

                VSIFPrintfL(m_fp, "/Vector%d Do\n",
                            oLayerDesc.aIds[iVector].toInt());

                if (!osName.empty())
                {
                    VSIFPrintfL(m_fp, "EMC\n");
                }
            }

            iObj++;
        }

        VSIFPrintfL(m_fp, "EMC\n");
    }

    /* -------------------------------------------------------------- */
    /*  Write drawing instructions for labels of vector features      */
    /* -------------------------------------------------------------- */
    iObj = 0;
    for (const GDALPDFLayerDesc &oLayerDesc : oPageContext.asVectorDesc)
    {
        if (oLayerDesc.nOCGTextId.toBool())
        {
            VSIFPrintfL(m_fp, "/OC /Lyr%d BDC\n", oLayerDesc.nOCGId.toInt());
            VSIFPrintfL(m_fp, "/OC /Lyr%d BDC\n",
                        oLayerDesc.nOCGTextId.toInt());

            for (size_t iVector = 0; iVector < oLayerDesc.aIdsText.size();
                 iVector++)
            {
                if (oLayerDesc.aIdsText[iVector].toBool())
                {
                    CPLString osName = oLayerDesc.aFeatureNames[iVector];
                    if (!osName.empty())
                    {
                        VSIFPrintfL(m_fp, "/feature <</MCID %d>> BDC\n", iObj);
                    }

                    VSIFPrintfL(m_fp, "/Text%d Do\n",
                                oLayerDesc.aIdsText[iVector].toInt());

                    if (!osName.empty())
                    {
                        VSIFPrintfL(m_fp, "EMC\n");
                    }
                }

                iObj++;
            }

            VSIFPrintfL(m_fp, "EMC\n");
            VSIFPrintfL(m_fp, "EMC\n");
        }
        else
            iObj += static_cast<int>(oLayerDesc.aIds.size());
    }

    /* -------------------------------------------------------------- */
    /*  Write drawing instructions for extra content.                 */
    /* -------------------------------------------------------------- */
    if (pszExtraStream || !asExtraImageDesc.empty())
    {
        if (nLayerExtraId.toBool())
            VSIFPrintfL(m_fp, "/OC /Lyr%d BDC\n", nLayerExtraId.toInt());

        /* -------------------------------------------------------------- */
        /*  Write drawing instructions for extra images.                  */
        /* -------------------------------------------------------------- */
        for (size_t iImage = 0; iImage < asExtraImageDesc.size(); iImage++)
        {
            VSIFPrintfL(m_fp, "q\n");
            GDALPDFObjectRW *poXSize =
                GDALPDFObjectRW::CreateReal(asExtraImageDesc[iImage].dfXSize);
            GDALPDFObjectRW *poYSize =
                GDALPDFObjectRW::CreateReal(asExtraImageDesc[iImage].dfYSize);
            GDALPDFObjectRW *poXOff =
                GDALPDFObjectRW::CreateReal(asExtraImageDesc[iImage].dfXOff);
            GDALPDFObjectRW *poYOff =
                GDALPDFObjectRW::CreateReal(asExtraImageDesc[iImage].dfYOff);
            VSIFPrintfL(
                m_fp, "%s 0 0 %s %s %s cm\n", poXSize->Serialize().c_str(),
                poYSize->Serialize().c_str(), poXOff->Serialize().c_str(),
                poYOff->Serialize().c_str());
            delete poXSize;
            delete poYSize;
            delete poXOff;
            delete poYOff;
            VSIFPrintfL(m_fp, "/Image%d Do\n",
                        asExtraImageDesc[iImage].nImageId.toInt());
            VSIFPrintfL(m_fp, "Q\n");
        }

        if (pszExtraStream)
            VSIFPrintfL(m_fp, "%s\n", pszExtraStream);

        if (nLayerExtraId.toBool())
            VSIFPrintfL(m_fp, "EMC\n");
    }

    EndObjWithStream();

    /* -------------------------------------------------------------- */
    /*  Write objects for feature tree.                               */
    /* -------------------------------------------------------------- */
    if (m_nStructTreeRootId.toBool())
    {
        auto nParentTreeId = AllocNewObject();
        StartObj(nParentTreeId);
        VSIFPrintfL(m_fp, "<< /Nums [ 0 ");
        VSIFPrintfL(m_fp, "[ ");
        for (size_t iLayer = 0; iLayer < oPageContext.asVectorDesc.size();
             iLayer++)
        {
            const GDALPDFLayerDesc &oLayerDesc =
                oPageContext.asVectorDesc[iLayer];
            for (size_t iVector = 0; iVector < oLayerDesc.aIds.size();
                 iVector++)
            {
                const auto &nId = oLayerDesc.aUserPropertiesIds[iVector];
                if (nId.toBool())
                    VSIFPrintfL(m_fp, "%d 0 R ", nId.toInt());
            }
        }
        VSIFPrintfL(m_fp, " ]\n");
        VSIFPrintfL(m_fp, " ] >> \n");
        EndObj();

        StartObj(m_nStructTreeRootId);
        VSIFPrintfL(m_fp,
                    "<< "
                    "/Type /StructTreeRoot "
                    "/ParentTree %d 0 R "
                    "/K [ ",
                    nParentTreeId.toInt());
        for (size_t iLayer = 0; iLayer < oPageContext.asVectorDesc.size();
             iLayer++)
        {
            VSIFPrintfL(
                m_fp, "%d 0 R ",
                oPageContext.asVectorDesc[iLayer].nFeatureLayerId.toInt());
        }
        VSIFPrintfL(m_fp, "] >>\n");
        EndObj();
    }

    /* -------------------------------------------------------------- */
    /*  Write page resource dictionary.                               */
    /* -------------------------------------------------------------- */
    StartObj(oPageContext.nResourcesId);
    {
        GDALPDFDictionaryRW oDict;
        GDALPDFDictionaryRW *poDictXObject = new GDALPDFDictionaryRW();
        oDict.Add("XObject", poDictXObject);
        size_t iImage;
        for (const GDALPDFRasterDesc &oDesc : oPageContext.asRasterDesc)
        {
            for (iImage = 0; iImage < oDesc.asImageDesc.size(); iImage++)
            {
                poDictXObject->Add(
                    CPLSPrintf("Image%d",
                               oDesc.asImageDesc[iImage].nImageId.toInt()),
                    oDesc.asImageDesc[iImage].nImageId, 0);
            }
        }
        for (iImage = 0; iImage < asExtraImageDesc.size(); iImage++)
        {
            poDictXObject->Add(
                CPLSPrintf("Image%d",
                           asExtraImageDesc[iImage].nImageId.toInt()),
                asExtraImageDesc[iImage].nImageId, 0);
        }
        for (const GDALPDFLayerDesc &oLayerDesc : oPageContext.asVectorDesc)
        {
            for (size_t iVector = 0; iVector < oLayerDesc.aIds.size();
                 iVector++)
            {
                if (oLayerDesc.aIds[iVector].toBool())
                    poDictXObject->Add(
                        CPLSPrintf("Vector%d",
                                   oLayerDesc.aIds[iVector].toInt()),
                        oLayerDesc.aIds[iVector], 0);
            }
            for (size_t iVector = 0; iVector < oLayerDesc.aIdsText.size();
                 iVector++)
            {
                if (oLayerDesc.aIdsText[iVector].toBool())
                    poDictXObject->Add(
                        CPLSPrintf("Text%d",
                                   oLayerDesc.aIdsText[iVector].toInt()),
                        oLayerDesc.aIdsText[iVector], 0);
            }
        }

        if (pszExtraStream)
        {
            std::vector<CPLString> aosNeededFonts;
            if (strstr(pszExtraStream, "/FTimes"))
            {
                aosNeededFonts.push_back("Times-Roman");
                aosNeededFonts.push_back("Times-Bold");
                aosNeededFonts.push_back("Times-Italic");
                aosNeededFonts.push_back("Times-BoldItalic");
            }
            if (strstr(pszExtraStream, "/FHelvetica"))
            {
                aosNeededFonts.push_back("Helvetica");
                aosNeededFonts.push_back("Helvetica-Bold");
                aosNeededFonts.push_back("Helvetica-Oblique");
                aosNeededFonts.push_back("Helvetica-BoldOblique");
            }
            if (strstr(pszExtraStream, "/FCourier"))
            {
                aosNeededFonts.push_back("Courier");
                aosNeededFonts.push_back("Courier-Bold");
                aosNeededFonts.push_back("Courier-Oblique");
                aosNeededFonts.push_back("Courier-BoldOblique");
            }
            if (strstr(pszExtraStream, "/FSymbol"))
                aosNeededFonts.push_back("Symbol");
            if (strstr(pszExtraStream, "/FZapfDingbats"))
                aosNeededFonts.push_back("ZapfDingbats");

            if (!aosNeededFonts.empty())
            {
                GDALPDFDictionaryRW *poDictFont = new GDALPDFDictionaryRW();

                for (CPLString &osFont : aosNeededFonts)
                {
                    GDALPDFDictionaryRW *poDictFontInner =
                        new GDALPDFDictionaryRW();
                    poDictFontInner->Add("Type",
                                         GDALPDFObjectRW::CreateName("Font"));
                    poDictFontInner->Add("BaseFont",
                                         GDALPDFObjectRW::CreateName(osFont));
                    poDictFontInner->Add(
                        "Encoding",
                        GDALPDFObjectRW::CreateName("WinAnsiEncoding"));
                    poDictFontInner->Add("Subtype",
                                         GDALPDFObjectRW::CreateName("Type1"));

                    osFont = "F" + osFont;
                    const size_t nHyphenPos = osFont.find('-');
                    if (nHyphenPos != std::string::npos)
                        osFont.erase(nHyphenPos, 1);
                    poDictFont->Add(osFont, poDictFontInner);
                }

                oDict.Add("Font", poDictFont);
            }
        }

        if (!m_asOCGs.empty())
        {
            GDALPDFDictionaryRW *poDictProperties = new GDALPDFDictionaryRW();
#ifdef HACK_TO_GENERATE_OCMD
            GDALPDFDictionaryRW *poOCMD = new GDALPDFDictionaryRW();
            poOCMD->Add("Type", GDALPDFObjectRW::CreateName("OCMD"));
            GDALPDFArrayRW *poArray = new GDALPDFArrayRW();
            poArray->Add(m_asOCGs[0].nId, 0);
            poArray->Add(m_asOCGs[1].nId, 0);
            poOCMD->Add("OCGs", poArray);
            poDictProperties->Add(CPLSPrintf("Lyr%d", m_asOCGs[1].nId.toInt()),
                                  poOCMD);
#else
            for (size_t i = 0; i < m_asOCGs.size(); i++)
                poDictProperties->Add(
                    CPLSPrintf("Lyr%d", m_asOCGs[i].nId.toInt()),
                    m_asOCGs[i].nId, 0);
#endif
            oDict.Add("Properties", poDictProperties);
        }

        VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());
    }
    EndObj();

    /* -------------------------------------------------------------- */
    /*  Write annotation arrays.                                      */
    /* -------------------------------------------------------------- */
    StartObj(oPageContext.nAnnotsId);
    {
        GDALPDFArrayRW oArray;
        for (size_t i = 0; i < oPageContext.anAnnotationsId.size(); i++)
        {
            oArray.Add(oPageContext.anAnnotationsId[i], 0);
        }
        VSIFPrintfL(m_fp, "%s\n", oArray.Serialize().c_str());
    }
    EndObj();

    return TRUE;
}

/************************************************************************/
/*                             WriteMask()                              */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::WriteMask(GDALDataset *poSrcDS, int nXOff,
                                              int nYOff, int nReqXSize,
                                              int nReqYSize,
                                              PDFCompressMethod eCompressMethod)
{
    int nMaskSize = nReqXSize * nReqYSize;
    GByte *pabyMask = static_cast<GByte *>(VSIMalloc(nMaskSize));
    if (pabyMask == nullptr)
        return GDALPDFObjectNum();

    CPLErr eErr;
    eErr = poSrcDS->GetRasterBand(4)->RasterIO(
        GF_Read, nXOff, nYOff, nReqXSize, nReqYSize, pabyMask, nReqXSize,
        nReqYSize, GDT_Byte, 0, 0, nullptr);
    if (eErr != CE_None)
    {
        VSIFree(pabyMask);
        return GDALPDFObjectNum();
    }

    int bOnly0or255 = TRUE;
    int bOnly255 = TRUE;
    /* int bOnly0 = TRUE; */
    int i;
    for (i = 0; i < nReqXSize * nReqYSize; i++)
    {
        if (pabyMask[i] == 0)
            bOnly255 = FALSE;
        else if (pabyMask[i] == 255)
        {
            /* bOnly0 = FALSE; */
        }
        else
        {
            /* bOnly0 = FALSE; */
            bOnly255 = FALSE;
            bOnly0or255 = FALSE;
            break;
        }
    }

    if (bOnly255)
    {
        CPLFree(pabyMask);
        return GDALPDFObjectNum();
    }

    if (bOnly0or255)
    {
        /* Translate to 1 bit */
        int nReqXSize1 = (nReqXSize + 7) / 8;
        GByte *pabyMask1 =
            static_cast<GByte *>(VSICalloc(nReqXSize1, nReqYSize));
        if (pabyMask1 == nullptr)
        {
            CPLFree(pabyMask);
            return GDALPDFObjectNum();
        }
        for (int y = 0; y < nReqYSize; y++)
        {
            for (int x = 0; x < nReqXSize; x++)
            {
                if (pabyMask[y * nReqXSize + x])
                    pabyMask1[y * nReqXSize1 + x / 8] |= 1 << (7 - (x % 8));
            }
        }
        VSIFree(pabyMask);
        pabyMask = pabyMask1;
        nMaskSize = nReqXSize1 * nReqYSize;
    }

    auto nMaskId = AllocNewObject();

    GDALPDFDictionaryRW oDict;
    oDict.Add("Type", GDALPDFObjectRW::CreateName("XObject"))
        .Add("Subtype", GDALPDFObjectRW::CreateName("Image"))
        .Add("Width", nReqXSize)
        .Add("Height", nReqYSize)
        .Add("ColorSpace", GDALPDFObjectRW::CreateName("DeviceGray"))
        .Add("BitsPerComponent", (bOnly0or255) ? 1 : 8);

    StartObjWithStream(nMaskId, oDict, eCompressMethod != COMPRESS_NONE);

    VSIFWriteL(pabyMask, nMaskSize, 1, m_fp);
    CPLFree(pabyMask);

    EndObjWithStream();

    return nMaskId;
}

/************************************************************************/
/*                             WriteBlock()                             */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::WriteBlock(
    GDALDataset *poSrcDS, int nXOff, int nYOff, int nReqXSize, int nReqYSize,
    const GDALPDFObjectNum &nColorTableIdIn, PDFCompressMethod eCompressMethod,
    int nPredictor, int nJPEGQuality, const char *pszJPEG2000_DRIVER,
    GDALProgressFunc pfnProgress, void *pProgressData)
{
    int nBands = poSrcDS->GetRasterCount();
    if (nBands == 0)
        return GDALPDFObjectNum();

    GDALPDFObjectNum nColorTableId(nColorTableIdIn);
    if (!nColorTableId.toBool())
        nColorTableId = WriteColorTable(poSrcDS);

    CPLErr eErr = CE_None;
    GDALDataset *poBlockSrcDS = nullptr;
    std::unique_ptr<MEMDataset> poMEMDS;
    GByte *pabyMEMDSBuffer = nullptr;

    if (eCompressMethod == COMPRESS_DEFAULT)
    {
        GDALDataset *poSrcDSToTest = poSrcDS;

        /* Test if we can directly copy original JPEG content */
        /* if available */
        if (VRTDataset *poVRTDS = dynamic_cast<VRTDataset *>(poSrcDS))
        {
            poSrcDSToTest = poVRTDS->GetSingleSimpleSource();
        }

        if (poSrcDSToTest != nullptr && poSrcDSToTest->GetDriver() != nullptr &&
            EQUAL(poSrcDSToTest->GetDriver()->GetDescription(), "JPEG") &&
            nXOff == 0 && nYOff == 0 &&
            nReqXSize == poSrcDSToTest->GetRasterXSize() &&
            nReqYSize == poSrcDSToTest->GetRasterYSize() && nJPEGQuality < 0)
        {
            VSILFILE *fpSrc = VSIFOpenL(poSrcDSToTest->GetDescription(), "rb");
            if (fpSrc != nullptr)
            {
                CPLDebug("PDF", "Copying directly original JPEG file");

                VSIFSeekL(fpSrc, 0, SEEK_END);
                const int nLength = static_cast<int>(VSIFTellL(fpSrc));
                VSIFSeekL(fpSrc, 0, SEEK_SET);

                auto nImageId = AllocNewObject();

                StartObj(nImageId);

                GDALPDFDictionaryRW oDict;
                oDict.Add("Length", nLength)
                    .Add("Type", GDALPDFObjectRW::CreateName("XObject"))
                    .Add("Filter", GDALPDFObjectRW::CreateName("DCTDecode"))
                    .Add("Subtype", GDALPDFObjectRW::CreateName("Image"))
                    .Add("Width", nReqXSize)
                    .Add("Height", nReqYSize)
                    .Add("ColorSpace",
                         (nBands == 1)
                             ? GDALPDFObjectRW::CreateName("DeviceGray")
                             : GDALPDFObjectRW::CreateName("DeviceRGB"))
                    .Add("BitsPerComponent", 8);
                VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());
                VSIFPrintfL(m_fp, "stream\n");

                GByte abyBuffer[1024];
                for (int i = 0; i < nLength; i += 1024)
                {
                    const auto nRead = VSIFReadL(abyBuffer, 1, 1024, fpSrc);
                    if (VSIFWriteL(abyBuffer, 1, nRead, m_fp) != nRead)
                    {
                        eErr = CE_Failure;
                        break;
                    }

                    if (eErr == CE_None && pfnProgress != nullptr &&
                        !pfnProgress(double(i + nRead) / double(nLength),
                                     nullptr, pProgressData))
                    {
                        CPLError(CE_Failure, CPLE_UserInterrupt,
                                 "User terminated CreateCopy()");
                        eErr = CE_Failure;
                        break;
                    }
                }

                VSIFPrintfL(m_fp, "\nendstream\n");

                EndObj();

                VSIFCloseL(fpSrc);

                return eErr == CE_None ? nImageId : GDALPDFObjectNum();
            }
        }

        eCompressMethod = COMPRESS_DEFLATE;
    }

    GDALPDFObjectNum nMaskId;
    if (nBands == 4)
    {
        nMaskId = WriteMask(poSrcDS, nXOff, nYOff, nReqXSize, nReqYSize,
                            eCompressMethod);
    }

    if (nReqXSize == poSrcDS->GetRasterXSize() &&
        nReqYSize == poSrcDS->GetRasterYSize() && nBands != 4)
    {
        poBlockSrcDS = poSrcDS;
    }
    else
    {
        if (nBands == 4)
            nBands = 3;

        poMEMDS.reset(
            MEMDataset::Create("", nReqXSize, nReqYSize, 0, GDT_Byte, nullptr));

        pabyMEMDSBuffer =
            static_cast<GByte *>(VSIMalloc3(nReqXSize, nReqYSize, nBands));
        if (pabyMEMDSBuffer == nullptr)
        {
            return GDALPDFObjectNum();
        }

        eErr = poSrcDS->RasterIO(GF_Read, nXOff, nYOff, nReqXSize, nReqYSize,
                                 pabyMEMDSBuffer, nReqXSize, nReqYSize,
                                 GDT_Byte, nBands, nullptr, 0, 0, 0, nullptr);

        if (eErr != CE_None)
        {
            CPLFree(pabyMEMDSBuffer);
            return GDALPDFObjectNum();
        }

        int iBand;
        for (iBand = 0; iBand < nBands; iBand++)
        {
            auto hBand = MEMCreateRasterBandEx(
                poMEMDS.get(), iBand + 1,
                pabyMEMDSBuffer + iBand * nReqXSize * nReqYSize, GDT_Byte, 0, 0,
                false);
            poMEMDS->AddMEMBand(hBand);
        }

        poBlockSrcDS = poMEMDS.get();
    }

    auto nImageId = AllocNewObject();

    GDALPDFObjectNum nMeasureId;
    if (CPLTestBool(
            CPLGetConfigOption("GDAL_PDF_WRITE_GEOREF_ON_IMAGE", "FALSE")) &&
        nReqXSize == poSrcDS->GetRasterXSize() &&
        nReqYSize == poSrcDS->GetRasterYSize())
    {
        PDFMargins sMargins;
        nMeasureId = WriteSRS_ISO32000(poSrcDS, 1, nullptr, &sMargins, FALSE);
    }

    GDALPDFDictionaryRW oDict;
    oDict.Add("Type", GDALPDFObjectRW::CreateName("XObject"));

    if (eCompressMethod == COMPRESS_DEFLATE)
    {
        if (nPredictor == 2)
            oDict.Add("DecodeParms", &((new GDALPDFDictionaryRW())
                                           ->Add("Predictor", 2)
                                           .Add("Colors", nBands)
                                           .Add("Columns", nReqXSize)));
    }
    else if (eCompressMethod == COMPRESS_JPEG)
    {
        oDict.Add("Filter", GDALPDFObjectRW::CreateName("DCTDecode"));
    }
    else if (eCompressMethod == COMPRESS_JPEG2000)
    {
        oDict.Add("Filter", GDALPDFObjectRW::CreateName("JPXDecode"));
    }

    oDict.Add("Subtype", GDALPDFObjectRW::CreateName("Image"))
        .Add("Width", nReqXSize)
        .Add("Height", nReqYSize)
        .Add("ColorSpace",
             (nColorTableId.toBool())
                 ? GDALPDFObjectRW::CreateIndirect(nColorTableId, 0)
             : (nBands == 1) ? GDALPDFObjectRW::CreateName("DeviceGray")
                             : GDALPDFObjectRW::CreateName("DeviceRGB"))
        .Add("BitsPerComponent", 8);
    if (nMaskId.toBool())
    {
        oDict.Add("SMask", nMaskId, 0);
    }
    if (nMeasureId.toBool())
    {
        oDict.Add("Measure", nMeasureId, 0);
    }

    StartObjWithStream(nImageId, oDict, eCompressMethod == COMPRESS_DEFLATE);

    if (eCompressMethod == COMPRESS_JPEG ||
        eCompressMethod == COMPRESS_JPEG2000)
    {
        GDALDriver *poJPEGDriver = nullptr;
        std::string osTmpfilename;
        char **papszOptions = nullptr;

        bool bEcwEncodeKeyRequiredButNotFound = false;
        if (eCompressMethod == COMPRESS_JPEG)
        {
            poJPEGDriver = GetGDALDriverManager()->GetDriverByName("JPEG");
            if (poJPEGDriver != nullptr && nJPEGQuality > 0)
                papszOptions = CSLAddString(
                    papszOptions, CPLSPrintf("QUALITY=%d", nJPEGQuality));
            osTmpfilename = VSIMemGenerateHiddenFilename("pdf_temp.jpg");
        }
        else
        {
            if (pszJPEG2000_DRIVER == nullptr ||
                EQUAL(pszJPEG2000_DRIVER, "JP2KAK"))
                poJPEGDriver =
                    GetGDALDriverManager()->GetDriverByName("JP2KAK");
            if (poJPEGDriver == nullptr)
            {
                if (pszJPEG2000_DRIVER == nullptr ||
                    EQUAL(pszJPEG2000_DRIVER, "JP2ECW"))
                {
                    poJPEGDriver =
                        GetGDALDriverManager()->GetDriverByName("JP2ECW");
                    if (poJPEGDriver &&
                        poJPEGDriver->GetMetadataItem(
                            GDAL_DMD_CREATIONDATATYPES) == nullptr)
                    {
                        poJPEGDriver = nullptr;
                    }
                    else if (poJPEGDriver)
                    {
                        if (strstr(poJPEGDriver->GetMetadataItem(
                                       GDAL_DMD_CREATIONOPTIONLIST),
                                   "ECW_ENCODE_KEY"))
                        {
                            if (!CPLGetConfigOption("ECW_ENCODE_KEY", nullptr))
                            {
                                bEcwEncodeKeyRequiredButNotFound = true;
                                poJPEGDriver = nullptr;
                            }
                        }
                    }
                }
                if (poJPEGDriver)
                {
                    papszOptions = CSLAddString(papszOptions, "PROFILE=NPJE");
                    papszOptions = CSLAddString(papszOptions, "LAYERS=1");
                    papszOptions = CSLAddString(papszOptions, "GeoJP2=OFF");
                    papszOptions = CSLAddString(papszOptions, "GMLJP2=OFF");
                }
            }
            if (poJPEGDriver == nullptr)
            {
                if (pszJPEG2000_DRIVER == nullptr ||
                    EQUAL(pszJPEG2000_DRIVER, "JP2OpenJPEG"))
                    poJPEGDriver =
                        GetGDALDriverManager()->GetDriverByName("JP2OpenJPEG");
                if (poJPEGDriver)
                {
                    papszOptions = CSLAddString(papszOptions, "GeoJP2=OFF");
                    papszOptions = CSLAddString(papszOptions, "GMLJP2=OFF");
                }
            }
            osTmpfilename = VSIMemGenerateHiddenFilename("pdf_temp.jp2");
        }

        if (poJPEGDriver == nullptr)
        {
            if (bEcwEncodeKeyRequiredButNotFound)
            {
                CPLError(CE_Failure, CPLE_NotSupported,
                         "No JPEG2000 driver usable (JP2ECW detected but "
                         "ECW_ENCODE_KEY configuration option not set");
            }
            else
            {
                CPLError(CE_Failure, CPLE_NotSupported, "No %s driver found",
                         (eCompressMethod == COMPRESS_JPEG) ? "JPEG"
                                                            : "JPEG2000");
            }
            eErr = CE_Failure;
            goto end;
        }

        GDALDataset *poJPEGDS =
            poJPEGDriver->CreateCopy(osTmpfilename.c_str(), poBlockSrcDS, FALSE,
                                     papszOptions, pfnProgress, pProgressData);

        CSLDestroy(papszOptions);
        if (poJPEGDS == nullptr)
        {
            eErr = CE_Failure;
            goto end;
        }

        GDALClose(poJPEGDS);

        vsi_l_offset nJPEGDataSize = 0;
        GByte *pabyJPEGData =
            VSIGetMemFileBuffer(osTmpfilename.c_str(), &nJPEGDataSize, TRUE);
        VSIFWriteL(pabyJPEGData, static_cast<size_t>(nJPEGDataSize), 1, m_fp);
        CPLFree(pabyJPEGData);
    }
    else
    {
        GByte *pabyLine = static_cast<GByte *>(
            CPLMalloc(static_cast<size_t>(nReqXSize) * nBands));
        for (int iLine = 0; iLine < nReqYSize; iLine++)
        {
            /* Get pixel interleaved data */
            eErr = poBlockSrcDS->RasterIO(
                GF_Read, 0, iLine, nReqXSize, 1, pabyLine, nReqXSize, 1,
                GDT_Byte, nBands, nullptr, nBands, 0, 1, nullptr);
            if (eErr != CE_None)
                break;

            /* Apply predictor if needed */
            if (nPredictor == 2)
            {
                if (nBands == 1)
                {
                    int nPrevValue = pabyLine[0];
                    for (int iPixel = 1; iPixel < nReqXSize; iPixel++)
                    {
                        int nCurValue = pabyLine[iPixel];
                        pabyLine[iPixel] =
                            static_cast<GByte>(nCurValue - nPrevValue);
                        nPrevValue = nCurValue;
                    }
                }
                else if (nBands == 3)
                {
                    int nPrevValueR = pabyLine[0];
                    int nPrevValueG = pabyLine[1];
                    int nPrevValueB = pabyLine[2];
                    for (int iPixel = 1; iPixel < nReqXSize; iPixel++)
                    {
                        int nCurValueR = pabyLine[3 * iPixel + 0];
                        int nCurValueG = pabyLine[3 * iPixel + 1];
                        int nCurValueB = pabyLine[3 * iPixel + 2];
                        pabyLine[3 * iPixel + 0] =
                            static_cast<GByte>(nCurValueR - nPrevValueR);
                        pabyLine[3 * iPixel + 1] =
                            static_cast<GByte>(nCurValueG - nPrevValueG);
                        pabyLine[3 * iPixel + 2] =
                            static_cast<GByte>(nCurValueB - nPrevValueB);
                        nPrevValueR = nCurValueR;
                        nPrevValueG = nCurValueG;
                        nPrevValueB = nCurValueB;
                    }
                }
            }

            if (VSIFWriteL(pabyLine, static_cast<size_t>(nReqXSize) * nBands, 1,
                           m_fp) != 1)
            {
                eErr = CE_Failure;
                break;
            }

            if (pfnProgress != nullptr &&
                !pfnProgress((iLine + 1) / double(nReqYSize), nullptr,
                             pProgressData))
            {
                CPLError(CE_Failure, CPLE_UserInterrupt,
                         "User terminated CreateCopy()");
                eErr = CE_Failure;
                break;
            }
        }

        CPLFree(pabyLine);
    }

end:
    CPLFree(pabyMEMDSBuffer);
    pabyMEMDSBuffer = nullptr;

    EndObjWithStream();

    return eErr == CE_None ? nImageId : GDALPDFObjectNum();
}

/************************************************************************/
/*                          WriteJavascript()                           */
/************************************************************************/

GDALPDFObjectNum GDALPDFBaseWriter::WriteJavascript(const char *pszJavascript,
                                                    bool bDeflate)
{
    auto nJSId = AllocNewObject();
    {
        GDALPDFDictionaryRW oDict;
        StartObjWithStream(nJSId, oDict, bDeflate);

        VSIFWriteL(pszJavascript, strlen(pszJavascript), 1, m_fp);
        VSIFPrintfL(m_fp, "\n");

        EndObjWithStream();
    }

    m_nNamesId = AllocNewObject();
    StartObj(m_nNamesId);
    {
        GDALPDFDictionaryRW oDict;
        GDALPDFDictionaryRW *poJavaScriptDict = new GDALPDFDictionaryRW();
        oDict.Add("JavaScript", poJavaScriptDict);

        GDALPDFArrayRW *poNamesArray = new GDALPDFArrayRW();
        poJavaScriptDict->Add("Names", poNamesArray);

        poNamesArray->Add("GDAL");

        GDALPDFDictionaryRW *poJSDict = new GDALPDFDictionaryRW();
        poNamesArray->Add(poJSDict);

        poJSDict->Add("JS", nJSId, 0);
        poJSDict->Add("S", GDALPDFObjectRW::CreateName("JavaScript"));

        VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());
    }
    EndObj();

    return m_nNamesId;
}

GDALPDFObjectNum GDALPDFWriter::WriteJavascript(const char *pszJavascript)
{
    return GDALPDFBaseWriter::WriteJavascript(
        pszJavascript, oPageContext.eStreamCompressMethod != COMPRESS_NONE);
}

/************************************************************************/
/*                        WriteJavascriptFile()                         */
/************************************************************************/

GDALPDFObjectNum
GDALPDFWriter::WriteJavascriptFile(const char *pszJavascriptFile)
{
    GDALPDFObjectNum nId;
    char *pszJavascriptToFree = static_cast<char *>(CPLMalloc(65536));
    VSILFILE *fpJS = VSIFOpenL(pszJavascriptFile, "rb");
    if (fpJS != nullptr)
    {
        const int nRead =
            static_cast<int>(VSIFReadL(pszJavascriptToFree, 1, 65536, fpJS));
        if (nRead < 65536)
        {
            pszJavascriptToFree[nRead] = '\0';
            nId = WriteJavascript(pszJavascriptToFree);
        }
        VSIFCloseL(fpJS);
    }
    CPLFree(pszJavascriptToFree);
    return nId;
}

/************************************************************************/
/*                              WritePages()                            */
/************************************************************************/

void GDALPDFWriter::WritePages()
{
    StartObj(m_nPageResourceId);
    {
        GDALPDFDictionaryRW oDict;
        GDALPDFArrayRW *poKids = new GDALPDFArrayRW();
        oDict.Add("Type", GDALPDFObjectRW::CreateName("Pages"))
            .Add("Count", static_cast<int>(m_asPageId.size()))
            .Add("Kids", poKids);

        for (size_t i = 0; i < m_asPageId.size(); i++)
            poKids->Add(m_asPageId[i], 0);

        VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());
    }
    EndObj();

    StartObj(m_nCatalogId);
    {
        GDALPDFDictionaryRW oDict;
        oDict.Add("Type", GDALPDFObjectRW::CreateName("Catalog"))
            .Add("Pages", m_nPageResourceId, 0);
        if (m_nXMPId.toBool())
            oDict.Add("Metadata", m_nXMPId, 0);
        if (!m_asOCGs.empty())
        {
            GDALPDFDictionaryRW *poDictOCProperties = new GDALPDFDictionaryRW();
            oDict.Add("OCProperties", poDictOCProperties);

            GDALPDFDictionaryRW *poDictD = new GDALPDFDictionaryRW();
            poDictOCProperties->Add("D", poDictD);

            /* Build "Order" array of D dict */
            GDALPDFArrayRW *poArrayOrder = new GDALPDFArrayRW();
            for (size_t i = 0; i < m_asOCGs.size(); i++)
            {
                poArrayOrder->Add(m_asOCGs[i].nId, 0);
                if (i + 1 < m_asOCGs.size() &&
                    m_asOCGs[i + 1].nParentId == m_asOCGs[i].nId)
                {
                    GDALPDFArrayRW *poSubArrayOrder = new GDALPDFArrayRW();
                    poSubArrayOrder->Add(m_asOCGs[i + 1].nId, 0);
                    poArrayOrder->Add(poSubArrayOrder);
                    i++;
                }
            }
            poDictD->Add("Order", poArrayOrder);

            /* Build "OFF" array of D dict */
            if (!m_osOffLayers.empty())
            {
                GDALPDFArrayRW *poArrayOFF = new GDALPDFArrayRW();
                char **papszTokens = CSLTokenizeString2(m_osOffLayers, ",", 0);
                for (int i = 0; papszTokens[i] != nullptr; i++)
                {
                    size_t j;
                    int bFound = FALSE;
                    for (j = 0; j < m_asOCGs.size(); j++)
                    {
                        if (strcmp(papszTokens[i], m_asOCGs[j].osLayerName) ==
                            0)
                        {
                            poArrayOFF->Add(m_asOCGs[j].nId, 0);
                            bFound = TRUE;
                        }
                        if (j + 1 < m_asOCGs.size() &&
                            m_asOCGs[j + 1].nParentId == m_asOCGs[j].nId)
                        {
                            j++;
                        }
                    }
                    if (!bFound)
                    {
                        CPLError(
                            CE_Warning, CPLE_AppDefined,
                            "Unknown layer name (%s) specified in OFF_LAYERS",
                            papszTokens[i]);
                    }
                }
                CSLDestroy(papszTokens);

                poDictD->Add("OFF", poArrayOFF);
            }

            /* Build "RBGroups" array of D dict */
            if (!m_osExclusiveLayers.empty())
            {
                GDALPDFArrayRW *poArrayRBGroups = new GDALPDFArrayRW();
                char **papszTokens =
                    CSLTokenizeString2(m_osExclusiveLayers, ",", 0);
                for (int i = 0; papszTokens[i] != nullptr; i++)
                {
                    size_t j;
                    int bFound = FALSE;
                    for (j = 0; j < m_asOCGs.size(); j++)
                    {
                        if (strcmp(papszTokens[i], m_asOCGs[j].osLayerName) ==
                            0)
                        {
                            poArrayRBGroups->Add(m_asOCGs[j].nId, 0);
                            bFound = TRUE;
                        }
                        if (j + 1 < m_asOCGs.size() &&
                            m_asOCGs[j + 1].nParentId == m_asOCGs[j].nId)
                        {
                            j++;
                        }
                    }
                    if (!bFound)
                    {
                        CPLError(CE_Warning, CPLE_AppDefined,
                                 "Unknown layer name (%s) specified in "
                                 "EXCLUSIVE_LAYERS",
                                 papszTokens[i]);
                    }
                }
                CSLDestroy(papszTokens);

                if (poArrayRBGroups->GetLength())
                {
                    GDALPDFArrayRW *poMainArrayRBGroups = new GDALPDFArrayRW();
                    poMainArrayRBGroups->Add(poArrayRBGroups);
                    poDictD->Add("RBGroups", poMainArrayRBGroups);
                }
                else
                    delete poArrayRBGroups;
            }

            GDALPDFArrayRW *poArrayOGCs = new GDALPDFArrayRW();
            for (size_t i = 0; i < m_asOCGs.size(); i++)
                poArrayOGCs->Add(m_asOCGs[i].nId, 0);
            poDictOCProperties->Add("OCGs", poArrayOGCs);
        }

        if (m_nStructTreeRootId.toBool())
        {
            GDALPDFDictionaryRW *poDictMarkInfo = new GDALPDFDictionaryRW();
            oDict.Add("MarkInfo", poDictMarkInfo);
            poDictMarkInfo->Add("UserProperties",
                                GDALPDFObjectRW::CreateBool(TRUE));

            oDict.Add("StructTreeRoot", m_nStructTreeRootId, 0);
        }

        if (m_nNamesId.toBool())
            oDict.Add("Names", m_nNamesId, 0);

        VSIFPrintfL(m_fp, "%s\n", oDict.Serialize().c_str());
    }
    EndObj();
}

/************************************************************************/
/*                        GDALPDFGetJPEGQuality()                       */
/************************************************************************/

static int GDALPDFGetJPEGQuality(char **papszOptions)
{
    int nJpegQuality = -1;
    const char *pszValue = CSLFetchNameValue(papszOptions, "JPEG_QUALITY");
    if (pszValue != nullptr)
    {
        nJpegQuality = atoi(pszValue);
        if (!(nJpegQuality >= 1 && nJpegQuality <= 100))
        {
            CPLError(CE_Warning, CPLE_IllegalArg,
                     "JPEG_QUALITY=%s value not recognised, ignoring.",
                     pszValue);
            nJpegQuality = -1;
        }
    }
    return nJpegQuality;
}

/************************************************************************/
/*                         GDALPDFClippingDataset                       */
/************************************************************************/

class GDALPDFClippingDataset final : public GDALDataset
{
    GDALDataset *poSrcDS = nullptr;
    double adfGeoTransform[6];

    CPL_DISALLOW_COPY_ASSIGN(GDALPDFClippingDataset)

  public:
    GDALPDFClippingDataset(GDALDataset *poSrcDSIn, double adfClippingExtent[4])
        : poSrcDS(poSrcDSIn)
    {
        double adfSrcGeoTransform[6];
        poSrcDS->GetGeoTransform(adfSrcGeoTransform);
        adfGeoTransform[0] = adfClippingExtent[0];
        adfGeoTransform[1] = adfSrcGeoTransform[1];
        adfGeoTransform[2] = 0.0;
        adfGeoTransform[3] = adfSrcGeoTransform[5] < 0 ? adfClippingExtent[3]
                                                       : adfClippingExtent[1];
        adfGeoTransform[4] = 0.0;
        adfGeoTransform[5] = adfSrcGeoTransform[5];
        nRasterXSize =
            static_cast<int>((adfClippingExtent[2] - adfClippingExtent[0]) /
                             adfSrcGeoTransform[1]);
        nRasterYSize =
            static_cast<int>((adfClippingExtent[3] - adfClippingExtent[1]) /
                             fabs(adfSrcGeoTransform[5]));
    }

    virtual CPLErr GetGeoTransform(double *padfGeoTransform) override
    {
        memcpy(padfGeoTransform, adfGeoTransform, 6 * sizeof(double));
        return CE_None;
    }

    virtual const OGRSpatialReference *GetSpatialRef() const override
    {
        return poSrcDS->GetSpatialRef();
    }
};

/************************************************************************/
/*                          GDALPDFCreateCopy()                         */
/************************************************************************/

GDALDataset *GDALPDFCreateCopy(const char *pszFilename, GDALDataset *poSrcDS,
                               int bStrict, char **papszOptions,
                               GDALProgressFunc pfnProgress,
                               void *pProgressData)
{
    int nBands = poSrcDS->GetRasterCount();
    int nWidth = poSrcDS->GetRasterXSize();
    int nHeight = poSrcDS->GetRasterYSize();

    if (!pfnProgress(0.0, nullptr, pProgressData))
        return nullptr;

    /* -------------------------------------------------------------------- */
    /*      Some some rudimentary checks                                    */
    /* -------------------------------------------------------------------- */
    if (nBands != 1 && nBands != 3 && nBands != 4)
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "PDF driver doesn't support %d bands.  Must be 1 (grey or "
                 "with color table), "
                 "3 (RGB) or 4 bands.\n",
                 nBands);

        return nullptr;
    }

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

        if (bStrict)
            return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*     Read options.                                                    */
    /* -------------------------------------------------------------------- */
    PDFCompressMethod eCompressMethod = COMPRESS_DEFAULT;
    const char *pszCompressMethod = CSLFetchNameValue(papszOptions, "COMPRESS");
    if (pszCompressMethod)
    {
        if (EQUAL(pszCompressMethod, "NONE"))
            eCompressMethod = COMPRESS_NONE;
        else if (EQUAL(pszCompressMethod, "DEFLATE"))
            eCompressMethod = COMPRESS_DEFLATE;
        else if (EQUAL(pszCompressMethod, "JPEG"))
            eCompressMethod = COMPRESS_JPEG;
        else if (EQUAL(pszCompressMethod, "JPEG2000"))
            eCompressMethod = COMPRESS_JPEG2000;
        else
        {
            CPLError((bStrict) ? CE_Failure : CE_Warning, CPLE_NotSupported,
                     "Unsupported value for COMPRESS.");

            if (bStrict)
                return nullptr;
        }
    }

    PDFCompressMethod eStreamCompressMethod = COMPRESS_DEFLATE;
    const char *pszStreamCompressMethod =
        CSLFetchNameValue(papszOptions, "STREAM_COMPRESS");
    if (pszStreamCompressMethod)
    {
        if (EQUAL(pszStreamCompressMethod, "NONE"))
            eStreamCompressMethod = COMPRESS_NONE;
        else if (EQUAL(pszStreamCompressMethod, "DEFLATE"))
            eStreamCompressMethod = COMPRESS_DEFLATE;
        else
        {
            CPLError((bStrict) ? CE_Failure : CE_Warning, CPLE_NotSupported,
                     "Unsupported value for STREAM_COMPRESS.");

            if (bStrict)
                return nullptr;
        }
    }

    if (nBands == 1 && poSrcDS->GetRasterBand(1)->GetColorTable() != nullptr &&
        (eCompressMethod == COMPRESS_JPEG ||
         eCompressMethod == COMPRESS_JPEG2000))
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "The source raster band has a color table, which is not "
                 "appropriate with JPEG or JPEG2000 compression.\n"
                 "You should rather consider using color table expansion "
                 "(-expand option in gdal_translate)");
    }

    int nBlockXSize = nWidth;
    int nBlockYSize = nHeight;

    const bool bTiled = CPLFetchBool(papszOptions, "TILED", false);
    if (bTiled)
    {
        nBlockXSize = 256;
        nBlockYSize = 256;
    }

    const char *pszValue = CSLFetchNameValue(papszOptions, "BLOCKXSIZE");
    if (pszValue != nullptr)
    {
        nBlockXSize = atoi(pszValue);
        if (nBlockXSize <= 0 || nBlockXSize >= nWidth)
            nBlockXSize = nWidth;
    }

    pszValue = CSLFetchNameValue(papszOptions, "BLOCKYSIZE");
    if (pszValue != nullptr)
    {
        nBlockYSize = atoi(pszValue);
        if (nBlockYSize <= 0 || nBlockYSize >= nHeight)
            nBlockYSize = nHeight;
    }

    int nJPEGQuality = GDALPDFGetJPEGQuality(papszOptions);

    const char *pszJPEG2000_DRIVER =
        CSLFetchNameValue(papszOptions, "JPEG2000_DRIVER");

    const char *pszGEO_ENCODING =
        CSLFetchNameValueDef(papszOptions, "GEO_ENCODING", "ISO32000");

    const char *pszXMP = CSLFetchNameValue(papszOptions, "XMP");

    const char *pszPredictor = CSLFetchNameValue(papszOptions, "PREDICTOR");
    int nPredictor = 1;
    if (pszPredictor)
    {
        if (eCompressMethod == COMPRESS_DEFAULT)
            eCompressMethod = COMPRESS_DEFLATE;

        if (eCompressMethod != COMPRESS_DEFLATE)
        {
            CPLError(CE_Warning, CPLE_NotSupported,
                     "PREDICTOR option is only taken into account for DEFLATE "
                     "compression");
        }
        else
        {
            nPredictor = atoi(pszPredictor);
            if (nPredictor != 1 && nPredictor != 2)
            {
                CPLError(CE_Warning, CPLE_NotSupported,
                         "Supported PREDICTOR values are 1 or 2");
                nPredictor = 1;
            }
        }
    }

    const char *pszNEATLINE = CSLFetchNameValue(papszOptions, "NEATLINE");

    int nMargin = atoi(CSLFetchNameValueDef(papszOptions, "MARGIN", "0"));

    PDFMargins sMargins;
    sMargins.nLeft = nMargin;
    sMargins.nRight = nMargin;
    sMargins.nTop = nMargin;
    sMargins.nBottom = nMargin;

    const char *pszLeftMargin = CSLFetchNameValue(papszOptions, "LEFT_MARGIN");
    if (pszLeftMargin)
        sMargins.nLeft = atoi(pszLeftMargin);

    const char *pszRightMargin =
        CSLFetchNameValue(papszOptions, "RIGHT_MARGIN");
    if (pszRightMargin)
        sMargins.nRight = atoi(pszRightMargin);

    const char *pszTopMargin = CSLFetchNameValue(papszOptions, "TOP_MARGIN");
    if (pszTopMargin)
        sMargins.nTop = atoi(pszTopMargin);

    const char *pszBottomMargin =
        CSLFetchNameValue(papszOptions, "BOTTOM_MARGIN");
    if (pszBottomMargin)
        sMargins.nBottom = atoi(pszBottomMargin);

    const char *pszDPI = CSLFetchNameValue(papszOptions, "DPI");
    double dfDPI = DEFAULT_DPI;
    if (pszDPI != nullptr)
        dfDPI = CPLAtof(pszDPI);

    const char *pszWriteUserUnit =
        CSLFetchNameValue(papszOptions, "WRITE_USERUNIT");
    bool bWriteUserUnit;
    if (pszWriteUserUnit != nullptr)
        bWriteUserUnit = CPLTestBool(pszWriteUserUnit);
    else
        bWriteUserUnit = (pszDPI == nullptr);

    double dfUserUnit = dfDPI * USER_UNIT_IN_INCH;
    double dfWidthInUserUnit =
        nWidth / dfUserUnit + sMargins.nLeft + sMargins.nRight;
    double dfHeightInUserUnit =
        nHeight / dfUserUnit + sMargins.nBottom + sMargins.nTop;
    if (dfWidthInUserUnit > MAXIMUM_SIZE_IN_UNITS ||
        dfHeightInUserUnit > MAXIMUM_SIZE_IN_UNITS)
    {
        if (pszDPI == nullptr)
        {
            if (sMargins.nLeft + sMargins.nRight >= MAXIMUM_SIZE_IN_UNITS ||
                sMargins.nBottom + sMargins.nTop >= MAXIMUM_SIZE_IN_UNITS)
            {
                CPLError(
                    CE_Warning, CPLE_AppDefined,
                    "Margins too big compared to maximum page dimension (%d) "
                    "in user units allowed by Acrobat",
                    MAXIMUM_SIZE_IN_UNITS);
            }
            else
            {
                if (dfWidthInUserUnit >= dfHeightInUserUnit)
                {
                    dfDPI = ceil(double(nWidth) /
                                 (MAXIMUM_SIZE_IN_UNITS -
                                  (sMargins.nLeft + sMargins.nRight)) /
                                 USER_UNIT_IN_INCH);
                }
                else
                {
                    dfDPI = ceil(double(nHeight) /
                                 (MAXIMUM_SIZE_IN_UNITS -
                                  (sMargins.nBottom + sMargins.nTop)) /
                                 USER_UNIT_IN_INCH);
                }
                CPLDebug("PDF",
                         "Adjusting DPI to %d so that page dimension in "
                         "user units remain in what is accepted by Acrobat",
                         static_cast<int>(dfDPI));
            }
        }
        else
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "The page dimension in user units is %d x %d whereas the "
                     "maximum allowed by Acrobat is %d x %d",
                     static_cast<int>(dfWidthInUserUnit + 0.5),
                     static_cast<int>(dfHeightInUserUnit + 0.5),
                     MAXIMUM_SIZE_IN_UNITS, MAXIMUM_SIZE_IN_UNITS);
        }
    }

    if (dfDPI < DEFAULT_DPI)
        dfDPI = DEFAULT_DPI;

    const char *pszClippingExtent =
        CSLFetchNameValue(papszOptions, "CLIPPING_EXTENT");
    int bUseClippingExtent = FALSE;
    double adfClippingExtent[4] = {0.0, 0.0, 0.0, 0.0};
    if (pszClippingExtent != nullptr)
    {
        char **papszTokens = CSLTokenizeString2(pszClippingExtent, ",", 0);
        if (CSLCount(papszTokens) == 4)
        {
            bUseClippingExtent = TRUE;
            adfClippingExtent[0] = CPLAtof(papszTokens[0]);
            adfClippingExtent[1] = CPLAtof(papszTokens[1]);
            adfClippingExtent[2] = CPLAtof(papszTokens[2]);
            adfClippingExtent[3] = CPLAtof(papszTokens[3]);
            if (adfClippingExtent[0] > adfClippingExtent[2] ||
                adfClippingExtent[1] > adfClippingExtent[3])
            {
                CPLError(CE_Warning, CPLE_AppDefined,
                         "Invalid value for CLIPPING_EXTENT. Should be "
                         "xmin,ymin,xmax,ymax");
                bUseClippingExtent = FALSE;
            }

            if (bUseClippingExtent)
            {
                double adfGeoTransform[6];
                if (poSrcDS->GetGeoTransform(adfGeoTransform) == CE_None)
                {
                    if (adfGeoTransform[2] != 0.0 || adfGeoTransform[4] != 0.0)
                    {
                        CPLError(CE_Warning, CPLE_AppDefined,
                                 "Cannot use CLIPPING_EXTENT because main "
                                 "raster has a rotated geotransform");
                        bUseClippingExtent = FALSE;
                    }
                }
                else
                {
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "Cannot use CLIPPING_EXTENT because main raster "
                             "has no geotransform");
                    bUseClippingExtent = FALSE;
                }
            }
        }
        CSLDestroy(papszTokens);
    }

    const char *pszLayerName = CSLFetchNameValue(papszOptions, "LAYER_NAME");

    const char *pszExtraImages =
        CSLFetchNameValue(papszOptions, "EXTRA_IMAGES");
    const char *pszExtraStream =
        CSLFetchNameValue(papszOptions, "EXTRA_STREAM");
    const char *pszExtraLayerName =
        CSLFetchNameValue(papszOptions, "EXTRA_LAYER_NAME");

    const char *pszOGRDataSource =
        CSLFetchNameValue(papszOptions, "OGR_DATASOURCE");
    const char *pszOGRDisplayField =
        CSLFetchNameValue(papszOptions, "OGR_DISPLAY_FIELD");
    const char *pszOGRDisplayLayerNames =
        CSLFetchNameValue(papszOptions, "OGR_DISPLAY_LAYER_NAMES");
    const char *pszOGRLinkField =
        CSLFetchNameValue(papszOptions, "OGR_LINK_FIELD");
    const bool bWriteOGRAttributes =
        CPLFetchBool(papszOptions, "OGR_WRITE_ATTRIBUTES", true);

    const char *pszExtraRasters =
        CSLFetchNameValue(papszOptions, "EXTRA_RASTERS");
    const char *pszExtraRastersLayerName =
        CSLFetchNameValue(papszOptions, "EXTRA_RASTERS_LAYER_NAME");

    const char *pszOffLayers = CSLFetchNameValue(papszOptions, "OFF_LAYERS");
    const char *pszExclusiveLayers =
        CSLFetchNameValue(papszOptions, "EXCLUSIVE_LAYERS");

    const char *pszJavascript = CSLFetchNameValue(papszOptions, "JAVASCRIPT");
    const char *pszJavascriptFile =
        CSLFetchNameValue(papszOptions, "JAVASCRIPT_FILE");

    /* -------------------------------------------------------------------- */
    /*      Create file.                                                    */
    /* -------------------------------------------------------------------- */
    VSILFILE *fp = VSIFOpenL(pszFilename, "wb");
    if (fp == nullptr)
    {
        CPLError(CE_Failure, CPLE_OpenFailed, "Unable to create PDF file %s.\n",
                 pszFilename);
        return nullptr;
    }

    GDALPDFWriter oWriter(fp);

    GDALDataset *poClippingDS = poSrcDS;
    if (bUseClippingExtent)
        poClippingDS = new GDALPDFClippingDataset(poSrcDS, adfClippingExtent);

    if (CPLFetchBool(papszOptions, "WRITE_INFO", true))
        oWriter.SetInfo(poSrcDS, papszOptions);
    oWriter.SetXMP(poClippingDS, pszXMP);

    oWriter.StartPage(poClippingDS, dfDPI, bWriteUserUnit, pszGEO_ENCODING,
                      pszNEATLINE, &sMargins, eStreamCompressMethod,
                      pszOGRDataSource != nullptr && bWriteOGRAttributes);

    int bRet;

    if (!bUseClippingExtent)
    {
        bRet = oWriter.WriteImagery(poSrcDS, pszLayerName, eCompressMethod,
                                    nPredictor, nJPEGQuality,
                                    pszJPEG2000_DRIVER, nBlockXSize,
                                    nBlockYSize, pfnProgress, pProgressData);
    }
    else
    {
        bRet = oWriter.WriteClippedImagery(
            poSrcDS, pszLayerName, eCompressMethod, nPredictor, nJPEGQuality,
            pszJPEG2000_DRIVER, nBlockXSize, nBlockYSize, pfnProgress,
            pProgressData);
    }

    char **papszExtraRasters =
        CSLTokenizeString2(pszExtraRasters ? pszExtraRasters : "", ",", 0);
    char **papszExtraRastersLayerName = CSLTokenizeString2(
        pszExtraRastersLayerName ? pszExtraRastersLayerName : "", ",", 0);
    int bUseExtraRastersLayerName =
        (CSLCount(papszExtraRasters) == CSLCount(papszExtraRastersLayerName));
    int bUseExtraRasters = TRUE;

    const char *pszClippingProjectionRef = poSrcDS->GetProjectionRef();
    if (CSLCount(papszExtraRasters) != 0)
    {
        double adfGeoTransform[6];
        if (poSrcDS->GetGeoTransform(adfGeoTransform) == CE_None)
        {
            if (adfGeoTransform[2] != 0.0 || adfGeoTransform[4] != 0.0)
            {
                CPLError(CE_Warning, CPLE_AppDefined,
                         "Cannot use EXTRA_RASTERS because main raster has a "
                         "rotated geotransform");
                bUseExtraRasters = FALSE;
            }
        }
        else
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "Cannot use EXTRA_RASTERS because main raster has no "
                     "geotransform");
            bUseExtraRasters = FALSE;
        }
        if (bUseExtraRasters && (pszClippingProjectionRef == nullptr ||
                                 pszClippingProjectionRef[0] == '\0'))
        {
            CPLError(CE_Warning, CPLE_AppDefined,
                     "Cannot use EXTRA_RASTERS because main raster has no "
                     "projection");
            bUseExtraRasters = FALSE;
        }
    }

    for (int i = 0; bRet && bUseExtraRasters && papszExtraRasters[i] != nullptr;
         i++)
    {
        auto poDS = std::unique_ptr<GDALDataset>(GDALDataset::Open(
            papszExtraRasters[i], GDAL_OF_RASTER | GDAL_OF_VERBOSE_ERROR,
            nullptr, nullptr, nullptr));
        if (poDS != nullptr)
        {
            double adfGeoTransform[6];
            int bUseRaster = TRUE;
            if (poDS->GetGeoTransform(adfGeoTransform) == CE_None)
            {
                if (adfGeoTransform[2] != 0.0 || adfGeoTransform[4] != 0.0)
                {
                    CPLError(
                        CE_Warning, CPLE_AppDefined,
                        "Cannot use %s because it has a rotated geotransform",
                        papszExtraRasters[i]);
                    bUseRaster = FALSE;
                }
            }
            else
            {
                CPLError(CE_Warning, CPLE_AppDefined,
                         "Cannot use %s because it has no geotransform",
                         papszExtraRasters[i]);
                bUseRaster = FALSE;
            }
            const char *pszProjectionRef = poDS->GetProjectionRef();
            if (bUseRaster &&
                (pszProjectionRef == nullptr || pszProjectionRef[0] == '\0'))
            {
                CPLError(CE_Warning, CPLE_AppDefined,
                         "Cannot use %s because it has no projection",
                         papszExtraRasters[i]);
                bUseRaster = FALSE;
            }
            if (bUseRaster)
            {
                if (pszClippingProjectionRef != nullptr &&
                    pszProjectionRef != nullptr &&
                    !EQUAL(pszClippingProjectionRef, pszProjectionRef))
                {
                    OGRSpatialReferenceH hClippingSRS =
                        OSRNewSpatialReference(pszClippingProjectionRef);
                    OGRSpatialReferenceH hSRS =
                        OSRNewSpatialReference(pszProjectionRef);
                    if (!OSRIsSame(hClippingSRS, hSRS))
                    {
                        CPLError(CE_Warning, CPLE_AppDefined,
                                 "Cannot use %s because it has a different "
                                 "projection than main dataset",
                                 papszExtraRasters[i]);
                        bUseRaster = FALSE;
                    }
                    OSRDestroySpatialReference(hClippingSRS);
                    OSRDestroySpatialReference(hSRS);
                }
            }
            if (bUseRaster)
            {
                bRet = oWriter.WriteClippedImagery(
                    poDS.get(),
                    bUseExtraRastersLayerName ? papszExtraRastersLayerName[i]
                                              : nullptr,
                    eCompressMethod, nPredictor, nJPEGQuality,
                    pszJPEG2000_DRIVER, nBlockXSize, nBlockYSize, nullptr,
                    nullptr);
            }
        }
    }

    CSLDestroy(papszExtraRasters);
    CSLDestroy(papszExtraRastersLayerName);

    if (bRet && pszOGRDataSource != nullptr)
        oWriter.WriteOGRDataSource(pszOGRDataSource, pszOGRDisplayField,
                                   pszOGRDisplayLayerNames, pszOGRLinkField,
                                   bWriteOGRAttributes);

    if (bRet)
        oWriter.EndPage(pszExtraImages, pszExtraStream, pszExtraLayerName,
                        pszOffLayers, pszExclusiveLayers);

    if (pszJavascript)
        oWriter.WriteJavascript(pszJavascript);
    else if (pszJavascriptFile)
        oWriter.WriteJavascriptFile(pszJavascriptFile);

    oWriter.Close();

    if (poClippingDS != poSrcDS)
        delete poClippingDS;

    if (!bRet)
    {
        VSIUnlink(pszFilename);
        return nullptr;
    }
    else
    {
#ifdef HAVE_PDF_READ_SUPPORT
        GDALDataset *poDS = GDALPDFOpen(pszFilename, GA_ReadOnly);
        if (poDS == nullptr)
            return nullptr;
        char **papszMD = CSLDuplicate(poSrcDS->GetMetadata());
        papszMD = CSLMerge(papszMD, poDS->GetMetadata());
        const char *pszAOP = CSLFetchNameValue(papszMD, GDALMD_AREA_OR_POINT);
        if (pszAOP != nullptr && EQUAL(pszAOP, GDALMD_AOP_AREA))
            papszMD = CSLSetNameValue(papszMD, GDALMD_AREA_OR_POINT, nullptr);
        poDS->SetMetadata(papszMD);
        if (EQUAL(pszGEO_ENCODING, "NONE"))
        {
            double adfGeoTransform[6];
            if (poSrcDS->GetGeoTransform(adfGeoTransform) == CE_None)
            {
                poDS->SetGeoTransform(adfGeoTransform);
            }
            const char *pszProjectionRef = poSrcDS->GetProjectionRef();
            if (pszProjectionRef != nullptr && pszProjectionRef[0] != '\0')
            {
                poDS->SetProjection(pszProjectionRef);
            }
        }
        CSLDestroy(papszMD);
        return poDS;
#else
        return new GDALFakePDFDataset();
#endif
    }
}
